Custom Force Model Interface

Greetings,

I’m currently utilizing several custom force models (written in Python) in my own orbit propagator and am interested in integrating them with Orekit. I’ve come across discussions (like this one: Force depending on attitude) suggesting the feasibility of incorporating “home-made” force models into Orekit. However, I’m struggling to find detailed information on implementing this through the Python Wrapper. Could someone confirm if it’s possible to do so? Ideally, my models would read in inputs like position, velocity, and attitude (either Quaternions or Euler angles) from the SpacecraftState and produce accelerations as output. I would greatly appreciate any examples or advice on the best approach to achieve this in Orekit.

Thank you in advance for your assistance!

Hi @charlesc,

As you probably already know, Python doesn’t have interface and abstract class like Java do. As a workaround, you will find Python equivalent classes in the Orekit python wrapper. In your case, the ForceModel interface is present through the Python PythonForceModel or through the AbstractPythonForceModel (i recommend to use this one).

Hence, to create your custom force model in python, you will have to create a class that will inherit from PythonForceModel and override methods from this super class.

This is what i got using PyCharm (FOR PRE 12.0 VERSION ONLY):

class CustomForceModel(PythonAbstractForceModel):

    def __init__(self):
        super().__init__()

    @typing.overload
    def acceleration(self, spacecraftState: org.orekit.propagation.SpacecraftState, doubleArray: typing.List[float]) -> org.hipparchus.geometry.euclidean.threed.Vector3D:
        """
            Compute acceleration. Extension point for Python.

            Parameters:
                s (:class:`~org.orekit.propagation.SpacecraftState`): current state information: date, kinematics, attitude
                parameters (double[]): values of the force model parameters

            Returns:
                acceleration in same frame as state

            Since:
                9.0

            Compute acceleration. Automatically directs to the Python extension point acceleration_FT

            Parameters:
                s (:class:`~org.orekit.propagation.FieldSpacecraftState`<T> s): current state information: date, kinematics, attitude
                parameters (T[]): values of the force model parameters

            Returns:
                acceleration in same frame as state

            Since:
                9.0


        """
        ...

    @typing.overload
    def acceleration(self, fieldSpacecraftState: org.orekit.propagation.FieldSpacecraftState[_acceleration_1__T], tArray: typing.List[_acceleration_1__T]) -> org.hipparchus.geometry.euclidean.threed.FieldVector3D[_acceleration_1__T]: ...

    def acceleration(self, spacecraftState, doubleArray):
        return super().acceleration(spacecraftState, doubleArray)

    def acceleration_FT(self, fieldSpacecraftState, tArray):
        return super().acceleration_FT(fieldSpacecraftState, tArray)

    def dependsOnPositionOnly(self):
        return super().dependsOnPositionOnly()

    def finalize(self):
        super().finalize()

    def getEventsDetectors(self):
        return super().getEventsDetectors()

    def getFieldEventsDetectors(self, field):
        return super().getFieldEventsDetectors(field)

    def getParametersDrivers(self):
        return super().getParametersDrivers()

And this in case you are using the 12.0 or later version of Orekit:

