Adding an AdditionalDerivativesProvider using python

Hi everyone,

I am currently trying to implement a battery management state related to propulsion and position with respect to the sun, but I am having trouble adding an AdditionalDerivativesProvider to my numerical propagator.

class PowerConsumption(AdditionalDerivativesProvider):

    def __init__(self, trigger_name, manage_start, maneuver):
        """ Initialize the Power Consumption provider. """
        self.power_consumption_name = "power-" + trigger_name  
        self.manage_start = manage_start
        self.maneuver = maneuver

    def getName(self):
        """ Get the name of the additional derivatives provider. """
        return self.power_consumption_name

    def getDimension(self):
        """ Get the dimension of the derivatives. """
        return 1  

    def init(self, initial_state, target):
        """ Initialize the state at the beginning of propagation . """
        pass  

    def combinedDerivatives(self, state):
        """ Return no derivatives (pass). """
        return CombinedDerivatives([0.0],[0.0])  


trig = DateBasedManeuverTriggers("trig", AbsoluteDate(2024, 2, 1, TimeScalesFactory.getUTC()), 10000.0)
prop_model = BasicConstantThrustPropulsionModel(1.0, 1000.0, Vector3D.PLUS_I, "LowThrust")
my_maneuver = Maneuver(LofOffset(inertialFrame, LOFType.TNW), trig, prop_model)


initialState = SpacecraftState(initialOrbit, satellite_mass)


power_consumption_provider = PowerConsumption(trigger_name="maneuver1", manage_start=True, maneuver=my_maneuver)
power_consumption_provider.init(initialState, initial_date)


propagator = NumericalPropagator(integrator)
propagator.addAdditionalDerivativesProvider(power_consumption_provider)
propagator.setInitialState(initialState)
final_state = propagator.propagate(initial_date, end_date)

I always get this error :

orekit.JavaError: <super: <class 'JavaError'>, <JavaError object>>
    Java stacktrace:
java.lang.NullPointerException
        at org.orekit.propagation.integration.AbstractIntegratedPropagator.addAdditionalDerivativesProvider(AbstractIntegratedPropagator.java:305)

I looked at the implementation of MassDepletionDelay, but I can’t figure out what I am doing wrong. Thanks in advance, I could really use your help.

Hello @HugoBocc,

You are getting this error because you are trying to directly extends a Java interface which is not possible with the wrapper.

Every interface has a Python equivalent in the wrapper. In your case,
you’ll want to use PythonAdditionalDerivativesProvider instead of AdditionalDerivativesProvider.

class PowerConsumption(PythonAdditionalDerivativesProvider):

Also, don’t forget to call the super init method :

    def __init__(self, trigger_name, manage_start, maneuver):
        super().__init__()
        """ Initialize the Power Consumption provider. """
        self.power_consumption_name = "power-" + trigger_name
        self.manage_start = manage_start
        self.maneuver = maneuver

Additional very useful documentation : examples/1_The_Basics.ipynb · master · Orekit Labs / Orekit Python Wrapper · GitLab

Cheers,
Vincent

1 Like

Okay thank you, it’s working perfectly fine, I chose the AdditionalStateProvider instead of the AdditionalDerivativesProvider to handle the battery capacity which seems to be more appropriate.

2 Likes

Hi again, I’m having trouble using my additional state. Basically, I’m trying to get the thrust duration in real-time to subtract the battery capacity during propagation. When I use a state.getDate() inside my additional state definition, I notice some anomalies. The dates can go backward, like this:

2024-07-20T03:26:39.10816923360107Z  
2024-07-20T03:26:49.10816923360107Z  
2024-07-20T03:26:39.10816923360107Z  
2024-07-20T03:26:49.10816923360107Z  
2024-07-20T03:26:39.10816923360107Z  

It seems to be due to the integrator’s variable step size, but I don’t understand how the time step can be negative.

Hi @HugoBocc,

