Event after a certain time in eclipse

Hi all,

I’ve been trying to count the amount of time that the satellite has spent in each eclipse in order to generate an event when the cumulative time in the current eclipse reaches a certain threshold. I’ve used this post as a starting guide and ultimately have been able to come up with the below minimum working example, which seems to be counting the eclipse durations accurately. However, I don’t know how to use it in order to trigger events after the ElapsedTimeCounter.elapsedSec reaches the ElapsedTimeCounter.limit. I suppose this is related to the ElapsedTimeCounter.apply method introduced in the above post but I simply don’t know how to leverage this. Any suggestions?

Also, a bunch of additional questions that arose while I was working on this MWE and I haven’t been able to find answers to (marked with #TODO in the MWE):

  1. The order of adding event detectors and handlers seems to sometimes affect the results that one gets. Is there any “golden rule” on how to do this safely to guarantee robust results? Does the order as in the #%% Configure the propagator events cell below make sense?
  2. Why if I move eclipseDetectorWithPassCounter up or down in the #%% Configure the propagator events cell the number of events found by loggedDetector changes?
  3. Why are the PassCounter.init and ElapsedTimeCounter.init methods never called? When exactly would they be called?
import numpy

MAX_ECLIPSES = 5 # Max no. eclipses after which propagation will be stopped.

#%% Start Orekit and configure frames & bodies
from orekit.pyhelpers import download_orekit_data_curdir, setup_orekit_curdir
if not os.path.isfile('orekit-data.zip'):
    download_orekit_data_curdir()
setup_orekit_curdir()

from org.orekit.orbits import KeplerianOrbit, PositionAngle
from org.orekit.time import AbsoluteDate, TimeScalesFactory
from org.orekit.utils import Constants, IERSConventions
from org.orekit.frames import FramesFactory
from org.orekit.bodies import OneAxisEllipsoid, CelestialBodyFactory

from orekit import JArray_double
from org.orekit.orbits import OrbitType
from org.orekit.propagation import SpacecraftState
from org.orekit.propagation.numerical import NumericalPropagator
from org.hipparchus.ode.nonstiff import DormandPrince853Integrator

from org.orekit.python import PythonEventHandler
from org.hipparchus.ode.events import Action
from org.orekit.propagation.events import EclipseDetector, EventsLogger
from org.orekit.propagation.events.handlers import ContinueOnEvent

utc = TimeScalesFactory.getUTC()

j2000 = FramesFactory.getEME2000()

itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, True)

earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, 
                         Constants.WGS84_EARTH_FLATTENING,  itrf)

sun = CelestialBodyFactory.getSun()

#%% Custom event handlers
class PassCounter(PythonEventHandler):
    """ Counts events. Will stop propagation after first n events."""

    def __init__(self, limit):
        self.limit = limit
        self.passesInc = 0
        self.passesDec = 0
        super(PassCounter, self).__init__()

    def init(self, initialState, target):
        # Not used, just to try and port functionality from ElapsedTimeCounter.
        self.startEpoch = initialState.getDate() #TODO 'PassCounter' object has no attribute 'startEpoch'?
        self.elapsedSec = 0.0 #TODO: 'PassCounter' object has no attribute 'elapsedSec'?
        super(PassCounter, self).init() # Doesn't affect the odd attribute errors.
        #TODO for whatever reason, init is never called. What does this method do exactly?
    
    def eventOccurred(self, s, T, increasing):
        if increasing:
            self.passesInc = self.passesInc + 1
            print(f'Increasing pass at {s.getDate()}, passes = {self.passesInc}')
        else:
            self.passesDec = self.passesDec + 1
            print(f'Decreasing pass at {s.getDate()}, passes = {self.passesDec}')
        
        # Just as an example, stop propagation after some no. events.
        if self.passesInc >= self.limit and self.passesDec >= self.limit:
            return Action.STOP
        else:
            return Action.CONTINUE

    def resetState(self, detector, oldState):
        return oldState