class CustomForceModel(PythonForceModel):

    def __init__(self):
        super().__init__()

    @typing.overload
    def acceleration(self, spacecraftState: org.orekit.propagation.SpacecraftState, doubleArray: typing.List[float]) -> org.hipparchus.geometry.euclidean.threed.Vector3D:
        """
            Compute acceleration.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.acceleration` in interface :class:`~org.orekit.forces.ForceModel`

            Parameters:
                s (:class:`~org.orekit.propagation.SpacecraftState`): current state information: date, kinematics, attitude
                parameters (double[]): values of the force model parameters

            Returns:
                acceleration in same frame as state

            Since:
                9.0

            Compute acceleration.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.acceleration` in interface :class:`~org.orekit.forces.ForceModel`

            Parameters:
                s (:class:`~org.orekit.propagation.FieldSpacecraftState`<T> s): current state information: date, kinematics, attitude
                parameters (T[]): values of the force model parameters

            Returns:
                acceleration in same frame as state

            Since:
                9.0


        """
        ...

    @typing.overload
    def acceleration(self, fieldSpacecraftState: org.orekit.propagation.FieldSpacecraftState[_acceleration_1__T], tArray: typing.List[_acceleration_1__T]) -> org.hipparchus.geometry.euclidean.threed.FieldVector3D[_acceleration_1__T]: ...

    def acceleration(self, spacecraftState, doubleArray):
        return super().acceleration(spacecraftState, doubleArray)

    def acceleration_FT(self, fieldSpacecraftState, tArray):
        return super().acceleration_FT(fieldSpacecraftState, tArray)

    @typing.overload
    def addContribution(self, spacecraftState: org.orekit.propagation.SpacecraftState, timeDerivativesEquations: org.orekit.propagation.numerical.TimeDerivativesEquations) -> None:
        """
            Compute the contribution of the force model to the perturbing acceleration.

            The default implementation simply adds the :meth:`~org.orekit.forces.PythonForceModel.acceleration` as a non-Keplerian
            acceleration.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.addContribution` in interface :class:`~org.orekit.forces.ForceModel`

            Parameters:
                s (:class:`~org.orekit.propagation.SpacecraftState`): current state information: date, kinematics, attitude
                adder (:class:`~org.orekit.propagation.numerical.TimeDerivativesEquations`): object where the contribution should be added

        """
        ...

    @typing.overload
    def addContribution(self, fieldSpacecraftState: org.orekit.propagation.FieldSpacecraftState[_addContribution_1__T], fieldTimeDerivativesEquations: org.orekit.propagation.numerical.FieldTimeDerivativesEquations[_addContribution_1__T]) -> None:
        """
            Compute the contribution of the force model to the perturbing acceleration.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.addContribution` in interface :class:`~org.orekit.forces.ForceModel`

            Parameters:
                s (:class:`~org.orekit.propagation.FieldSpacecraftState`<T> s): current state information: date, kinematics, attitude
                adder (:class:`~org.orekit.propagation.numerical.FieldTimeDerivativesEquations`<T> adder): object where the contribution should be added


        """
        ...

    def addContribution(self, spacecraftState, timeDerivativesEquations):
        super().addContribution(spacecraftState, timeDerivativesEquations)

    def addContribution_FF(self, fieldSpacecraftState, fieldTimeDerivativesEquations):
        super().addContribution_FF(fieldSpacecraftState, fieldTimeDerivativesEquations)

    def dependsOnPositionOnly(self):
        return super().dependsOnPositionOnly()

    def finalize(self):
        super().finalize()

    def getEventsDetectors(self):
        return super().getEventsDetectors()

    def getFieldEventsDetectors(self, field):
        return super().getFieldEventsDetectors(field)

    def getParameterDriver(self, string):
        return super().getParameterDriver(string)

    @typing.overload
    def getParameters(self) -> typing.List[float]:
        """
            Get force model parameters.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.getParameters` in interface :class:`~org.orekit.forces.ForceModel`

            Returns:
                force model parameters

            Since:
                9.0

        """
        ...

    @typing.overload
    def getParameters(self, field: org.hipparchus.Field[_getParameters_1__T]) -> typing.List[_getParameters_1__T]:
        """
            Get force model parameters.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.getParameters` in interface :class:`~org.orekit.forces.ForceModel`

            Parameters:
                field (Field<T> field): field to which the elements belong

            Returns:
                force model parameters

            Since:
                9.0


        """
        ...

    def getParameters(self):
        return super().getParameters()

    def getParametersDrivers(self):
        return super().getParametersDrivers()

    def getParameters_F(self, field):
        return super().getParameters_F(field)

    @typing.overload
    def init(self, fieldSpacecraftState: org.orekit.propagation.FieldSpacecraftState[_init_0__T], fieldAbsoluteDate: org.orekit.time.FieldAbsoluteDate[_init_0__T]) -> None: ...

    @typing.overload
    def init(self, spacecraftState: org.orekit.propagation.SpacecraftState, absoluteDate: org.orekit.time.AbsoluteDate) -> None:
        """
            Initialize the force model at the start of propagation. This method will be called before any calls to
            :meth:`~org.orekit.forces.PythonForceModel.addContribution`,
            :meth:`~org.orekit.forces.PythonForceModel.addContribution`, :meth:`~org.orekit.forces.PythonForceModel.acceleration` or
            :meth:`~org.orekit.forces.PythonForceModel.acceleration`

            The default implementation of this method does nothing.

            Specified by:
                :meth:`~org.orekit.forces.ForceModel.init` in interface :class:`~org.orekit.forces.ForceModel`

            Parameters:
                initialState (:class:`~org.orekit.propagation.SpacecraftState`): spacecraft state at the start of propagation.
                target (:class:`~org.orekit.time.AbsoluteDate`): date of propagation. Not equal to :code:`initialState.getDate()`.


        """
        ...

    def init(self, fieldSpacecraftState, fieldAbsoluteDate):
        super().init(fieldSpacecraftState, fieldAbsoluteDate)

    def isSupported(self, string):
        return super().isSupported(string)

