Station keeping using low thrust

Hi, I have been trying to implement a station keeping script using low thrust maneuvers. Two models could be used , the ConfigurableLowThrust , and constantThrust . The issue is that, with configurableLowThrustManeuver I can’t make PythonStartStopEventTrigger work using altitude detector in order to start the maneuver at a given altitude until i reach the second altitude then stop , then keep propagating till i reach again the lower threshold altitude and again.
The error is the following one :

    propagator.propagate(final_date)
orekit.JavaError: <super: <class 'JavaError'>, <JavaError object>>
    Java stacktrace:
java.lang.RuntimeException: AttributeError
        at org.orekit.forces.maneuvers.trigger.PythonStartStopEventsTrigger.getParametersDrivers(Native Method)
        at org.orekit.forces.maneuvers.Maneuver.getParametersDrivers(Maneuver.java:275)
        at org.orekit.utils.ParameterDriversProvider.getParameters(ParameterDriversProvider.java:82)
        at org.orekit.forces.maneuvers.Maneuver.addContribution(Maneuver.java:159)
        at org.orekit.propagation.numerical.NumericalPropagator$Main.computeDerivatives(NumericalPropagator.java:979)
        at org.orekit.propagation.integration.AbstractIntegratedPropagator$ConvertedMainStateEquations.computeDerivatives(AbstractIntegratedPropagator.java:794)
        at org.hipparchus.ode.ExpandableODE.computeDerivatives(ExpandableODE.java:134)
        at org.hipparchus.ode.AbstractIntegrator.computeDerivatives(AbstractIntegrator.java:259)
        at org.hipparchus.ode.AbstractIntegrator.initIntegration(AbstractIntegrator.java:204)
        at org.hipparchus.ode.nonstiff.EmbeddedRungeKuttaIntegrator.integrate(EmbeddedRungeKuttaIntegrator.java:195)
        at org.orekit.propagation.integration.AbstractIntegratedPropagator.integrateDynamics(AbstractIntegratedPropagator.java:493)
        at org.orekit.propagation.integration.AbstractIntegratedPropagator.propagate(AbstractIntegratedPropagator.java:440)
        at org.orekit.propagation.integration.AbstractIntegratedPropagator.propagate(AbstractIntegratedPropagator.java:400)

This is the current code :

simDur = 365 * 3600 * 24*1.0



initial_date = AbsoluteDate(2020, 8, 23, 12, 0, 0.0, TimeScalesFactory.getUTC())

############################ Orbit Definition ######################################
####################################################################################

ra = 500.0 * 1000  # Apogee in meters
rp = 500.0 * 1000  
i = 87.0  # Inclination (degres)
omega = 20.0  # Perigee argument in radians
raan = 10.0    # Right ascension of ascending node in radians
lv = 0.0     # True anomaly in radians
a = (rp + ra + 2 * Constants.WGS84_EARTH_EQUATORIAL_RADIUS) / 2.0    
e = 1.0 - (rp + Constants.WGS84_EARTH_EQUATORIAL_RADIUS) / a

mu = Constants.EGM96_EARTH_MU 
# Setting the initial orbit : 
initialOrbit = KeplerianOrbit(a, e, math.radians(i), math.radians(omega), math.radians(raan), math.radians(lv),
                              PositionAngleType.MEAN, FramesFactory.getEME2000(), initial_date, mu)



############################ Satellite Definition ######################################
########################################################################################


satellite_mass = 6.0
area = 0.15


############################ Environment Definition ######################################
########################################################################################


gravityProvider = GravityFieldFactory.getNormalizedProvider(60, 60)
earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS,
                         Constants.WGS84_EARTH_FLATTENING,
                         FramesFactory.getITRF(IERSConventions.IERS_2010, True))

