TimeEvaluables

In this notebook we are going to learn more about TimeEvaluables in GODOT.

A TimeEvaluable class is simply a class which contains an evaluation method which takes a time and returns a value.

TimeEvaluable is actually a family of interfaces:

There are other similar interfaces you can learn about later. This is where to get started.

There are many implementations of actual classes (not just interfaces) which implement the TimeEvaluable interfaces: look for names containing https://orbit_software.io.esa.int/godotdocs/0.3.1/api/godot.model.html#module-godot.model.common in the godot common library, or peruse the list of (C++) derived types.

This is a big part of the GODOT computation philosophy.

Here we are going to explain how TimeEvaluables can be used, and extended. This all applies to C++ as well as Python of course.

Importing Modules

First we import some necessary modules:

In [1]:
from godot.core import tempo
import godot.core.autodif as ad
import godot.model.common as common

# optionally avoid verbose logging messages
import godot.core.util as util
util.suppressLogger()

Next we will define our first ScalarTimeEvaluable class:

In [2]:
class Square ( common.ScalarTimeEvaluable ):
    def __init__(self, x : common.ScalarTimeEvaluable ):
        super().__init__()
        self.__x = x
    def eval(self, e ):
        x = self.__x.eval(e)
        return x * x

This simple takes computes the square of its input. The input must also be a ScalarTimeEvaluable.

The eval method evaluates its inputs and does the computation.

We introduce common.ConstantScalarTimeEvaluable, which is simply a constant wrapped in a constant TimeEvaluable:

In [3]:
x = common.ConstantScalarTimeEvaluable(2.0)
z = x.eval(tempo.Epoch())
print(z)
2.0

Now we create an instance of our Square object, and evaluate it:

In [4]:
f = Square(x)
f.eval(tempo.Epoch())
Out[4]:
4.0

Now these objects are very useful.

autodif

For one thing, I can use the same class to compute with and without gradients. Suppose I pass Square an autodif type, then I can evaluate it with or without partials by using Epoch or XEpoch respectively.

In [5]:
x = common.ConstantScalarTimeEvaluable(ad.Scalar( 2.0, "X" ))
f = Square(x)
z = f.eval(tempo.Epoch())
print(z)
z = f.eval(tempo.XEpoch())
print(z)
4.0
         Value |             X
  4.000000e+00 |  4.000000e+00

Note that to use autodif properly you need to use the bridge module for Python.

TimeEvaluable Operators

For another thing, I can use some mathematical operations on these objects:

In [6]:
x = common.ConstantScalarTimeEvaluable(2.0)
f = Square(x)
g = f + 3.0 * f
z =  g.eval(tempo.Epoch())
print(z)
z =  g.eval(tempo.XEpoch())
print(z)
16.0
         Value
  1.600000e+01

In [7]:
x = common.ConstantScalarTimeEvaluable(ad.Scalar( 2.0, "X" ))
f = Square(x)
g = f + x * f
z =  g.eval(tempo.Epoch())
print(z)
z =  g.eval(tempo.XEpoch())
print(z)
12.0
         Value |             X
  1.200000e+01 |  1.600000e+01

Now it is important to realise that all this makes use of 'lazy evaluation'. The operations do not actually do the computation, they only set up a tree of functions which can all later, evaluate one another.

To illustrate this, here is another example.

In [8]:
class OtherSquare ( common.ScalarTimeEvaluable ):
    def __init__(self, x : common.ScalarTimeEvaluable ):
        super().__init__()
        self.setx(x)
    def setx(self,x : common.ScalarTimeEvaluable):
        self.__x = x
    def eval(self, e ):
        x = self.__x.eval(e)
        return x * x

Lets create an OtherSquare object, initialise with value 2.0, and compute a function of f.

In [9]:
x = common.ConstantScalarTimeEvaluable(ad.Scalar( 2.0, "X" ))
f = OtherSquare(x)
g = f + x * f
z =  g.eval(tempo.Epoch())
print(z)
z =  g.eval(tempo.XEpoch())
print(z)
12.0
         Value |             X
  1.200000e+01 |  1.600000e+01

Now, we set the value of x inside f to be 3.0, and evaluate g. Because of the lazy evaluation, The result of g is now updated. In addition, there is a another gradient element.

In [10]:
f.setx(common.ConstantScalarTimeEvaluable(ad.Scalar( 3.0, "newX" )))
z =  g.eval(tempo.XEpoch())
print(z)
         Value |             X |          newX
  2.700000e+01 |  9.000000e+00 |  1.800000e+01

The reason there are now 2 partials is because g = f + x * f, there is the gradient of the X in that expression, and the gradient of the newX which is introduced to f.

So, it is very powerful but you have to be careful. When generating you own TimeEvaluable classes for use with autodif, always write unit tests to compute the gradients numerically.

Gradients with respect to initial state

The following is an interesting example of how to test the partials, using the functionality just described above.

First, we load cosmos:

In [11]:
from godot import cosmos
from godot.core import util

Next, we load the universe configuration and create a universe object:

In [12]:
# create the universe
uniConfig = cosmos.util.load_yaml('universe.yml')
uni = cosmos.Universe(uniConfig)