Usually, you do not have to override every methods in the class but that depend on your use case so i added every method just in case. From there, you will have access to the spacecraft state and its attitude to perform your computations :slight_smile: !

Hope i was of help.

Cheers,
Vincent

2 Likes

Hi @Vincent,

This is exactly what I was looking for! :heart_eyes:

Thank you so much.

Charles

You are welcome :smile: !

Also, i highly recommend you to look at this in case you are starting out with the Orekit python wrapper.

Several examples of implementation of Orekit interfaces in the Python wrapper are also availabe on this page

Cheers,
Vincent

Perfect. Thanks again for taking the time to share all this info! I will take a look at those.

I have a couple of follow up questions:

  1. You recommended to use AbstractPythonForceModel, but it seems from this post (Wrong and missing methods in PythonAbstractForceModel (#458) · Issues · Orekit Labs / Orekit Python Wrapper · GitLab) that it no longer exists in since Orekit v12. Moreover I can only find PythonForceModel in org.orekit.forces. Should I be looking for AbstractPythonForceModel elsewhere? EDIT: I can see that you are answering my questions before I can finish writing them- this is what I call a responsive community :rofl:

  2. Looking at the code you provided and the example of how to subclass a Java class in Python here (cell 14 and 15 of this notebook).
    I have been trying to make a very simple dummy force model as follows:

class CustomForceModel(PythonForceModel):

    def __init__(self):
        super().__init__()

    def init(self):
        pass
    
    def acceleration(self, spacecraftState):
        """
            Compute simple acceleration.

        """
        acceleration = Vector3D(1, 0, 0)
        return acceleration
    
    def getParametersDrivers(self):
        pass
    
    def getEventDetectors(self):
        super().getEventDetectors()

However, I am getting the following error- which seems like similar issue to the one raised here
I simply took the code below from the tutorial you showed me and added the force model:

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)
satellite_mass = 100.0  # The models need a spacecraft mass, unit kg.
initialState = SpacecraftState(initialOrbit, satellite_mass) 
propagator_num = NumericalPropagator(integrator)
propagator_num.setOrbitType(OrbitType.CARTESIAN)
propagator_num.setInitialState(initialState)
gravityProvider = GravityFieldFactory.getNormalizedProvider(10, 10)
propagator_num.addForceModel(HolmesFeatherstoneAttractionModel(FramesFactory.getITRF(IERSConventions.IERS_2010, True), gravityProvider))
customForceModel = CustomForceModel()
propagator_num.addForceModel(customForceModel)
end_state = propagator_num.propagate(initialDate, initialDate.shiftedBy(3600.0 * 1))
end_state
---------------------------------------------------------------------------
JavaError                                 Traceback (most recent call last)
Cell In[40], line 1
----> 1 end_state = propagator_num.propagate(initialDate, initialDate.shiftedBy(3600.0 * 1))
      2 end_state
      4 #<SpacecraftState: SpacecraftState{orbit=Cartesian parameters: {P(3448526.581483817, 244918.37251755025, -5864890.887030139), V(6464.092116094485, 1251.1535044696986, 3907.265608060511)}, attitude=org.orekit.attitudes.Attitude@3bae64d0, mass=100.0, additional={}, additionalDot={}}>

JavaError: <super: <class 'JavaError'>, <JavaError object>>
    Java stacktrace:
java.lang.NullPointerException
	at org.orekit.propagation.events.EventDetectorsProvider.getEventDetectors(EventDetectorsProvider.java:85)
	at org.orekit.forces.ForceModel.getEventDetectors(ForceModel.java:101)
	at org.orekit.propagation.numerical.NumericalPropagator$Main.<init>(NumericalPropagator.java:914)
	at org.orekit.propagation.numerical.NumericalPropagator.getMainStateEquations(NumericalPropagator.java:890)
	at org.orekit.propagation.integration.AbstractIntegratedPropagator.createODE(AbstractIntegratedPropagator.java:598)
	at org.orekit.propagation.integration.AbstractIntegratedPropagator.integrateDynamics(AbstractIntegratedPropagator.java:471)
	at org.orekit.propagation.integration.AbstractIntegratedPropagator.propagate(AbstractIntegratedPropagator.java:424)

Do you know if I should be initialising the getEventDetectors as part of my force model? I have tried with/without and simply writing pass in the body. I have also tried defining all the functions with pass but I always get this error…

Apologies for the lengthy reply and thanks again for all your help so far.

Best regards,
Charles

I managed to reproduce the issue, I’m investigating the issue :+1:

1 Like

Hey @charlesc, i’ve got a solution :partying_face: :

import orekit
from orekit.pyhelpers import setup_orekit_curdir
from org.hipparchus.geometry.euclidean.threed import Vector3D
from org.hipparchus.ode.nonstiff import DormandPrince853Integrator
from org.orekit.forces import PythonForceModel
from org.orekit.forces.gravity import HolmesFeatherstoneAttractionModel
from org.orekit.forces.gravity.potential import GravityFieldFactory
from org.orekit.frames import FramesFactory
from org.orekit.orbits import KeplerianOrbit
from org.orekit.orbits import OrbitType
from org.orekit.orbits import PositionAngleType
from org.orekit.propagation import SpacecraftState
from org.orekit.propagation.numerical import NumericalPropagator
from orekit import JArray_double
from java.util import Collections
from java.util.stream import Stream
from org.orekit.time import AbsoluteDate
from org.orekit.utils import Constants
from org.orekit.utils import IERSConventions

vm = orekit.initVM()
setup_orekit_curdir("../common/resources/orekit-data.zip")

class CustomForceModel(PythonForceModel):

    def __init__(self):
        super().__init__()

    def acceleration(self, fieldSpacecraftState, tArray):
        """
            Compute simple acceleration.

        """
        acceleration = Vector3D(1, 0, 0)
        return acceleration

    def addContribution(self, fieldSpacecraftState, fieldTimeDerivativesEquations):
        pass

    def getParametersDrivers(self):
        return Collections.emptyList()

    def init(self, fieldSpacecraftState, fieldAbsoluteDate):
        pass

    def getEventDetectors(self):
        return Stream.empty()


mu = Constants.IERS2010_EARTH_MU
initialDate = AbsoluteDate()
inertialFrame = FramesFactory.getEME2000()
initialOrbit = KeplerianOrbit(6878000., 0.001, 1., 0., 0., 0., PositionAngleType.MEAN, inertialFrame, initialDate, mu)

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)
satellite_mass = 100.0  # The models need a spacecraft mass, unit kg.
initialState = SpacecraftState(initialOrbit, satellite_mass)
propagator_num = NumericalPropagator(integrator)
propagator_num.setOrbitType(OrbitType.CARTESIAN)
propagator_num.setInitialState(initialState)
gravityProvider = GravityFieldFactory.getNormalizedProvider(10, 10)
propagator_num.addForceModel(HolmesFeatherstoneAttractionModel(FramesFactory.getITRF(IERSConventions.IERS_2010, True), gravityProvider))
customForceModel = CustomForceModel()
propagator_num.addForceModel(customForceModel)
end_state = propagator_num.propagate(initialDate, initialDate.shiftedBy(3600.0 * 1))
end_state

