Detector missing events (GroundAtNightDetector + ElevationDetector)

Hello everybody,

Sorry for my basic question.
I have the following problem i would like to implement. Using the TLE and the SGP4 integrator i would like to detect when the S/C passes over Earth point (Lat and Long) but only when the point is in night.
I implemented the following code:

import os
import math
import datetime as dt
import pandas as pd

import jpype
from jpype import JImplements, JOverride
import jdk4py
import orekit_jpype as orekit

# ---------------------------------------------------------------------
#  Orekit / JVM bootstrap
# ---------------------------------------------------------------------
def start_jvm_and_load_orekit(data_path: str | None = None) -> None:
    """
    Start the JVM (if not already running) and initialise the Orekit
    data context. Looks for the JVM shared library under JAVA_HOME.
    """
    # Resolve path where Orekit data are stored
    if data_path is None:
        base_path = os.path.join("C:", "Users", os.getlogin())
        data_path = os.environ.get("PATH_OREKIT", os.path.join(base_path, "Documents", "orekit"))

    # Locate the JVM shared library on Windows
    libjvm_path = os.path.join(jdk4py.JAVA_HOME, "bin", "server", "jvm.dll")
    if not os.path.exists(libjvm_path):
        raise FileNotFoundError(f"Could not find jvm.dll at {libjvm_path}")

    # Start JVM if necessary
    if not jpype.isJVMStarted():
        orekit.initVM(jvmpath=libjvm_path)
        from orekit_jpype.pyhelpers import setup_orekit_data
        setup_orekit_data(filenames=data_path, from_pip_library=False)
    else:
        print("JVM already started; skipping initialisation.")


start_jvm_and_load_orekit()

# ---------------------------------------------------------------------
#  Java / Orekit imports (must be done **after** the JVM is up)
# ---------------------------------------------------------------------
from orekit_jpype.pyhelpers import (
    setup_orekit_curdir,
    absolutedate_to_datetime,
    to_elevationmask,
    datetime_to_absolutedate,
)

from org.orekit.data import DataProvidersManager, DirectoryCrawler
from org.orekit.utils import IERSConventions, Constants, PVCoordinatesProvider
from org.orekit.frames import FramesFactory, TopocentricFrame
from org.orekit.time import AbsoluteDate, TimeScalesFactory
from org.orekit.bodies import OneAxisEllipsoid, CelestialBodyFactory, GeodeticPoint
from org.orekit.models.earth import Geoid
from org.orekit.propagation.analytical.tle import TLE, TLEPropagator
from org.orekit.propagation.events import (
    ElevationDetector,
    EventEnablingPredicateFilter,
    EventsLogger,
    NodeDetector,
    EventSlopeFilter,
    FilterType,
    GroundAtNightDetector,
    BooleanDetector,
)
from org.orekit.propagation.events.handlers import ContinueOnEvent
from org.orekit.orbits import KeplerianOrbit
from org.orekit.propagation.sampling import OrekitFixedStepHandler
from java.util.function import Predicate  # noqa: F401  (kept for completeness)

# ---------------------------------------------------------------------
#  User-defined configuration
# ---------------------------------------------------------------------
OBS_LAT    = 43.3           # Observer latitude  (deg)
OBS_LON    = 11.3           # Observer longitude (deg)
OBS_ALT    = 0.0            # Observer altitude  (m)
UTC        = TimeScalesFactory.getUTC()

TLE_STRING = (
    "1 41335U 16011A   25159.14374113  .00000095  00000-0  57243-4 0  9990\n"
    "2 41335  98.6255 226.8630 0001342 108.4863 251.6464 14.26740800484725\n"
)
print("Latest TLE received:\n", TLE_STRING)

# ---------------------------------------------------------------------
#  Build propagator & reference frames
# ---------------------------------------------------------------------
tle_lines   = TLE_STRING.splitlines()
tle         = TLE(tle_lines[0], tle_lines[1])
propagator  = TLEPropagator.selectExtrapolator(tle)

inertial    = FramesFactory.getEME2000()
itrf        = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
earth_model = OneAxisEllipsoid(
    Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
    Constants.WGS84_EARTH_FLATTENING,
    itrf,
)

obs_geodetic = GeodeticPoint(math.radians(OBS_LAT), math.radians(OBS_LON), OBS_ALT)
obs_frame    = TopocentricFrame(earth_model, obs_geodetic, "Ground")

sun          = CelestialBodyFactory.getSun()

# ---------------------------------------------------------------------
#  Helper: fixed-step recorder
# ---------------------------------------------------------------------
@JImplements(OrekitFixedStepHandler)
class FixedStepRecorder:
    """Collects spacecraft states at a uniform step."""
    def __init__(self) -> None:
        self.states = []

    @JOverride
    def init(self, s0, t, step):  # noqa: N802  (Java naming preserved)
        pass

    @JOverride
    def handleStep(self, currentState):  # noqa: N802
        self.states.append(currentState)

    @JOverride
    def finish(self, finalState):  # noqa: N802
        pass