class ElapsedTimeCounter(PythonEventHandler):
    """ Counts the elapsed time since a decreasing event, and triggers and event
    when the elapsed  time reaches a predefined limit. Inspired by this:
    https://forum.orekit.org/t/eventdetection-based-on-time-in/293
    """
    
    def __init__(self, limit, initialState):
        self.limit = limit
        self.startEpoch = initialState.getDate() # Start counting from this epoch.
        self.elapsedSec = 0.0
        super(ElapsedTimeCounter, self).__init__()

    def init(self, initialState, target):
        pass
    
    def eventOccurred(self, s, T, increasing):
        if not increasing: # Eclipse start.
            self.startEpoch = s.getDate()
            self.elapsedSec = 0.0
        else: # increasing => eclipse stop.
            self.elapsedSec += s.getDate().durationFrom(self.startEpoch)
            print(f'Elapsed {self.elapsedSec} sec since {self.startEpoch}')
            self.startEpoch = None
            
        return Action.CONTINUE
    
    def apply(self, s):
        """ Defines g function of a new Functional detector. """
        print('apply') #TODO This is never called, how to trigger events from here?
        if self.startEpoch is None:
            return self.elapsedSec - self.limit
        else:   
            return (self.elapsedSec +
                    s.getDate().durationFrom(self.startEpoch)) - self.limit

    def resetState(self, detector, oldState):
        return oldState

#%% Initial state of the satellite
ra = 500 * 1000.        #  Apogee
rp = 400 * 1000.        #  Perigee
i = float(numpy.deg2rad(87.0)) # inclination
omega = float(numpy.deg2rad(20.0))   # argument of perigee
raan = float(numpy.deg2rad(10.0))  # right ascension of ascending node
ta = float(numpy.deg2rad(0.0))    # True anomaly

epoch = AbsoluteDate(2020, 1, 1, 0, 0, 00.000, utc)

a = (rp + ra + 2 * Constants.WGS84_EARTH_EQUATORIAL_RADIUS) / 2.0    
e = 1.0 - (rp + Constants.WGS84_EARTH_EQUATORIAL_RADIUS) / a

initialMass = 1.0 # kg

## Orbit construction as Keplerian
initialOrbit = KeplerianOrbit(a, e, i, omega, raan, ta, PositionAngle.TRUE,
                              j2000, epoch, Constants.WGS84_EARTH_MU)

#%% Configure the propagator
minIntegStep = 0.0001
maxIntegStep = 1000.0
initIntegStep = 1.0
posTolerance = 0.1

orbitType = OrbitType.CARTESIAN

tol = NumericalPropagator.tolerances(posTolerance, initialOrbit, orbitType)

# Setup the integrator.
integrator = DormandPrince853Integrator(minIntegStep,
                                             maxIntegStep,
                                             JArray_double.cast_(tol[0]),
                                             JArray_double.cast_(tol[1]))
integrator.setInitialStepSize(initIntegStep)

# Setup the propagator.
propagator = NumericalPropagator(integrator)
propagator.setOrbitType(orbitType)

# Set the initial state.
initialState = SpacecraftState(initialOrbit, initialMass)
propagator.setInitialState(initialState)

#%% Configure the propagator events
passCounter = PassCounter(limit=MAX_ECLIPSES)
timeElapsedCounter = ElapsedTimeCounter(100.0, initialState)
logger = EventsLogger()

eclipseDetectorWithElapsedCounter = EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra().withHandler(timeElapsedCounter)
propagator.addEventDetector(eclipseDetectorWithElapsedCounter)

#TODO eclipseDetectorWithPassCounter has to be here for eclipseDetectorWithLogger to work properly. Otherwise, we get 9 logged events. Why?
eclipseDetectorWithPassCounter = EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra().withHandler(passCounter)
propagator.addEventDetector(eclipseDetectorWithPassCounter)