I have eliminated all methods that were not required in your case so this is the simplest example we can make !

Keep me updated ;).

Cheers,
Vincent

1 Like

Amazing!! Yes this works for me too!
Thank you so much again for your time @Vincent :pray: :grin:

Will post my implementation here once I have something working in case anyone else is trying to do the same in the future…

You are welcome once again :+1: !

That would be very helpful to other users, thank you !

Hello! It’s me again :slight_smile:

Here is the code that I ended up using to compute ERP using the CERES SYN1DEG data. It does not (although it should for real geodetic grade POD) include angular distribution models based on the land surface type and cloud types (outlined here if anyone is ever interested).
Sorry it is a bit “hacky” in some places and I use a number of my own helper functions- I imagine some of these already exist in orekit, but it was just faster for me to use these :stuck_out_tongue:

I took a starlink TLE as initial conditions and propagated for 6 hours

  • Once with just 10,10 gravity field
  • Once with 10,10 gravity field and KnockeRediffusedForceModel
  • Once with 10,10 gravity field and the method below.
    both methods are quite similar.

I would love to hear if anyone had any suggestions for improvements.
Thanks again for your help @Vincent !!

import orekit
from orekit.pyhelpers import setup_orekit_curdir, absolutedate_to_datetime
orekit.pyhelpers.download_orekit_data_curdir()
vm = orekit.initVM()
setup_orekit_curdir()
from org.orekit.utils import Constants, PVCoordinates, IERSConventions
from org.orekit.forces import PythonForceModel
from org.orekit.frames import FramesFactory
from org.orekit.bodies import OneAxisEllipsoid
from org.hipparchus.geometry.euclidean.threed import Vector3D
from orekit import JArray_double
from java.util import Collections
from java.util.stream import Stream