inertialFrame = FramesFactory.getEME2000()
sun = CelestialBodyFactory.getSun()
itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf)
dragCoef = 2.2
drag = IsotropicDrag(area, dragCoef)
strengthLevel = MarshallSolarActivityFutureEstimation.StrengthLevel.AVERAGE
supportedNames = MarshallSolarActivityFutureEstimation.DEFAULT_SUPPORTED_NAMES
inputParameters_Drag = MarshallSolarActivityFutureEstimation(supportedNames, strengthLevel)
atmosphere = NRLMSISE00(inputParameters_Drag, sun, earth)
orbitType = OrbitType.CARTESIAN


# Attitude
attitude_provider = LofOffset(inertialFrame, LOFType.NTW)
attitude = attitude_provider.getAttitude(initialOrbit, initialOrbit.getDate(), initialOrbit.getFrame())

# Initial state with attitude
initialState = SpacecraftState(initialOrbit, attitude, satellite_mass)


############################ Reentry Definition ######################################
########################################################################################################

Altitude_reentry = 200.0 * 1000.0
altitude_low=650*1000.0
altitude_high=700*1000.0
altitudeDetector =  AltitudeDetector(Altitude_reentry, earth).withHandler(StopOnEvent())


############################ Integrator and propagator Definition ######################################
########################################################################################################


# integrator
minStep = 0.00001
maxStep = 1000.0
initStep = 60.0
positionTolerance = 1.0
tolerances = NumericalPropagator.tolerances(positionTolerance, initialOrbit,orbitType)
integrator = DormandPrince853Integrator(minStep, maxStep,
                                        JArray_double.cast_(tolerances[0]), JArray_double.cast_(tolerances[1]))

integrator.setInitialStepSize(initStep)

# Propagator
propagator = NumericalPropagator(integrator)
propagator.addEventDetector(altitudeDetector)
# AddforceModel
propagator.addForceModel(DragForce(atmosphere, drag))
#propagator.addForceModel(HolmesFeatherstoneAttractionModel(earth.getBodyFrame(), gravityProvider))


############################ Handler Definition ########################################################
########################################################################################################
sma_limite=68800000
# Paramètres de la manœuvre à poussée constante
thrust = 0.01  # Poussée en Newtons
isp = 300.0  # Impulsion spécifique (s)
duration = 6000.0  # Durée de la manœuvre en secondes
direction = Vector3D(1.0, 0.0, 0.0)  # Direction de la poussée (dans le référentiel LVLH)
prop_model=BasicConstantThrustPropulsionModel(thrust,isp,direction,"low thrust")

class HandlerForDate(PythonOrekitFixedStepHandler):


    def init(self, so, t, step):
        super().__init__()
        self.smas = []
        self.dates = []
        self.incs = []
        self.eccs = []


    def handleStep(self, state):
        orbit = OrbitType.KEPLERIAN.convertType(state.getOrbit())
        sma = orbit.getA()
        inc = orbit.getI()
        date = state.getDate()
        ecc = state.getE()
        #print(sma)
        print(date)
        self.smas.append(sma / 1000 - Constants.WGS84_EARTH_EQUATORIAL_RADIUS / 1000)  # Convert to km
        self.dates.append(date.durationFrom(initial_date) / (3600 * 24 * 365))  # Convert to years
        self.incs.append(math.degrees(inc))
        self.eccs.append(ecc)
        maneuver = ConstantThrustManeuver(date, duration,attitude_provider, prop_model)
        print(sma,sma_limite)
      
           
    def finish(self, finalState):
        orbit = KeplerianOrbit(finalState.getOrbit())


time_step = 1000.0
#Handler_Date = HandlerForDate()
#Propagator.cast_(propagator).setStepHandler(time_step,Handler_Date)

direction = Vector3D(float(1), float(0), float(0))
thrust = 0.06
thruster_axis = Vector3D(float(1), float(0), float(0))

thrust_direction_provider = ConstantThrustDirectionProvider(direction)
thrust_direction_and_attitude_provider = ThrustDirectionAndAttitudeProvider.buildFromDirectionInLOF(
    LOFType.QSW,
    thrust_direction_provider,
    thruster_axis)
