Usage of BooleanDetector in python has unexpected results

Dear all,

I have been a member of this forum for a while, but needed to create a new user due to a change of job, and my previous email doesn’t work anymore :slight_smile:

I am trying to create a script with a FieldOfViewDetector and record events whenever a target on the ground is within the spacecraft’s FoV. From other posts of this forum I tried creating it by adding the elevation detector and then a Boolean detector so the FieldOfViewDetector detects the target only when it is visible from the horizon. The problem is that it seems that the results are completely governed by the ElevationDetector, i.e., the events occur as soon as the spacecraft is above the target horizon. Indeed, even drastically decreasing the FoV of the sensor onboard, the results seem to remain practically the same → the impression I get is that the results are using a OR rather than AND, even though I’m using the “BooleanDetector.andCombine”
Could I kindly get a feedback on what I’m doing wrong here? I would be much thankful.

Thank you so much in advance.

Leonardo.

import orekit
vm = orekit.initVM()
from orekit.pyhelpers import setup_orekit_curdir
setup_orekit_curdir()

from org.orekit.orbits import KeplerianOrbit
from org.orekit.orbits import PositionAngle

from org.orekit.propagation.analytical import KeplerianPropagator
from org.orekit.attitudes import NadirPointing

from org.hipparchus.geometry.euclidean.threed import Vector3D
from org.orekit.propagation.events import BooleanDetector

from org.orekit.propagation.events import ElevationDetector
from org.orekit.propagation.events import FieldOfViewDetector
from org.orekit.propagation.events import EventsLogger
from org.orekit.propagation.events.handlers import ContinueOnEvent

from org.orekit.frames import FramesFactory, TopocentricFrame
from org.orekit.bodies import OneAxisEllipsoid, GeodeticPoint
from org.orekit.utils import Constants, IERSConventions

from org.orekit.geometry.fov import DoubleDihedraFieldOfView

from org.orekit.time import TimeScalesFactory, AbsoluteDate

from orekit.pyhelpers import absolutedate_to_datetime

from math import radians
from datetime import datetime
import pandas as pd

utc = TimeScalesFactory.getUTC()
gcrf = FramesFactory.getGCRF()
itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, False)
eci  = gcrf  # Inertial frame
ecef = itrf  # Non-inertial frame
earth = OneAxisEllipsoid(
    Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
    Constants.WGS84_EARTH_FLATTENING,
    ecef)

nadirPointing = NadirPointing(eci, earth)

duration = 20*24*3600.0 # seconds
step   = 60.0 # seconds

'''
Spacecraft Definition
'''
mass = 25.0 # [kg]
cross_section = 0.08 # [m2]
cd = 2.2
cr = 1.8

'''
Keplerian Orbit
'''
epoch = datetime(2026, 1, 1, 10, 30, 0)
ra = 375.0         #  Apogee
rp = 375.0        #  Perigee
i =  15.0#ssoInclination(ra, rp) # inclination
omega = 90.0     # perigee argument
raan = 130.0  # RAAN
lv = 45.0    # True anomaly

sma = (ra*1e3+rp*1e3+2*Constants.WGS84_EARTH_EQUATORIAL_RADIUS)/2.0
ecc = abs((ra*1e3 - rp*1e3)/(ra*1e3 - rp*1e3 + 2*Constants.WGS84_EARTH_EQUATORIAL_RADIUS))

        
startTime = AbsoluteDate(epoch.year,epoch.month,epoch.day,
                         epoch.hour,epoch.minute,float(epoch.second),utc)
endTime = startTime.shiftedBy(duration)
        
initialOrbit = KeplerianOrbit(
    sma, ecc,
    radians(i), radians(omega),
    radians(raan), radians(lv),PositionAngle.TRUE,
    eci, startTime, Constants.WGS84_EARTH_MU)
propagator = KeplerianPropagator(
    initialOrbit,
    nadirPointing,
    Constants.WGS84_EARTH_MU,
    mass)

'''
Target
'''
targetLatitude = -0.994824
targetLongitude = -52.711238
point = GeodeticPoint(radians(targetLatitude),radians(targetLongitude), 0.0)
target = TopocentricFrame(earth, point, 'target')

'''
Sensor & Detector definition
'''
cameraFOV  = 40.0
sensorFOV = DoubleDihedraFieldOfView(
    Vector3D.PLUS_K, Vector3D.PLUS_I, radians(cameraFOV),
    Vector3D.PLUS_J, radians(cameraFOV), 0.0)
fovDetector = FieldOfViewDetector(target, sensorFOV).withHandler(ContinueOnEvent())
elevationDetector = ElevationDetector(target).withConstantElevation(0.0).withHandler(ContinueOnEvent())
finalDetector = BooleanDetector.andCombine([elevationDetector,fovDetector])
fovLogger = EventsLogger()
logged_detector = fovLogger.monitorDetector(finalDetector)
propagator.addEventDetector(logged_detector)