from tools.utilities import find_nearest_index, lla_to_ecef, julian_day_to_ceres_time
from tools.data_processing import calculate_satellite_fov, is_within_fov_vectorized, sat_normal_surface_angle_vectorized
import numpy as np

def compute_erp_at_sc(ceres_time_index, radiation_data, sat_lat, sat_lon, sat_alt, horizon_dist):
    # Earth radius in meters
    R = Constants.WGS84_EARTH_EQUATORIAL_RADIUS
    # Latitude and longitude arrays
    lat = np.arange(-89.5, 90.5, 1)  # 1-degree step from -89.5 to 89.5
    lon = np.arange(-179.5, 180.5, 1)  # 1-degree step from -179.5 to 179.5

    # Mesh grid creation
    lon2d, lat2d = np.meshgrid(lon, lat)

    # FOV calculations
    fov_mask = is_within_fov_vectorized(sat_lat, sat_lon, horizon_dist, lat2d, lon2d)
    radiation_data_fov = np.ma.masked_where(~fov_mask, radiation_data[ceres_time_index, :, :])
    cos_thetas = sat_normal_surface_angle_vectorized(sat_alt, sat_lat, sat_lon, lat2d[fov_mask], lon2d[fov_mask])
    cosine_factors_2d = np.zeros_like(radiation_data_fov)
    cosine_factors_2d[fov_mask] = cos_thetas

    # Adjusting radiation data
    adjusted_radiation_data = radiation_data_fov * cosine_factors_2d

    # Satellite position and distance calculations
    sat_ecef = np.array(lla_to_ecef(sat_lat, sat_lon, sat_alt))
    ecef_x, ecef_y, ecef_z = lla_to_ecef(lat2d, lon2d, np.zeros_like(lat2d))
    ecef_pixels = np.stack((ecef_x, ecef_y, ecef_z), axis=-1)
    vector_diff = sat_ecef.reshape((1, 1, 3)) - ecef_pixels
    distances = np.linalg.norm(vector_diff, axis=2) * 1000  # Convert to meters

    # Radiation calculation
    delta_lat = np.abs(lat[1] - lat[0])
    delta_lon = np.abs(lon[1] - lon[0])
    area_pixel = R**2 * np.radians(delta_lat) * np.radians(delta_lon) * np.cos(np.radians(lat2d))  # Convert to m^2
    P_rad = adjusted_radiation_data * area_pixel / (np.pi * distances**2) # map of power flux in W/m^2 for each pixel

    #need to convert the vector_diff to eci frame
    unit_vectors = vector_diff / distances[..., np.newaxis] * 1000 # Convert to unit vectors in meters
    radiation_force_vectors = unit_vectors * P_rad[..., np.newaxis] / (299792458)  # Convert to Newtons
    # Summing all force vectors
    total_radiation_force_vector = np.sum(radiation_force_vectors[fov_mask], axis=0)
    # Satellite area in square meters
    satellite_area = 10.0

    # Total force due to radiation pressure (not including reflection for now)
    total_force = total_radiation_force_vector * satellite_area

    # Satellite mass in kilograms
    satellite_mass = 500.0

    # Calculate acceleration
    acceleration_vector = total_force / satellite_mass

    scalar_acc = np.linalg.norm(acceleration_vector)
    print(f"scalar_acc:{scalar_acc}m/s^2")

    down_vector = - sat_ecef / np.linalg.norm(sat_ecef)  # Normalize the satellite's position vector to get the down vector
    total_radiation_vector_normalized = total_radiation_force_vector / np.linalg.norm(total_radiation_force_vector)  # Normalize the total radiation vector

    cos_theta = np.dot(total_radiation_vector_normalized, down_vector)  # Cosine of the angle
    angle_radians = np.arccos(cos_theta)  # Angle in radians
    angle_degrees = np.rad2deg(angle_radians)  # Convert to degrees

    return np.array(acceleration_vector), scalar_acc, angle_degrees