eclipseDetectorWithLogger = EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra().withHandler(ContinueOnEvent())
loggedDetector = logger.monitorDetector(eclipseDetectorWithLogger)
propagator.addEventDetector(loggedDetector)

#%% Propagate the orbit
epochList = [epoch, epoch.shiftedBy(36000.0)]
orekitStates = []

for currentEpoch in epochList:
    currentState = propagator.propagate(currentEpoch)
    orekitStates.append(currentState)

propEvents = logger.getLoggedEvents()
print(f'Found {propEvents.size()/2} eclipses out of maximum no. of {MAX_ECLIPSES}.')

Hi AlekLee,

I don’t see anything glaringly wrong with your propagator initialization. My guess is that you’re getting differing results because an occuring event changes the definition of another event detector and you return Action.Continue. Continue tells the propagator that no g functions or state changed as a result of the event. You should be using one of the reset Actions, is this case probably RESET_EVENTS which tells the propagator to re-check the g functions of the other event detectors. See Action (Hipparchus 2.0 API)

IMHO I would suggest something like the following, written in Java:

class DurationDetector extends AbstractDetector<> {
  private AbsoluteDate start;
  private double duration;
  private double target;

  @Override
  public void init(...) {
    start = null;
    duration = 0.0;
  }

  @Override
  public double g(SpacecraftState s) {
    return duration - target + 
        (start == null ? 0.0 : s.getDate().durationFrom(start));
  }

  public void start(AbsoluteDate d) {
    start = d;
  }

  public void end(AbsoluteDate d) {
    duration += d.durationFrom(start);
    start = null;
  }

}

Then you can connect the controlling detector and add to the propagator as follows:

DurationDetector durationDetector = new DurationDetector(...);
EclipseDetector eclipseDetector = ...;
propagator.addEventDetector(durationDetector);
propagator.addEventDetector(eclipseDetector.withHandler((s, increasing) -> {
  if (increasing) { // decide start vs. stop based on increasing. Varies with use case.
    durationDetector.stop(s.getDate());
  } else {
    durationDetector.start(s.getDate());
  }
  return Action.RESET_EVENTS;
}));

Hi @evan.ward thanks a lot for your kind help. I’ve changed the return value of my custom PassCounter to Action.RESET_EVENTS from Action.CONTINUE - is this what you’ve had in mind? Even after the change, I still get different results depending on the order in which I create handlers. The only difference is that now eclipseDetectorWithPassCounter has to be after eclipseDetectorWithLogger. I’m not sure if this is an indication of a larger issue?

Also, I see where you’re going with this, thanks for the idea. It didn’t cross my mind to communicate between detectors and handlers like this:

  if (increasing) {
    durationDetector.stop(s.getDate()); // Let the durationDetector know when eclipse starts/ends so that the duration event can be found by the propagator
  } else {
    durationDetector.start(s.getDate());
  }
  return Action.RESET_EVENTS;
}));

I’ve implemented this but with a custom handler (I don’t think Python supports lambda detectors, see this post). Unfortunately, it falls over when I try to attach this ElapsedTimeHandler to the EclipseDetector with the following error:

InvalidArgsError: (<class 'org.orekit.propagation.events.EclipseDetector'>, 'withHandler', <ElapsedTimeHandler: java.lang.Object@25f7391e>)

I’m not sure why this is the case because the custom handler is nearly identical to my PassCounter, which works. Any ideas what might be wrong? I’ve put my changed code here below.