trigger_start = altitudeDetector =  AltitudeDetector(altitude_low, earth).withHandler(StopOnEvent())
#trigger_start.init(propagator.getInitialState(), date_end)
trigger_end = altitudeDetector =  AltitudeDetector(altitude_high, earth).withHandler(StopOnEvent())
#trigger_end.init(propagator.getInitialState(), date_end)

trigg=PythonStartStopEventsTrigger(trigger_start,trigger_end)
configurable_low_thrust_maneuver = ConfigurableLowThrustManeuver(thrust_direction_and_attitude_provider,
                                                                 trigg,
                                                                 thrust,
                                                                 isp)
propagator.addForceModel(configurable_low_thrust_maneuver)


############################ Propagate #################################################################
########################################################################################################


propagator.setOrbitType(orbitType)
propagator.setInitialState(initialState)
#final_state = propagator.propagate(initial_date, initial_date.shiftedBy(simDur))
final_date=initial_date.shiftedBy(simDur)
propagator.propagate(final_date)

Thanks in advance for you answers

I have the same issue except that i’m using impulsive maneuvers, don’t know how to handle it …

I believe this issue comes from the subclass process of StartStopEventTrigger.

In the java doc, convertStartDetector and convertStopDetector are sometimes used, meaning your trigger aren’t considered by configurableLowThrustManeuver.

Would someone agree ?

Cheers,

Hello @HugoBocc and welcome to the orekit forum @Emilien_mrlt,

This message to let you know that i’m starting to make an example to see what’s wrong. I’ll keep you updated.

Cheers,
Vincent

1 Like

Ok so after stumbling on the same error, i realized that this is because the StartStopEventsTrigger is an abstract class and so, must be extended to be used.

Coming back when my example is ready :sweat_smile:

1 Like

Hi vincent, really appreciate you help. Thanks for your time

1 Like

Ok it has been a long time since i last used the Python wrapper but here is the example below.

I used a ridiculously high drag coefficient so that we can see the maneuvers in action in a single day :slight_smile: .

Description

This example shows the use of a custom ConfigurableLowThrustManeuver used with a custom semi major apsis event detector (called SMADetector within the script).

The maneuver is activated if the semi major apsis is below 490 km and stopped when above 510 km.

Beware : You need to change ‘PATH_TO_OREKIT_DATA’ to your orekit data directory. Some may use “orekit-data.zip” as well.

Code

import math

import matplotlib.pyplot as plt
import numpy as np
import orekit
from java.util import Collections
from orekit import JArray_double
from org.hipparchus.geometry.euclidean.threed import Vector3D
from org.hipparchus.ode.nonstiff import DormandPrince853Integrator
from org.orekit.attitudes import LofOffset
from org.orekit.bodies import CelestialBodyFactory
from org.orekit.forces.drag import DragForce
from org.orekit.forces.drag import IsotropicDrag
from org.orekit.forces.maneuvers import ConfigurableLowThrustManeuver
from org.orekit.forces.maneuvers.propulsion import ThrustDirectionAndAttitudeProvider
from org.orekit.forces.maneuvers.trigger import PythonStartStopEventsTrigger
from org.orekit.frames import FramesFactory
from org.orekit.frames import LOFType
from org.orekit.models.earth import ReferenceEllipsoid
from org.orekit.models.earth.atmosphere import NRLMSISE00
from org.orekit.models.earth.atmosphere.data import CssiSpaceWeatherData
from org.orekit.orbits import KeplerianOrbit
from org.orekit.orbits import OrbitType
from org.orekit.orbits import PositionAngleType
from org.orekit.propagation import Propagator
from org.orekit.propagation import SpacecraftState
from org.orekit.propagation.events import
from org.orekit.propagation.events import AbstractDetector
from org.orekit.propagation.events import BooleanDetector
from org.orekit.propagation.events import PythonAbstractDetector
from org.orekit.propagation.events.handlers import ContinueOnEvent
from org.orekit.propagation.numerical import NumericalPropagator
from org.orekit.propagation.sampling import PythonOrekitFixedStepHandler
from org.orekit.time import AbsoluteDate
from org.orekit.time import TimeScalesFactory
from org.orekit.utils import Constants
from org.orekit.utils import IERSConventions