class CERES_ERP_ForceModel(PythonForceModel):
    def __init__(self, ceres_times, combined_radiation_data):
        super().__init__()
        self.scalar_acc_data = []
        self.erp_angle_data = []
        self.time_data = []  
        self.ceres_times = ceres_times
        self.combined_radiation_data = combined_radiation_data

    def acceleration(self, spacecraftState, doubleArray):
        # Compute the current altitude within the acceleration method
        pos = spacecraftState.getPVCoordinates().getPosition()
        alt_m = pos.getNorm() - Constants.WGS84_EARTH_EQUATORIAL_RADIUS
        alt_km = alt_m / 1000.0
        horizon_dist = calculate_satellite_fov(alt_km)
        # Get the AbsoluteDate
        absolute_date = spacecraftState.getDate()
        # Convert AbsoluteDate to Python datetime
        date_time = absolutedate_to_datetime(absolute_date)
        # convert date_time to julianday
        jd_time = date_time.toordinal() + 1721425.5 + date_time.hour / 24 + date_time.minute / (24 * 60) + date_time.second / (24 * 60 * 60)
        # Get the ECI and ECEF frames
        ecef = FramesFactory.getITRF(IERSConventions.IERS_2010, True)
        # Transform the position vector to the ECEF frame
        pv_ecef = spacecraftState.getPVCoordinates(ecef).getPosition()
        # Define the Earth model
        earth = OneAxisEllipsoid(
            Constants.WGS84_EARTH_EQUATORIAL_RADIUS, 
            Constants.WGS84_EARTH_FLATTENING, 
            ecef)
        # Convert ECEF coordinates to geodetic latitude, longitude, and altitude
        geo_point = earth.transform(pv_ecef, ecef, spacecraftState.getDate())
        # Extract latitude and longitude in radians
        latitude = geo_point.getLatitude()
        longitude = geo_point.getLongitude()
        # Convert radians to degrees if needed
        latitude_deg = np.rad2deg(latitude)
        longitude_deg = np.rad2deg(longitude)
        ceres_time = julian_day_to_ceres_time(jd_time)
        ceres_indices = find_nearest_index(self.ceres_times, ceres_time)
        erp_vec, scalar_acc, erp_angle = compute_erp_at_sc(ceres_indices, self.combined_radiation_data, latitude_deg, longitude_deg, alt_km, horizon_dist)
        # convert the erp_vec to ECI
        eci = FramesFactory.getEME2000()
        # Transform the position vector to the ECI frame
        transform = ecef.getTransformTo(eci, spacecraftState.getDate())
        erp_vec_ecef_pv = PVCoordinates(Vector3D(float(erp_vec[0]), float(erp_vec[1]), float(erp_vec[2])))
        # Transform the ERP vector to ECI frame
        erp_vec_eci_pv = transform.transformPVCoordinates(erp_vec_ecef_pv)
        # Extract the transformed vector components
        erp_vec_eci = erp_vec_eci_pv.getPosition()
        # Store the scalar acceleration and other data
        self.scalar_acc_data.append(scalar_acc)
        self.erp_angle_data.append(erp_angle)
        self.time_data.append(jd_time)
        # Create the Orekit vector for the ERP force in ECI frame
        orekit_erp_vec = Vector3D(erp_vec_eci.getX(), erp_vec_eci.getY(), erp_vec_eci.getZ())
        # print("orekit_erp_vec components:", orekit_erp_vec.getX(), orekit_erp_vec.getY(), orekit_erp_vec.getZ())
        return orekit_erp_vec
    
    def addContribution(self, spacecraftState, timeDerivativesEquations):
        timeDerivativesEquations.addNonKeplerianAcceleration(self.acceleration(spacecraftState, None))

    def getParametersDrivers(self):
        return Collections.emptyList()

    def init(self, spacecraftState, absoluteDate):
        pass

    def getEventDetectors(self):
        return Stream.empty()