It’s probably due to event detection.
When the propagator detects the occurrence of an event in a time interval, this triggers a dichotomy method that will pinpoint the exact date of the event.
Thus it enters a loop with forward and backward steps.
In your case, it seems to be the same time steps though, switching between 39 and 49 seconds. I admit this looks weird.
If you can share with us a working example we may be able to understand what’s happening here.

Cheers,
Maxime

Okay I see , those are the interesting part of the code :
This is my custom additional state provider (BMS) that tracks battery capacity based on thrust duration, using the current date from state.getDate() to accumulate time. (The print state.getDate() is there)

class BMS(PythonAdditionalStateProvider):

    def __init__(self, capacity, power_rate, maneuver):
        """
        Initialize the battery management system (BMS).

        :param capacity: Initial battery capacity in Wh.
        :param power_rate: Thruster consumption.
        :param maneuver: The Maneuver object to track.
        """
        super().__init__()
        self.capacity = capacity
        self.power_rate = power_rate
    
        self.maneuver = maneuver
        self.name = "BMS"
        self.last_update_date = None  # To track the last update time
        
    def init(self, s, date):
        """
        Initialize the battery management system (BMS).

        :param s: SpacecraftState at the start.
        :param date: AbsoluteDate, the target date for propagation.
        """
        self.last_update_date = date  # Set the initial date
        pass

    def getName(self):
        """
        Returns the name of the additional state.

        :return: Name of the additional state as a string.
        """
        return self.name  

    def getAdditionalState(self, s):
        current_date = s.getDate()
        print(current_date)
        Etat=self.maneuver.getManeuverTriggers().isFiring(current_date,[])
        #print(Etat)
        global mandetec
        global dtglobal
        if Etat and len(mandetec) ==0:
            mandetec.append(current_date)
            
        if Etat and len(mandetec) !=0:
            previous_step = mandetec[-1]
            
            dt = current_date.durationFrom(previous_step)
            dtglobal=dtglobal+dt
            mandetec.append(current_date)
            
            self.capacity=self.capacity-(dt*self.power_rate)
            
            
        
        return [self.capacity]  # Return the current battery capacity

Then I have 3 detectors which are combined in a startStopDetector :

        sma_start_detector = BooleanDetector.notCombine(SMA_f_Detector(SK_SMA_LINF))             
        sma_stop_detector = SMA_f_Detector(SK_SMA_SUP).withMaxCheck(60.0)                                                            
        Remove_first_trigger = BooleanDetector.notCombine(MissionDurationDetector(initial_date.shiftedBy(2000.0)))
        mission_duration_detector = BooleanDetector.notCombine(MissionDurationDetector(Mission_End_Date))
        # mission_duration_detector = AbstractDetector.cast_(mission_duration_detector)

        # Combine detectors: Only execute SMA maneuvers if within mission duration
        
        combined_start_detector = BooleanDetector.andCombine([sma_start_detector, mission_duration_detector])     
        combined_stop_detector = BooleanDetector.andCombine([NegateDetector(BooleanDetector.notCombine(sma_stop_detector)), NegateDetector(Remove_first_trigger)])  

I also have a orekitfixedstephandler but I assume that It won’t impact the situation.

Finally the integrator is defined as :

    minStep = 0.0000001
    maxStep = 100.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]))

Using these parameters dates are appearing as :
image
Thanks for your time @MaximeJ

Keeping a state in something called by a propagator should be avoided if possible.
This is even more important with numerical propagators. The fact is that as an integrator is attempting to compute one step, it essentially samples both in time and in state vector, trying to build a smooth model. This implies going back and forth in time during step, but it also implies that the state and time may be inconsistent in the trial points. It is only once the step has been accepted that a smooth model is built by the integrator to represent consistent time evolution of the state vector along during the step.

What this means is that when the handleStep method of a step handler or the eventOccurred method of an event detector are called, the model can be trusted, but when either a force model acceleration, or an additional state provider or an event detector g function are called, the one-point input state cannot be trusted, it is only one attempt made by the integrator that may never be on the real trajectory.

This is why in some cases, when we nevertheless need to store something, we end up using TimeSpanMap instances to at least get proper time handling (but we still lose time/state consistency).