while(startTime.compareTo(endTime) <= 0.0):

    pvECI = propagator.propagate(startTime).getPVCoordinates()
    pvPosition = pvECI.getPosition()
    pvVelocity = pvECI.getVelocity()

    print('{} {} {} {} {} {} {}'.format(
        absolutedate_to_datetime(startTime),
        pvPosition.getX()/1e3,
        pvPosition.getY()/1e3,
        pvPosition.getZ()/1e3,
        pvVelocity.getX()/1e3,
        pvVelocity.getY()/1e3,
        pvVelocity.getZ()/1e3))
    startTime = startTime.shiftedBy(step)

start_time = None
contacts = pd.DataFrame()
events = fovLogger.getLoggedEvents()
for event in fovLogger.getLoggedEvents():
    if event.isIncreasing():
        start_time = event.getState().getDate()
    elif start_time:
        stop_time = event.getState().getDate()
        contacts = contacts.append({"Duration (s)": stop_time.durationFrom(start_time),
                                    "Start Time (UTC)": absolutedate_to_datetime(start_time),
                                    "Stop Time (UTC)": absolutedate_to_datetime(stop_time)},
                                    ignore_index=True)
        contacts.to_csv('Accesses_FoV_40deg', sep='\t', index=False)
        start_time = None

print(contacts)

Hi @leoghizoni,

Welcome back to the forum then !

I think you need to negate the FieldOfViewDetector detector when doing the andCombine, either with NegateDetector(fovDetector) or BooleanDetector.notCombine(fovDetector ).

g function of the FOV detector is negative when target is in FOV.
g function of elevation detector is positive when target elavation is above user-defined threshold.
g function of andCombine is positive when all detectors are positive, and negative otherwise.

So, in your code, you’re actually checking events where elevation is above threshold and target is not in FOV.
Maybe you could read this former post to have some more explanations.

Usage of BooleanDetector can be tricky because you need to be extra careful with the sign of the g functions of the combined detectors.

Hope this helps,
Maxime

1 Like

Dear Maxime,

Thank you so much for the feedback. I’ve implemented your inputs, and it works like a charm now :slight_smile:

It was a huge help, so many thanks for that!

Best regards,
Leonardo.

1 Like

Dear @MaximeJ ,

I have one more question, if you don’t mind.

What is the best solution if I want to get contacts only at day time, i.e., not consider any of the events when in eclipse? I guess I need to use the BooleanDetector again, but would it be with the EclipseDetector? I saw from other posts the usage of the GroundAtNightDetector, but am confused on how to use it and combine with the other detectors in the Boolean one.
Would you be kind to give me a light on this one?

And many thanks again.

Br,
Leonardo.

Hi @leoghizoni,

You’re very welcome ! We’re always happy to help!

If you want the satellite not to be in eclipse, then it is an EclipseDetector you need.
If you look at the Javadoc of the g function, it says:

* This function becomes negative when entering the region of shadow
* and positive when exiting.

So you can use it directly in the BooleanDetector.andCombine since you want the positive events (you don’t need to negate it).

A GroundAtNightDetector would be used if you want the ground station to be in the shadows. Typically you would use this for a telescope.
The g function of this one states that

* The {@code g} function of this detector is positive when ground is at night
* (i.e. Sun is below dawn/dusk elevation angle).

Once again you can use it “as is” in an andCombine detector for a telescope application.

Note that for a telescope you would need both an EclipseDetector (satellite needs to reflect sun light) and a GroundAtNightDetector (it must be dark on the ground to see the satellite).

Best regards,
Maxime

1 Like

Dear @MaximeJ ,

Thank you so much again. I really appreciate how welcoming and supporting this forum is!

So, maybe I expressed myself a little wrong (sorry for that): what I aim to achieve is to get the FOV events only at day time for the target itself (it is an optical camera onboard the spacecraft with a sensor on the visible spectrum), so the target cannot be in the shadows for it to be imaged. I’ve previously implemented the BooleanDetector in the correct way that you pointed me to, and now tried implementing the GroundAtNightDetector in a similar fashion. Would you be kind to let me know if this looks correct?

import orekit
vm = orekit.initVM()
from orekit.pyhelpers import setup_orekit_curdir
setup_orekit_curdir()

from org.orekit.orbits import KeplerianOrbit
from org.orekit.orbits import PositionAngle

from org.orekit.propagation.analytical import KeplerianPropagator
from org.orekit.attitudes import NadirPointing

from org.hipparchus.geometry.euclidean.threed import Vector3D
from org.orekit.propagation.events import BooleanDetector

from org.orekit.propagation.events import ElevationDetector
from org.orekit.propagation.events import GroundAtNightDetector
from org.orekit.propagation.events import FieldOfViewDetector
from org.orekit.propagation.events import EventsLogger
from org.orekit.propagation.events.handlers import ContinueOnEvent

from org.orekit.frames import FramesFactory, TopocentricFrame
from org.orekit.bodies import OneAxisEllipsoid, GeodeticPoint
from org.orekit.utils import Constants, IERSConventions

from org.orekit.geometry.fov import DoubleDihedraFieldOfView

