Astro.IQ

Getting Started

Defining a Dynamical Model

In Astro.IQ, all models are initialised based off of the Dynamical_Model class:

class Dynamical_Model(object):
  def __init__(self, si, st, slb, sub, clb, cub, tlb, tub):
  # For Direct Methods
  def EOM_State(self, state, control):
  def EOM_State_Jac(self, state, control):
  # For Indirect Methods
  def EOM_Fullstate(self, fullstate, control):
  def EOM_Fullstate_Jac(self, fullstate, control):
  def Hamiltonian(self, fullstate):
  def Pontryagin(self, fullstate):

Parameters

The system’s state is described by the iterables si and st, describing the system’s initial and target state respectively, which are constrained by lower and upper bounds, slb and sub, of equal dimension, i.e. shape(si) == shape(slb) and v.v.. Similarly, while the control is not yet defined, it is bounded by the iterables clb and cub of equal dimension to the problem’s control space dimensionality. One should note the the dimensionality of the state and control space is uniquely determined by the problem itself. Lastly, because each model environment can be vastly different, it is required to specify the lower and upper bounds of the trajectory’s duration with the floats tlb and tub.

Example

To further elaborate on this methodology, consider a two-dimensional planetary lander, modelled as a point mass capable of thrusting in any direction, given by the set first order ordinary differential equations

, where the set of parameters , maximum thrust, specific impulse, Earth’s sea-level gravity, and the environmental gravity, are inherent to this specific model. This dynamical model in particular has a state space dimensionality of 5 given by the vector

and a control space dimensionality of 3 given by the vector

One can define such a model as follows:

class Lander(Dynamical_Model):
    def __init__(
        self,                           # Allowing the
        si  = [10, 1000, 20, -5, 9500], # initial state,
        st  = [0, 0, 0, 0, 8000],       # target state,
        Isp = 311,                      # and parametres
        g   = 1.6229,                   # to be reinstantiated
        T   = 44000                     # as the user wishes
    ):

        # Problem parametres
        self.Isp  = float(Isp)   # Specific impulse [s]
        self.g    = float(g)     # Environment's gravity [m/s^2]
        self.T    = float(T)     # Maximum thrust [N]
        self.g0   = float(9.802) # Earth's sea-level gravity [m/s^2]

        # Instantiate as a Dynamical_Model
        Dynamical_Model.__init__(
            self,
            si,                            # Initial state
            st,                            # Target state
            [-1000, 0, -500, -500, 0],     # State lower bounds
            [1000, 2000, 500, 500, 10000], # State upper bounds
            [0, -1, -1],                   # Control lower bounds
            [1, 1, 1],                     # Control upper bounds
            1,                             # Time lower bounds
            200                            # Time upper bounds
        )

Equations of Motion

The model must be instantiated having at least the instance method EOM_State, describing it’s equations of motion. Referring back to the above definition of , one can do so as follows

def EOM_State(self, state, control):
    x, y, vx, vy, m = state
    u, ux, uy       = control
    x0 = self.T*u/m
    return array([
        vx,
        vy,
        ux*x0,
        uy*x0 - self.g,
        -self.T*u/(self.Isp*self.g0)
    ], float)

In order to improve the speed and accuracy of gradient based optimisation methods and the a numerical integration, one can optionally also define the square Jacobian matrix for the equations of motion, which for the planetary lander is

which is implemented with the instance method

def EOM_State_Jac(self, state, control):
    x, y, vx, vy, m = state
    u, st, ct       = control
    x0              = self.T*u/m**2
    return array([
        [0, 0, 1, 0,        0],
        [0, 0, 0, 1,        0],
        [0, 0, 0, 0, -ux*x0/m],
        [0, 0, 0, 0, -uy*x0/m],
        [0, 0, 0, 0,        0]
    ], float)

Just defining a model with the instance method EOM_State alone is enough to perform trajectory optimisation using direct methods.

Full State Equations of Motion

If one wishes to use indirect methods to solve a trajectory optimisation problem, it is necessary to define the instance methods EOM_Fullstate, Hamiltonian, and Pontryagin, which require analytical derivation through optimal control theory. Starting out, one first defines the vector of costate variables or l corresponding to the system’s state such that shape(l) == shape(s). In the particular case of the lander described above, this would be