class PassCounter(PythonEventHandler):
    """ Counts events. Will stop propagation after first n events."""

    def __init__(self, limit):
        self.limit = limit
        self.passesInc = 0
        self.passesDec = 0
        super(PassCounter, self).__init__()

    def init(self, initialState, target):
        # Not used, just to try and port functionality from ElapsedTimeCounter.
        self.startEpoch = initialState.getDate() #TODO 'PassCounter' object has no attribute 'startEpoch'?
        self.elapsedSec = 0.0 #TODO: 'PassCounter' object has no attribute 'elapsedSec'?
        super(PassCounter, self).init() # Doesn't affect the odd attribute errors.
        #TODO for whatever reason, init is never called. This is why I had cryptic Java errors in ElapsedTimeCounter - because attributes were undefined!
    
    def eventOccurred(self, s, T, increasing):
        if increasing:
            self.passesInc = self.passesInc + 1
            print(f'Increasing pass at {s.getDate()}, passes = {self.passesInc}')
        else:
            self.passesDec = self.passesDec + 1
            print(f'Decreasing pass at {s.getDate()}, passes = {self.passesDec}')
        
        # Just as an example, stop propagation after some no. events.
        if self.passesInc >= self.limit and self.passesDec >= self.limit:
            return Action.STOP
        else:
            return Action.RESET_EVENTS # Check other event detectors, not simply continue.

    def resetState(self, detector, oldState):
        return oldState

class ElapsedTimeDetector(PythonAbstractDetector):
    """ Counts the elapsed time since a decreasing event, and triggers and event
    when the elapsed  time reaches a predefined limit. Inspired by this:
    https://forum.orekit.org/t/eventdetection-based-on-time-in/293
    """
    
    def __init__(self, target, handler=None):
        self.target = target
        self.startEpoch = None
        self.elapsedSec = 0.0
        
        dmax = float(PythonAbstractDetector.DEFAULT_MAXCHECK)
        dthresh = float(PythonAbstractDetector.DEFAULT_THRESHOLD)
        dmaxiter = PythonAbstractDetector.DEFAULT_MAX_ITER
        
        if handler is None:
            handler = ContinueOnEvent().of_(ElapsedTimeDetector)
        
        super(ElapsedTimeDetector, self).__init__(dmax, dthresh, dmaxiter, handler) #super(maxCheck, threshold, maxIter, handler);

    def init(self, initialState, target):
        pass
    
    def g(self, s):
        if self.startEpoch is None:
            add = 0.0
        else:
            add = s.getDate().durationFrom(self.startEpoch)
            
        return self.elapsedSec - self.target + add
    
    def start(self, d):
        self.startEpoch = d
    
    def stop(self, d):
        self.elapsedSec += d.durationFrom(self.startEpoch)
        self.startEpoch = None

class ElapsedTimeHandler(PythonEventHandler):
    """ Handler for EclipseDetector that also marks start/stop times in ElapsedTimeDetector. """

    def __init__(self):
        super(PythonEventHandler, self).__init__()
    
    def init(self, initialState, target):
        pass
    
    def setDetector(self, elapsedDetector):
        self.elapsedDetector = elapsedDetector
        
    def eventOccurred(self, s, T, increasing):
        if increasing:
            self.elapsedDetector.start(s.getDate())
            print(f'Increasing pass at {self.elapsedDetector.startEpoch}')
        else:
            self.elapsedDetector.stop(s.getDate())
            print(f'Increasing pass at {self.elapsedDetector.startEpoch}, duration = {self.elapsedHandler.elapsedSec}')
        
        return Action.RESET_EVENTS # Check other event detectors, not simply continue.

    def resetState(self, detector, oldState):
        return oldState

(…)

#%% Configure the propagator events
passCounter = PassCounter(limit=MAX_ECLIPSES)
elapsedTimeDetector = ElapsedTimeDetector(100.0)
elapsedTimeHandler = ElapsedTimeHandler()
elapsedTimeHandler.setDetector(elapsedTimeDetector)
logger = EventsLogger()

propagator.addEventDetector(elapsedTimeDetector)
#TODO eclipseDetectorWithElapsedHandler fails to instantiate due to InvalidArgsError in withHandler???
eclipseDetectorWithElapsedHandler = EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra().withHandler(elapsedTimeHandler)