Now we create the trajectory:

In [13]:
# create the trajectory
traConfig = cosmos.util.load_yaml('trajectory.yml')
tra = cosmos.Trajectory(uni, traConfig)

Now, propagate the trajectory with all available partial derivatives tracked:

In [14]:
# avoid verbose propagator logging
util.suppressLogger()

# track all available partials 
uni.parameters.track(uni.parameters.contains())
print(uni.parameters)

# propagate with partials
tra.compute(partials = True)
Parameter name          Type        Physical value Scale          Scaled value   Lower bound    Upper bound    
match1_right_tan        Free        -1.5708        0.1            -15.708        -1.79769e+308  1.79769e+308   
man2_dec                Free        0              0.1            0              -1.79769e+308  1.79769e+308   
man2_end_dv             Free        0.6            0.01           60             1e-06          1.79769e+308   
man2_ras                Free        3.14159        0.1            31.4159        -1.79769e+308  1.79769e+308   
man2_start_tlo          Free        0              0.1            0              -1.79769e+308  1.79769e+308   
arrival_GeoSat_center_inxFree        0              0.1            0              -1.79769e+308  1.79769e+308   
arrival_GeoSat_center_ecyFree        0              0.1            0              -1.79769e+308  1.79769e+308   
launch_GeoSat_center_aopFree        0              0.1            0              -1.79769e+308  1.79769e+308   
arrival_dt              Free        0              1e+06          0              -1.79769e+308  1.79769e+308   
arrival_GeoSat_dv       Free        1.5            0.01           150            -1.79769e+308  1.79769e+308   
launch_GeoSat_center_ranFree        0              0.1            0              -1.79769e+308  1.79769e+308   
arrival_GeoSat_mass     Free        600            100            6              -1.79769e+308  1.79769e+308   
arrival_GeoSat_center_tloFree        3.14159        0.1            31.4159        -1.79769e+308  1.79769e+308   
launch_GeoSat_center_incFree        0.314159       0.1            3.14159        -1.79769e+308  1.79769e+308   
launch_GeoSat_center_rapFree        80000          1e+06          0.08           -1.79769e+308  1.79769e+308   
launch_dt               Free        0              1e+06          0              -1.79769e+308  1.79769e+308   
arrival_GeoSat_center_inyFree        0              0.1            0              -1.79769e+308  1.79769e+308   
launch_GeoSat_center_rpeFree        6678           1e+06          0.006678       -1.79769e+308  1.79769e+308   
arrival_GeoSat_center_ecxFree        0              0.1            0              -1.79769e+308  1.79769e+308   
launch_GeoSat_center_tanFree        0              0.1            0              -1.79769e+308  1.79769e+308   
launch_GeoSat_mass      Free        1000           100            10             -1.79769e+308  1.79769e+308   
man1_end_dv             Free        1.1            0.01           110            1e-06          1.79769e+308   
launch_GeoSat_dv        Free        0              0.01           0              -1.79769e+308  1.79769e+308   
man1_start_tan          Free        3.14159        0.1            31.4159        -1.79769e+308  1.79769e+308   
man1_ras                Free        0              0.1            0              -1.79769e+308  1.79769e+308   
man1_dec                Free        0              0.1            0              -1.79769e+308  1.79769e+308   
match1_left_tan         Free        -1.5708        0.1            -15.708        -1.79769e+308  1.79769e+308   
arrival_GeoSat_center_slrFree        42165          1e+06          0.042165       -1.79769e+308  1.79769e+308   

We now create a TimeEValuable class to compute the distance of the propagated spacecraft to Earth

In [15]:
# define a function
class f(common.ScalarTimeEvaluable):
    def __init__(self):
        super().__init__()
        pass
    def eval(self, e):
        return uni.frames.distance('Earth', 'GeoSat_center', e)

Now, we evaluate it and compute the gradient with respect to the initial state time.

In [16]:
# print out the computed partials with respect to the initial epoch, "launch_dt"
func = f()
e = tempo.XEpoch("2000-01-01T12:00:00.000 TDB")
d = common.resolve( func.eval(e) )
df_dt_ad = d.at("launch_dt")[0]
print(df_dt_ad)
-0.067223855258172

I want to test the value here using numerical differentiation.

In [17]:
# set the initial parameter value to -0.5 secs, and evaluate the function at epoch
uni.parameters.get("launch_dt").setPhysicalValue(-0.5)
tra.compute(partials = False)
fminus = func.eval( e.value() )

# set the initial parameter value to +0.5 secs, and evaluate the function at epoch
uni.parameters.get("launch_dt").setPhysicalValue(+0.5)
tra.compute(partials = False)
fplus = func.eval( e.value() )

# compute the gradient numerically
df_dt_num = fplus-fminus

# print the difference
print( 'Autodif: %15.6f\nNumerical: %15.6f\nDiff(percent): %15.6f' % ( df_dt_ad, df_dt_num, 100 * ( df_dt_ad - df_dt_num )/df_dt_num ) )
Autodif:       -0.067224
Numerical:       -0.067217
Diff(percent):        0.010231