vm = orekit.initVM()

from orekit.pyhelpers import setup_orekit_curdir

setup_orekit_curdir('PATH_TO_OREKIT_DATA')

### CONSTANTS
UTC = TimeScalesFactory.getUTC()
INERTIAL = FramesFactory.getEME2000()
ITRF = FramesFactory.getITRF(IERSConventions.IERS_2010, False)
EARTH_BODYSHAPE = ReferenceEllipsoid.getIers2010(ITRF)
EARTH_RADIUS = Constants.WGS84_EARTH_EQUATORIAL_RADIUS
SUN = CelestialBodyFactory.getSun()
INITIAL_DATE = AbsoluteDate(2024, 8, 30, 15, 0, 00.000, UTC)


### CLASSES
class SMAHandler(PythonOrekitFixedStepHandler):
    def __init__(self):
        super().__init__()
        self.values = []
        self.dates = []

    def init(self, spacecraftState, absoluteDate, double):
        pass

    def handleStep(self, spacecraftState):
        self.values.append(spacecraftState.getA())
        self.dates.append(spacecraftState.getDate().durationFrom(INITIAL_DATE))

    def finish(self, spacecraftState):
        pass


class ActualStartStopTriggerEvents(PythonStartStopEventsTrigger):
    def __init__(self, start_detector, stop_detector):
        super().__init__(start_detector, stop_detector)

    def getParametersDrivers(self):
        # Needs to return an empty java list so that the code does not fail !
        return Collections.emptyList()


class SMADetector(PythonAbstractDetector):

    def __init__(self, sma_threshold,
                 max_check=AbstractDetector.DEFAULT_MAXCHECK,
                 threshold=AbstractDetector.DEFAULT_THRESHOLD,
                 max_iter=AbstractDetector.DEFAULT_MAX_ITER,
                 event_handler=ContinueOnEvent()):
        super().__init__(max_check, threshold, max_iter, event_handler)
        self.sma_threshold = sma_threshold

    def g(self, spacecraftState):
        return spacecraftState.getA() - self.sma_threshold

    def create(self, max_check, threshold, max_iter, event_handler):
        return SMADetector(self.sma_threshold, max_check.currentInterval(None), threshold, max_iter, event_handler)


### METHODS
def getNumericalPropagator(initialOrbit):
    minStep = 0.001
    maxstep = 1000.0
    initStep = 60.0
    positionTolerance = 1.0

    tolerances = NumericalPropagator.tolerances(positionTolerance,
                                                initialOrbit,
                                                initialOrbit.getType())

    integrator = DormandPrince853Integrator(minStep, maxstep,

                                            JArray_double.cast_(tolerances[0]),
                                            # Double array of doubles needs to be casted in Python
                                            JArray_double.cast_(tolerances[1]))
    integrator.setInitialStepSize(initStep)

    return NumericalPropagator(integrator)


def getOrbit():
    # Define orbit
    ra = 500 * 1000  # Apogee
    rp = 498 * 1000  # Perigee
    i = math.radians(87.0)  # inclination
    omega = math.radians(20.0)  # perigee argument
    raan = math.radians(10.0)  # right ascension of ascending node
    lv = math.radians(0.0)  # True anomaly

    a = (rp + ra + 2 * EARTH_RADIUS) / 2.0
    e = 1.0 - (rp + EARTH_RADIUS) / a

    return KeplerianOrbit(a, e, i, omega, raan, lv,
                          PositionAngleType.TRUE,
                          INERTIAL, INITIAL_DATE, Constants.WGS84_EARTH_MU)


def getSMABasedManeuverTriggers():
    """
    Create a Semi Major Apsis based maneuver triggers which START maneuver when sma is below 490 km + earth radius
      and STOP maneuver when sma is above 510 km + earth radius.
    :return: Altitude based maneuver trigger
    """
    # Define start detector
    start_detector = BooleanDetector.notCombine(SMADetector(EARTH_RADIUS + 490 * 1000).withMaxCheck(float(10)))

    # Define stop detector
    stop_detector = (SMADetector(EARTH_RADIUS + 510 * 1000).withMaxCheck(float(10)))

    return ActualStartStopTriggerEvents(AbstractDetector.cast_(start_detector), AbstractDetector.cast_(stop_detector))