, from which the Hamiltonian is derived through

, where the system’s Lagrangian or cost functional, in this model, is defined as

, which for this model basically says that the optimal trajectory should minimise the fuel expenditure. The Hamiltonian for this model becomes

which is then defined with the instance method

def Hamiltonian(self, fullstate, control):
    x, y, vx, vy, m, lx, ly, lvx, lvy, lm = fullstate
    u, ux, uy = control
    # Get the model parametres
    T, Isp, g0, g = self.T, self.Isp, self.g0, self.g
    # Common sub expression elimination
    x0 = T*u/m
    x1 = 1/(Isp*g0)
    # Dot the costates with the states
    H  = lx*vx + ly*vy + lvx*ux*x0 + lvy*(uy*x0 - g) - T*lvm*u*x1
    # Add the Lagrangian or cost functional
    H += T*u
    return H

After the Hamiltonian is derived, one then derives the costate equations motion, which through optimal control theory is found through

, which for this model becomes

, allowing for instance method EOM_Fullstate to be defined as

def EOM_Fullstate(self, fullstate, control):
    x, y, vx, vy, m, lx, ly, lvx, lvy, lm = fullstate
    u, ux, uy     = control
    T, Isp, g0, g = self.T, self.Isp, self.g0, self.g
    x0            = T*u/m
    x1            = T*u/m**2
    return array([
        [                  vx],
        [                  vy],
        [               ux*x0],
        [           uy*x0 - g],
        [       -T*u/(Isp*g0)],
        [                   0],
        [                   0],
        [                 -lx],
        [                 -ly],
        [x1*(lvx*ux + lvy*uy)]
    ], float)

Additionally, for increased performance of optimisation and numerical integration one can also define the Jacobian matrix of the full state equations of motion

which manifests itself in the instance method

def EOM_Fullstate_Jac(self, fullstate, control):
    x, y, vx, vy, m, lx, ly, lvx, lvy, lm = fullstate
    u, ux, uy = control
    T         = self.T
    x0        = T*u/m**2
    x1        = ux*x0
    x2        = uy*x0
    x3        = 2*T*u/m**3
    return array([
        [0, 0, 1, 0,                      0,  0,  0,  0,  0, 0],
        [0, 0, 0, 1,                      0,  0,  0,  0,  0, 0],
        [0, 0, 0, 0,                    -x1,  0,  0,  0,  0, 0],
        [0, 0, 0, 0,                    -x2,  0,  0,  0,  0, 0],
        [0, 0, 0, 0,                      0,  0,  0,  0,  0, 0],
        [0, 0, 0, 0,                      0,  0,  0,  0,  0, 0],
        [0, 0, 0, 0,                      0,  0,  0,  0,  0, 0],
        [0, 0, 0, 0,                      0, -1,  0,  0,  0, 0],
        [0, 0, 0, 0,                      0,  0, -1,  0,  0, 0],
        [0, 0, 0, 0, -uy*lvy*x3 - lvx*ux*x3,  0,  0, x1, x2, 0]
    ], float)

The only necessary step left to be taken in order to implement indirect methods of trajectory optimisation is to define the instance method Pontryagin, named after Pontryagin’s maximum principle, which takes as its parametres the system’s full state . Through maximizing the Hamiltonian over the set of all possible controls

, an on board optimal control policy is found that maps the system’s full state to an appropriate control, i.e.

In the specific case of the planetary lander being discussed, this manifests in the optimal thrust direction being

and the optimal thrust throttle being

, where the switching function is

After all the analytical derivations are done, one can define the instance method Pontryagin as

def Pontryagin(self, fullstate):
    x, y, vx, vy, m, lx, ly, lvx, lvy, lm = fullstate
    lv = sqrt(abs(lvx)**2 + abs(lvy)**2)
    ux = -lvx/lv
    uy = -lvy/lv
    S  = 1 - self.Isp*self.g0*lv/m - lm
    if S < 0:
      u = 1
    elif S => 0:
      u = 0
    return u, ux, uy

and the full process of defining one’s own Dynamical_Model is completed! One could now use any of the Astro.IQ methods of optimisation to optimise the trajectory.