4 Likes

Hi there!

I am relatively new to Orekit (and also Python) but have some experience with Orbit Determination. I came across this post when trying to implement a custom force model using the Python wrapper. First of all, thank you for raising the issue and providing a workable solution. I implemented the above with my custom acceleration and it seems to work flawlessly but since I intend to use this as part of an estimation process, I need to compute the partial derivatives (add a MatricesHarvester to the propagator).

If I only implement the general (SpacecraftState) acceleration method, I get the following error:

orekit_stelar.JavaError: <super: <class 'JavaError'>, <JavaError object>>
    Java stacktrace:
java.lang.RuntimeException: InvalidArgsError
	at org.orekit.forces.PythonForceModel.acceleration(Native Method)
	at org.orekit.propagation.numerical.StateTransitionMatrixGenerator.computePartials(StateTransitionMatrixGenerator.java:251)
	at org.orekit.propagation.numerical.StateTransitionMatrixGenerator.combinedDerivatives(StateTransitionMatrixGenerator.java:183)
	at org.orekit.propagation.integration.AbstractIntegratedPropagator$ConvertedSecondaryStateEquations.computeDerivatives(AbstractIntegratedPropagator.java:827)
	at org.hipparchus.ode.ExpandableODE.computeDerivatives(ExpandableODE.java:139)
	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:477)
	at org.orekit.propagation.integration.AbstractIntegratedPropagator.propagate(AbstractIntegratedPropagator.java:424)

And I am unable to properly instantiate the acceleration overload method that takes FieldSpacecraftState[_acceleration_0__T] as input, I see the following:

Signature of "acceleration" incompatible with supertype "PythonForceModel"mypyoverride
     Superclass:
         @overload
         def [_acceleration_0__T: CalculusFieldElement[Any]] acceleration(self, fieldSpacecraftState: FieldSpacecraftState[_acceleration_0__T], tArray: list[_acceleration_0__T] | Any) -> FieldVector3D[_acceleration_0__T]
         @overload
         def acceleration(self, spacecraftState: SpacecraftState, doubleArray: list[float] | Any) -> Vector3D
     Subclass:
         def acceleration(self, fieldSpacecraftState: FieldSpacecraftState[Any], tArray: Any) -> Any