def getSMABasedManeuvers():
    # Define thrust direction provider (thrust along velocity axis to increase sma)
    thrust_direction_provider = (
        ThrustDirectionAndAttitudeProvider.buildFromCustomAttitude(LofOffset(INERTIAL, LOFType.TNW), Vector3D.PLUS_I))

    # Define maneuver triggers
    maneuver_triggers = getSMABasedManeuverTriggers()

    # Define thrust
    thrust = float(1)  # N, High thrust on purpose

    # Define isp
    isp = float(2000)  # s, High isp on purpose

    return ConfigurableLowThrustManeuver(thrust_direction_provider, maneuver_triggers, thrust, isp)


def getDragForce():
    # Get atmosphere
    atmosphere = NRLMSISE00(CssiSpaceWeatherData(CssiSpaceWeatherData.DEFAULT_SUPPORTED_NAMES), SUN, EARTH_BODYSHAPE)

    # Get drag sensitive (ridiculously high drag coeff on purpose)
    cross_section = float(2)
    drag_coeff = float(2000)
    drag_sensitive = IsotropicDrag(cross_section, drag_coeff)

    return DragForce(atmosphere, drag_sensitive)


### MAIN
if __name__ == '__main__':
    ## Get initial orbit
    initialOrbit = getOrbit()

    # Get Numerical propagator
    propagator_num = getNumericalPropagator(initialOrbit)

    # Create initial state
    satellite_mass = 1000.0  # kg.
    initialState = SpacecraftState(initialOrbit, satellite_mass)

    # Set orbit type & initial state
    propagator_num.setOrbitType(OrbitType.CARTESIAN)
    propagator_num.setInitialState(initialState)

    # Add drag so that the orbit loses energy
    drag_force = getDragForce()
    propagator_num.addForceModel(drag_force)

    # Add configurable low thrust
    sma_maneuvers = getSMABasedManeuvers()
    propagator_num.addForceModel(sma_maneuvers)

    # Add custom handler to register semi major axis evolution throughout the propagation
    sma_handler = SMAHandler()
    Propagator.cast_(propagator_num).setStepHandler(float(10), sma_handler)

    # Propagate for one day
    propagator_num.propagate(initialOrbit.getDate().shiftedBy(Constants.JULIAN_DAY))

    # Plot semi major axis minus earth radius to see maneuver effects
    fig, ax = plt.subplots()
    ax.scatter(np.array(sma_handler.dates) / 3600, (np.array(sma_handler.values) - EARTH_RADIUS) / 1000)
    ax.set(xlabel='time since start (hour)', ylabel='SMA - Earth radius (km)')
    ax.grid()
    plt.show()

Plot

Below is the plot for those who do not have matplotlib :

4 Likes

Thanks a lot for your help it’s working perfectly fine. It’s nice to see how involved is the orekit community. Have a nice day :grinning:

2 Likes

Hello everyone, it’s a pleasure to see how reactive the Orekit community is !