#TODO eclipseDetectorWithPassCounter has to be here for eclipseDetectorWithLogger to work properly. Otherwise, we get 9 logged events.

eclipseDetectorWithLogger = EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra().withHandler(ContinueOnEvent())
loggedDetector = logger.monitorDetector(eclipseDetectorWithLogger)
propagator.addEventDetector(loggedDetector)

#TODO After changing from Action.CONTINUE to RESET_EVENTS had to move the eclipseDetectorWithPassCounter to the end. Why?
eclipseDetectorWithPassCounter = EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra().withHandler(passCounter)
propagator.addEventDetector(eclipseDetectorWithPassCounter)

I don’t use the Python interface. Perhaps @petrus.hyvonen could help make sense of that?

Could you provide a print out of the events detected and annotate where it is incorrect?

Keep in mind that if two events occur within maxCheck then those two events may or may not be ignored. Also keep in mind that events can occur at the time of the true zero +/- tolerance. Your code seems to define three event detectors to detect the same events. There is no guarantee of what order simultaneous events will be delivered to the application as they can all occur at the same time +/- tolerance, allowing an arbitrary order. It’s also inefficient to find the same event three times. A more efficient and repeatable choice would be to only find each event once. Perhaps introducing a multiplexing event handler that calls other handlers, if that is what your application needs.

See org.hipparchus.ode.events (Hipparchus 3.0 API)

Hi,

thanks a lot for your continuous support. By [quote=“evan.ward, post:4, topic:1434”]
multiplexing event handler [/quote] I assume you mean this. Indeed, that looks like a neat way to handled this. Thanks and sorry - I wasn’t aware of how it all works. It even mentions “non-deterministic behaviour” in the docs :wink: I’ll give it a try. As for the outputs, here are the contents of propEvents that I’ve been getting depending on the relative location of eclipseDetectorWithLogger and eclipseDetectorWithPassCounter:

propEvents
Out[4]: <List: [org.orekit.propagation.events.EventsLogger$LoggedEvent@6aecbb8d, org.orekit.propagation.events.EventsLogger$LoggedEvent@1af146, org.orekit.propagation.events.EventsLogger$LoggedEvent@4da602fc, org.orekit.propagation.events.EventsLogger$LoggedEvent@2a8d39c4, org.orekit.propagation.events.EventsLogger$LoggedEvent@25b2cfcb, org.orekit.propagation.events.EventsLogger$LoggedEvent@72758afa, org.orekit.propagation.events.EventsLogger$LoggedEvent@fb9c7aa, org.orekit.propagation.events.EventsLogger$LoggedEvent@4c398c80, org.orekit.propagation.events.EventsLogger$LoggedEvent@7fc6de5b, org.orekit.propagation.events.EventsLogger$LoggedEvent@21baa903]>
propEvents.size()
Out[5]: 10

and

propEvents
Out[8]: <List: [org.orekit.propagation.events.EventsLogger$LoggedEvent@30d4b288, org.orekit.propagation.events.EventsLogger$LoggedEvent@4cc6fa2a, org.orekit.propagation.events.EventsLogger$LoggedEvent@40f1be1b, org.orekit.propagation.events.EventsLogger$LoggedEvent@7a791b66, org.orekit.propagation.events.EventsLogger$LoggedEvent@6f2cb653, org.orekit.propagation.events.EventsLogger$LoggedEvent@14c01636, org.orekit.propagation.events.EventsLogger$LoggedEvent@590c73d3, org.orekit.propagation.events.EventsLogger$LoggedEvent@6b9ce1bf, org.orekit.propagation.events.EventsLogger$LoggedEvent@61884cb1]>
propEvents.size()
Out[9]: 9 # One too few because it all ends after 5 complete eclipses, i.e. 5 starts & 5 stops = 10 events.

Thanks for posting the output. I would call that expected behavior as there are three simultaneous events and one of they returns STOP. So variation in the number of events detected is expected.