# ---------------------------------------------------------------------
#  Build event detectors
# ---------------------------------------------------------------------
logger = EventsLogger()
detector_registry: dict[str, object] = []
detector_table: list[list[object]]  = []  # for pretty printing / analysis

def register(name: str, detector, frame=None) -> None:
    """Attach detector to propagator and add to registry & table."""
    logged = logger.monitorDetector(detector)
    propagator.addEventDetector(logged)
    detector_registry.append((name, detector))
    detector_table.append([name, frame is not None and "GroundPoint" or None, detector, frame])

# 1) Ascending node (ANX) — only increasing events
anx = EventSlopeFilter(NodeDetector(inertial), FilterType.TRIGGER_ONLY_INCREASING_EVENTS) \
        .withHandler(ContinueOnEvent())
register("ANX", anx)

# 2) Start of visible-at-night pass (satellite above 0°, Sun below horizon)
sat_visible   = ElevationDetector(obs_frame).withConstantElevation(0.0).withHandler(ContinueOnEvent())
night         = GroundAtNightDetector(obs_frame, sun, 0.0, None).withHandler(ContinueOnEvent())
night_visible = BooleanDetector.andCombine(sat_visible, night)
night_vis_in  = EventSlopeFilter(night_visible, FilterType.TRIGGER_ONLY_INCREASING_EVENTS) \
                    .withHandler(ContinueOnEvent())
register("NIGHT_VIS_START", night_vis_in, obs_frame)

# 3) End of visible-at-night pass (either satellite sets OR Sun rises)
sat_set       = EventSlopeFilter(sat_visible, FilterType.TRIGGER_ONLY_DECREASING_EVENTS)
sun_rise      = EventSlopeFilter(night, FilterType.TRIGGER_ONLY_INCREASING_EVENTS)
night_vis_out = BooleanDetector.orCombine(sat_set, sun_rise).withHandler(ContinueOnEvent())
register("NIGHT_VIS_END", night_vis_out, obs_frame)

# ---------------------------------------------------------------------
#  Prepare propagation window
# ---------------------------------------------------------------------
START_DT = dt.datetime(2025, 6, 8, 13, 15, 46, 708_267, tzinfo=dt.timezone.utc)
END_DT   = START_DT + dt.timedelta(days=3)           # 3-day look-ahead
start_ab = datetime_to_absolutedate(START_DT)
end_ab   = datetime_to_absolutedate(END_DT)

# ---------------------------------------------------------------------
#  Propagate with 1-second fixed steps
# ---------------------------------------------------------------------
recorder = FixedStepRecorder()
propagator.setStepHandler(1.0, recorder)
_ = propagator.propagate(start_ab, end_ab)

logged_events = list(logger.getLoggedEvents())

# ---------------------------------------------------------------------
#  Post-processing & display
# ---------------------------------------------------------------------
det_df = pd.DataFrame(
    detector_table,
    columns=["Event_Type", "Entity", "Detector", "Frame"]
)

for ev in logged_events:
    date  = ev.getDate()
    pv    = propagator.getPVCoordinates(date, inertial)
    pos   = pv.getPosition()
    kepler= KeplerianOrbit(ev.getState().getOrbit())
    ta    = math.degrees(kepler.getTrueAnomaly()) % 360  # wrap to 0-360°

    # Map detector -> human-readable label
    row   = det_df[det_df["Detector"] == ev.getEventDetector()].iloc[0]
    print(date, row["Event_Type"], row["Entity"], f"{ta:6.2f}°")

However, i get the following output in the pirnt:

2025-06-08T13:32:52.78768757871105Z ANX None 289.26°
2025-06-08T15:13:51.99386833109795Z ANX None 289.28°
2025-06-08T16:54:51.19996760825872Z ANX None 289.30°
2025-06-08T18:35:50.4059832535012Z ANX None 289.32°
2025-06-08T18:47:53.5558379165496Z NIGHT_VIS_END GroundPoint 280.50°
2025-06-08T18:47:53.5558379165496Z NIGHT_VIS_START GroundPoint 280.50°
2025-06-08T20:16:49.61191539308927Z ANX None 289.34°
2025-06-08T20:21:45.59644523905565Z NIGHT_VIS_START GroundPoint 292.79°
2025-06-08T21:57:48.81776400844264Z ANX None 289.35°
2025-06-08T22:03:11.05966905698308Z NIGHT_VIS_START GroundPoint 292.54°
2025-06-08T23:38:48.02352894783998Z ANX None 289.37°
2025-06-09T01:19:47.22921038568133Z ANX None 289.39°
2025-06-09T03:00:46.43480817383831Z ANX None 289.41°
2025-06-09T04:41:45.64032232484546Z ANX None 289.43°
2025-06-09T06:22:44.84575295834852Z ANX None 289.45°
2025-06-09T08:03:44.05109978500868Z ANX None 289.46°
2025-06-09T09:44:43.25636304694526Z ANX None 289.48°
2025-06-09T11:25:42.46154278362908Z ANX None 289.50°
2025-06-09T13:06:41.66663851188311Z ANX None 289.52°
2025-06-09T14:47:40.8716507980105Z ANX None 289.54°
2025-06-09T16:28:40.0765792830513Z ANX None 289.56°
2025-06-09T18:09:39.2814240515477Z ANX None 289.58°
2025-06-09T19:50:38.48618529288515Z ANX None 289.59°
2025-06-09T19:56:13.45736762569471Z NIGHT_VIS_START GroundPoint 292.61°
2025-06-09T20:10:54.59477768710176Z NIGHT_VIS_END GroundPoint 240.66°
2025-06-09T21:31:37.69086248605301Z ANX None 289.61°
2025-06-09T21:36:21.88977036085199Z NIGHT_VIS_START GroundPoint 293.14°
2025-06-09T21:50:30.31189236655414Z NIGHT_VIS_END GroundPoint 250.67°
2025-06-09T23:12:36.89545613060172Z ANX None 289.63°
2025-06-10T00:53:36.09996627647117Z ANX None 289.65°
2025-06-10T02:34:35.30439202885943Z ANX None 289.67°
2025-06-10T04:15:34.50873451012935Z ANX None 289.69°
2025-06-10T05:56:33.71299353229394Z ANX None 289.71°
2025-06-10T07:37:32.91716768156302Z ANX None 289.73°
2025-06-10T08:22:29.51176758069654Z NIGHT_VIS_END GroundPoint  65.06°
2025-06-10T09:18:32.12125900011178Z ANX None 289.75°
2025-06-10T10:04:09.39006070067584Z NIGHT_VIS_END GroundPoint  64.66°
2025-06-10T10:59:31.32526579779235Z ANX None 289.76°
2025-06-10T11:41:08.16775595275821Z NIGHT_VIS_END GroundPoint  69.42°
2025-06-10T12:40:30.52918926836971Z ANX None 289.78°
2025-06-10T14:21:29.73302946589094Z ANX None 289.80°
2025-06-10T16:02:28.93678436897792Z ANX None 289.82°
2025-06-10T17:43:28.14045669875716Z ANX None 289.84°
2025-06-10T18:02:21.83255163538818Z NIGHT_VIS_END GroundPoint 250.66°
2025-06-10T19:24:27.34404576381169Z ANX None 289.86°
2025-06-10T19:30:58.95943220223269Z NIGHT_VIS_START GroundPoint 291.98°
2025-06-10T21:05:26.54754858646574Z ANX None 289.88°
2025-06-10T21:09:56.83759386899017Z NIGHT_VIS_START GroundPoint 293.48°
2025-06-10T22:46:25.75096998956419Z ANX None 289.90°
2025-06-10T22:54:49.38728346465599Z NIGHT_VIS_START GroundPoint 289.42°
2025-06-11T00:27:24.9543053579521Z ANX None 289.92°
2025-06-11T02:08:24.15755859596631Z ANX None 289.94°
2025-06-11T03:49:23.36072870076728Z ANX None 289.96°
2025-06-11T05:30:22.5638116688141Z ANX None 289.98°
2025-06-11T07:11:21.76681458805731Z ANX None 290.00°
2025-06-11T08:52:20.96973181448109Z ANX None 290.02°
2025-06-11T10:33:20.17256468721407Z ANX None 290.03°
2025-06-11T12:14:19.37531430566255Z ANX None 290.05°

as you can see the detector triger sometime only start the event and sometime only when the event stop but this sistuation is paradoxical. if exist the event i should have both the start and the stop of the event. i’m not sure why the propagator or the detector don’t work in this way. Maytbe the detector is wrong setup or the accuracy of the propagator?
someone coudl help me on this problem?

Thank you very much in advance for any help :slightly_smiling_face:

I guess you should remove completely the end of visible-at-night pass logic, and you should also remove the use of EventSlopeFilter in the visible at night logic.

The reason is that when you call BooleanDetector.andCombine, you already have everything set up. The visibility will start when the g functions of both ElevationDetector and GroundAtNightDetector are positive. So if for example GroundAtNightDetector becomes positive first while ElevationDetector is still negative, then it will wait until ElevationDetector becomes positive too. The increasing event will be the start of the visibility. Then when either function becomes negative (i.e. when either the satellite sets or the Sun rises), one of the two g functions will become negative and the and combined detector will trigger a decreasing event that will notify you visibility conditions are not met anymore.

One thing that may explain your problems is that you register several times the same low level detectors (sat_visible and night) to the same propagator, wrapped in different ways with combinations of boolean detectors and slope filters. If you really want to register separately the same detector several times (this could happen if for example you want to register different event handlers), then you should recreate the detector several times so you have separate instances.