I combined the SMA based trigger with a MissionDuration event which desable any maneuvers after the propagator detect the end of mission date, which is working great. (Use of the Boolean.andCombine([SMADetector, MissionDurationDetector])

To do so, I create a subclass exactly like SMADetector.

Now I’m trying to create the maneuver only when the spacecraft isn’t in Eclipse in order to Combine the start maneuver which will be triggered by (SMA detector and MissionDurationDetector and notInEclipse.) In the same way, my stoptrigger is now
Trigger_start = BooleanDetector.andCombine([sma_start_detector,eclipse_detect_penumbra, MissionDuration_detector])
Trigger_stop = BooleanDetector.orCombine([sma_stop_detector, eclipse_detect_umbra])

However, the eclipse aren’t detected. I think this is due to the fact that EclipseDetector isn’t an abstractDetector (but MissionDurationDetector and SMADetector are AbstractDetector). So it seems that casting detector that aren’t the same type is not possible right ?
Would you have any idea on how to handle it ?

I have a code if needed to let you know how I combined this EventsDetector.

Thanks for your help !

Hello @Emilien_mrlt,

First i think you should use this :

Trigger_start = BooleanDetector.andCombine([sma_start_detector,eclipse_detect_penumbra, MissionDuration_detector])
Trigger_stop = BooleanDetector.andCombine([sma_stop_detector, eclipse_detect_penumbra])

Do you mean that the maneuvers do not even start ?

EclipseDetector is indeed an AbstractDetector so casting should work.

Keep us updated :slight_smile: !

Cheers,
Vincent

Hi Vincent,
To explain a bit, the following code aims to create station keeping maneuver between 480km and 500km. The mission duration is here setted to be 3 years and the propagation duration 4 years (meaning that the maneuver should not be triggered after 3 years.)
Without any Eclipse duration, here is the result, which I think is working nice.

Now I’m trying to allow these differents raising SMA maneuvers only in when the satellite is not in eclipse, by adding EclipseDetector to the trigger maneuver and to stop it too. Only a natural decay is allowed in umbra.
the code is the following :

def getSMABasedManeuverTriggers():
        # Define detectors
        sma_start_detector = BooleanDetector.notCombine(SMA_f_Detector(SK_SMA_LINF).withMaxCheck(float(600)))    # Detect when the lower altitude is reached (which will begin the maneuver)
        sma_stop_detector = BooleanDetector.notCombine(SMA_f_Detector(SK_SMA_SUP).withMaxCheck(float(600)))                                 # Detect when the upper altitude is reached (which will end the maneuver)

        # Defin Eclipse detector
        sun = CelestialBodyFactory.getSun()
        itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
        earth = OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf)

        Eclipse_Detect_Penumbra = BooleanDetector.notCombine(EclipseDetector(sun, Constants.SUN_RADIUS, earth).withPenumbra())
        Eclipse_Detect_Umbra = BooleanDetector.notCombine(EclipseDetector(sun, Constants.SUN_RADIUS, earth).withUmbra())
        # Define mission duration detector
        mission_duration_detector = BooleanDetector.notCombine(MissionDurationDetector(initial_date.shiftedBy(MD)).withMaxCheck(float(10))) # Detect when the end of mission date is passed by the propagator.
        
        # Cast Detectors
        sma_start_detector = AbstractDetector.cast_(sma_start_detector)
        mission_duration_detector = AbstractDetector.cast_(mission_duration_detector)
        sma_stop_detector = AbstractDetector.cast_(sma_stop_detector)   
        Eclipse_Detect_Penumbra = AbstractDetector.cast_(Eclipse_Detect_Penumbra)
        Eclipse_Detect_Umbra = AbstractDetector.cast_(Eclipse_Detect_Umbra)

        # Define detectors for logging events based on SMA limits
        First_Possible_trigger = BooleanDetector.andCombine([sma_start_detector, mission_duration_detector, Eclipse_Detect_Penumbra])
        Second_Possible_trigger = BooleanDetector.andCombine([sma_start_detector, mission_duration_detector,Eclipse_Detect_Penumbra])
        
        # Combine detectors: Only execute SMA maneuvers if within mission duration

        Detector_events_logg_low  =  BooleanDetector.notCombine(SMA_f_Detector(SK_SMA_LINF))
        Mission_duration_event_logg_low = AbstractDetector.cast_(BooleanDetector.notCombine(MissionDurationDetector(initial_date.shiftedBy(MD)).withMaxCheck(float(10))))
        combined_start_detector_loggs = BooleanDetector.andCombine([Detector_events_logg_low, Mission_duration_event_logg_low])
        eventsLogger_lower = EventsLogger()     # Logger to count and record maneuver start events
        eventsLogger_upper = EventsLogger()     # Logger to count and record maneuver end events
        ev_logs_lower = eventsLogger_lower.monitorDetector(combined_start_detector_loggs)  # Monitor events and store them in the logs
        ev_logs_upper = eventsLogger_upper.monitorDetector(SMA_f_Detector(SK_SMA_SUP).withMaxCheck(float(600)))
        
        

        # Stop Trigger: Since my station keeping is defined by severals finite burns to raise the SMA, the finite maneuvers end when there is an eclipse or when the Upper bound of the SMA is reached. 
        Trigger_stop = BooleanDetector.orCombine([sma_stop_detector, Eclipse_Detect_Umbra])
        start_trigger=BooleanDetector.andCombine([First_Possible_trigger, Second_Possible_trigger])
        return ev_logs_lower, eventsLogger_lower, ev_logs_upper, eventsLogger_upper, ActualStartStopTriggerEvents(AbstractDetector.cast_(start_trigger), AbstractDetector.cast_(Trigger_stop))

    def getSMABasedManeuvers():
        thrust_direction_provider = (
            ThrustDirectionAndAttitudeProvider.buildFromCustomAttitude(LofOffset(inertialFrame, LOFType.TNW), Vector3D.PLUS_I)) # Define thrust direction provider for the maneuver

        ev_logs, eventsLogger, ev_logs_upper, eventslogger_upper, maneuver_triggers = getSMABasedManeuverTriggers() # Retrieve event logs, loggers, and maneuver triggers

        return ev_logs, eventsLogger,ev_logs_upper, eventslogger_upper, ConfigurableLowThrustManeuver(thrust_direction_provider, maneuver_triggers, Thrust, ISP)
        # Creation of the low thrust maneuver
    return getSMABasedManeuvers()