My current implementation is the following, but I assume the overloaded acceleration should be modified to provide a FieldVector3D:

class ExponentialForceModel(PythonForceModel):
    def __init__(self, accel0, beta, t_0, context):
        super().__init__()
        self.accel0 = Vector3D(float(accel0[0]), float(accel0[1]), float(accel0[2]))
        self.beta = Vector3D(float(beta[0]), float(beta[1]), float(beta[2]))
        self.t_0 = t_0
        self.context = context
        # For some reason the code below does not work so we need to find a workaround
        # This is in principle related with the NSOrekitContext setup since it seems to work in Jupyter
        # self.parameters_drivers = ArrayList().of_(ParameterDriver)
        # self.parameters_drivers.add(ParameterDriver("aR", float(accel0[0]), 1.0, -1.0, 1.0))
        # self.parameters_drivers.add(ParameterDriver("aS", float(accel0[1]), 1.0, -1.0, 1.0))
        # self.parameters_drivers.add(ParameterDriver("aW", float(accel0[2]), 1.0, -1.0, 1.0))

    # _acceleration_0__T = typing.TypeVar("_acceleration_0__T", bound=CalculusFieldElement)  # <T>

    def acceleration(self, spacecraftState: SpacecraftState, doubleArray):
        state = self.context.pvcoordinates_to_array(spacecraftState.getPVCoordinates())
        deltat = spacecraftState.getDate().offsetFrom(self.t_0, self.context.timescales.getUTC())
        rot_mat = get_rsw_mat(state)
        accel0 = np.array(self.accel0.toArray())
        beta = np.array(self.beta.toArray())
        accel_rsw = np.multiply(np.exp(-beta * deltat), accel0)
        accel = rot_mat.transpose() @ accel_rsw
        acceleration = Vector3D(float(accel[0]), float(accel[1]), float(accel[2]))
        return acceleration

    def acceleration(self, fieldSpacecraftState: FieldSpacecraftState, tArray):
        """
        Compute exponentially decreasing acceleration of the form a(t) = exp(-beta*(t-t0))*a(t0)

        """
        state = self.context.pvcoordinates_to_array(fieldSpacecraftState.getPVCoordinates())
        deltat = fieldSpacecraftState.getDate().offsetFrom(self.t_0, self.context.timescales.getUTC())
        rot_mat = get_rsw_mat(state)
        accel0 = np.array(self.accel0.toArray())
        beta = np.array(self.beta.toArray())
        accel_rsw = np.multiply(np.exp(-beta * deltat), accel0)
        accel = rot_mat.transpose() @ accel_rsw
        acceleration = Vector3D(float(accel[0]), float(accel[1]), float(accel[2]))
        return acceleration

    def addContribution(self, fieldSpacecraftState, fieldTimeDerivativesEquations):
        fieldTimeDerivativesEquations.addNonKeplerianAcceleration(self.acceleration(fieldSpacecraftState, None))

    def dependsOnPositionOnly(self):
        return False  # super().dependsOnPositionOnly()

    def getParametersDrivers(self):
        # return self.parameters_drivers
        return Collections.emptyList()

    # def getParameterDriver(self, string):
    # if string == "aR":
    #    return self.parameters_drivers.get(0).cast_(ParameterDriver)
    # elif string == "aS":
    #    return self.parameters_drivers.get(1)
    # elif string == "aW":
    #    return self.parameters_drivers.get(2)

    def init(self, fieldSpacecraftState, fieldAbsoluteDate):
        pass

    def getEventDetectors(self):
        return Stream.empty()

Any help would be highly appreciated.

Thank you in advance.

Hi,

I assumed you’re using Orekit 12.X. With these versions, if a method in Java has a Field and a non Field version, there will be only one method in Python. You can use the match pattern (native since 3.10) on the argument to deal with it.

Cheers,
Romain.

1 Like