from org.orekit.time import TimeScalesFactory, AbsoluteDate

from orekit.pyhelpers import absolutedate_to_datetime

from math import radians
from datetime import datetime
import pandas as pd

utc = TimeScalesFactory.getUTC()
gcrf = FramesFactory.getGCRF()
itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, False)
eci  = gcrf  # Inertial frame
ecef = itrf  # Non-inertial frame
earth = OneAxisEllipsoid(
    Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
    Constants.WGS84_EARTH_FLATTENING,
    ecef)
sun  = CelestialBodyFactory.getSun()

nadirPointing = NadirPointing(eci, earth)

duration = 20*24*3600.0 # seconds
step   = 60.0 # seconds

'''
Spacecraft Definition
'''
mass = 25.0 # [kg]
cross_section = 0.08 # [m2]
cd = 2.2
cr = 1.8

'''
Keplerian Orbit
'''
epoch = datetime(2026, 1, 1, 10, 30, 0)
ra = 375.0         #  Apogee
rp = 375.0        #  Perigee
i =  15.0#ssoInclination(ra, rp) # inclination
omega = 90.0     # perigee argument
raan = 130.0  # RAAN
lv = 45.0    # True anomaly

sma = (ra*1e3+rp*1e3+2*Constants.WGS84_EARTH_EQUATORIAL_RADIUS)/2.0
ecc = abs((ra*1e3 - rp*1e3)/(ra*1e3 - rp*1e3 + 2*Constants.WGS84_EARTH_EQUATORIAL_RADIUS))

        
startTime = AbsoluteDate(epoch.year,epoch.month,epoch.day,
                         epoch.hour,epoch.minute,float(epoch.second),utc)
endTime = startTime.shiftedBy(duration)
        
initialOrbit = KeplerianOrbit(
    sma, ecc,
    radians(i), radians(omega),
    radians(raan), radians(lv),PositionAngle.TRUE,
    eci, startTime, Constants.WGS84_EARTH_MU)
propagator = KeplerianPropagator(
    initialOrbit,
    nadirPointing,
    Constants.WGS84_EARTH_MU,
    mass)

'''
Target
'''
targetLatitude = -0.994824
targetLongitude = -52.711238
point = GeodeticPoint(radians(targetLatitude),radians(targetLongitude), 0.0)
target = TopocentricFrame(earth, point, 'target')

'''
Sensor & Detector definition
'''
cameraFOV  = 40.0
sensorFOV = DoubleDihedraFieldOfView(
    Vector3D.PLUS_K, Vector3D.PLUS_I, radians(cameraFOV),
    Vector3D.PLUS_J, radians(cameraFOV), 0.0)
fovDetector = FieldOfViewDetector(target, sensorFOV).withHandler(ContinueOnEvent())
elevationDetector = ElevationDetector(target).withConstantElevation(0.0).withHandler(ContinueOnEvent())
groundInSunDetector = GroundAtNightDetector(target, sun, GroundAtNightDetector.CIVIL_DAWN_DUSK_ELEVATION, None).withHandler(ContinueOnEvent())
finalDetector = BooleanDetector.andCombine([
					elevationDetector,
					BooleanDetector.notCombine(groundInSunDetector),
					BooleanDetector.notCombine(fovDetector)])
fovLogger = EventsLogger()
logged_detector = fovLogger.monitorDetector(finalDetector)
propagator.addEventDetector(logged_detector)

while(startTime.compareTo(endTime) <= 0.0):

    pvECI = propagator.propagate(startTime).getPVCoordinates()
    pvPosition = pvECI.getPosition()
    pvVelocity = pvECI.getVelocity()

    print('{} {} {} {} {} {} {}'.format(
        absolutedate_to_datetime(startTime),
        pvPosition.getX()/1e3,
        pvPosition.getY()/1e3,
        pvPosition.getZ()/1e3,
        pvVelocity.getX()/1e3,
        pvVelocity.getY()/1e3,
        pvVelocity.getZ()/1e3))
    startTime = startTime.shiftedBy(step)

start_time = None
contacts = pd.DataFrame()
events = fovLogger.getLoggedEvents()
for event in fovLogger.getLoggedEvents():
    if event.isIncreasing():
        start_time = event.getState().getDate()
    elif start_time:
        stop_time = event.getState().getDate()
        contacts = contacts.append({"Duration (s)": stop_time.durationFrom(start_time),
                                    "Start Time (UTC)": absolutedate_to_datetime(start_time),
                                    "Stop Time (UTC)": absolutedate_to_datetime(stop_time)},
                                    ignore_index=True)
        contacts.to_csv('Accesses_FoV_40deg', sep='\t', index=False)
        start_time = None

print(contacts)

And thank you so much again! :slightly_smiling_face:

Hi again,

I’ve taken a very quick look at your code and it looks good to me: you want the g function to be positive when the ground target is in daylight, so you negate the GroundAtNightDetector :+1:

1 Like

Dear @MaximeJ ,

Super! Many many thanks for your help! :slightly_smiling_face:

1 Like