The logs are used here to get several informations on the maneuvers (date, duration, …)
I obtained the following output


By zooming, the maneuver isn’t triggered at 480 km (a bit under this threshold) and as you can see, the eclipseDetector isn’t working neither

I divided the start trigger in two separates one because if the second one should be triggered only if the first is (The other maneuvers should only take in consideration mission duration and EclipseDetector, and not the sma Threshold which is the case in here …)
I know this maneuver is a bit tricky to understand as it is described here and I really apologize for it.
the maneuver logic is the following

MANEUVER triggered by ((SMA detector AND time < mission duration AND Penumbra) OR (first maneuver triggered AND time < MD AND Penumbra) UNTIL (SMA_Upper_bound OR Umbra)

Thanks foryour help and thanks for your reply @Vincent !

Trigger_start = BooleanDetector.andCombine([sma_start_detector,eclipse_detect_penumbra, MissionDuration_detector])
Trigger_stop = BooleanDetector.andCombine([sma_stop_detector, eclipse_detect_penumbra])

By doing this, the maneuver will be triggered when all the three detector are setted to True but I want it to be true for the first one and then the maneuver should be triggered when not in eclipse until the sma upper threshold is detected (Without going back to the lower SMA threshold.)

That is why I thought to use BooleanDetector.orCombine([sma_stop_detector, eclipse_detect_penumbra]) to stop the maneuver. (not using a andCombine), but it’s indeed not working as shown in the previous post.

Thanks !

This is (probably) normal as your plot shows the altitude and not the SMA assuming that you are not using a spherical Earth or an equatorial orbit.

It’s hard to see whether it is working or not given the scale on the x axis. I would advise you to purposely increase the drag to see this kind of plots over several days only. It will be :

  • Faster to simulate
  • Easier to see eclipses

Ok thank you for adding this part, it makes things much clearer. You are going to need to do something a bit more elaborate to have this kind of behaviour.

Basically the start trigger is only a trigger so you need to store somewhere the fact that it has been triggered at one point for your second maneuvers to continue their thrust until target upper SMA is reached.

You could create a copy of the start event trigger of your first maneuvers that would be wrapped inside a custom detector. The goal of this custom detector would be to switch to a positive value when the wrapped detector is triggered. This way, the second maneuvers would thrusts when outside eclipse until target upper SMA is reached.

1 Like