Module MAPLEAF.Motion.Integration

Defines ODE integrators for constant and adaptive time stepping. Used by the RigidBody classes to integrate rocket motion

Both the integrator and the adaptive integrator classes are callable, meaning they can be called like a function once instantiated This is facilitated by their call methods

See RigidBody_3DoF.timeStep() for an example use of these Integrators (see RigidBody_3DoF for creation of the self.integrate function using the integratorFactory below)

Many of the integration methods defined below rely on Butcher tableaus, in MAPLEAF they are represented by lists of lists as follows: Expected Format (Example is RK4 - 3/8 method, see conventional Butcher tableau here: https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods#3/8-rule_fourth-order_method): [ # Top 0 row ommitted [ 1/3, 1/3 ], # Row 1: all coefficients sequentially, first column is current time (c_i), remainder are a_{i1} to a_{in} (derivative coefficients) [ 2/3, -1/3, 1.0 ], # Row 2: '' [ 1.0, 1.0, -1.0, 1.0 ], # Row 3: '' [ 1/8, 3/8, 3/8, 1/8 ] # Row 4: (result calculation row) - empty space on left ignored, just enter all coefficients ]

For adaptive methods, the bottom two rows are 'result calculation rows'
    The second last row is expected to represent the higher-accuracy method
    The last row is expected to represent the lower-accuracy method 
    The difference between the results obtained from the two methods becomes the error estimate

Learn about Butcher Tableaus here: <https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods>
    RK4 - 3/8 Butcher Tableau is present there in the Examples section
Expand source code
''' 
    Defines ODE integrators for constant and adaptive time stepping. Used by the `MAPLEAF.Motion.RigidBody` classes to integrate rocket motion 

    Both the integrator and the adaptive integrator classes are callable, meaning they can be called like a function once instantiated
        This is facilitated by their __call__ methods

    See `MAPLEAF.Motion.RigidBodies.RigidBody_3DoF.timeStep` for an example use of these Integrators 
        (see `MAPLEAF.Motion.RigidBodies.RigidBody_3DoF.__init__` for creation of the self.integrate function using the integratorFactory below)

    Many of the integration methods defined below rely on Butcher tableaus, in MAPLEAF they are represented by lists of lists as follows:
        Expected Format (Example is RK4 - 3/8 method, see conventional Butcher tableau here: https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods#3/8-rule_fourth-order_method):
            [                                   # Top 0 row ommitted
                [ 1/3, 1/3 ],                   # Row 1: all coefficients sequentially, first column is current time (c_i), remainder are a_{i1} to a_{in} (derivative coefficients)
                [ 2/3, -1/3, 1.0 ],             # Row 2: ''
                [ 1.0, 1.0, -1.0, 1.0 ],        # Row 3: ''
                [ 1/8, 3/8, 3/8, 1/8 ]          # Row 4: (result calculation row) - empty space on left ignored, just enter all coefficients
            ]

        For adaptive methods, the bottom two rows are 'result calculation rows'
            The second last row is expected to represent the higher-accuracy method
            The last row is expected to represent the lower-accuracy method 
            The difference between the results obtained from the two methods becomes the error estimate

        Learn about Butcher Tableaus here: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods
            RK4 - 3/8 Butcher Tableau is present there in the Examples section
'''
import math
from abc import ABC, abstractmethod
from typing import Callable

__all__ = [ "integratorFactory" ]

def checkButcherTableau(tableau):
    ''' Checks that the Butcher tableau passed in represents a consistent R-K method '''
    lastRowLength = 0
    for i in range(len(tableau)):
        if len(tableau[i]) == lastRowLength:
            # We're checking a row of "b" coefficients (constructing a final answer)
            # Here the sum of all numbers should be 1
            sumB = sum(tableau[i])
            assert math.isclose(sumB, 1.0), "Sum of 'b' coefficients ({}) of butcher tableau row {} don't = 1".format(sumB, i)
        else:
            # We're checking a row of "c" and "a" coefficients (constructing one of the k values)
            # Here the first number, the c coefficient, should be equal to the sum of all the other numbers (the a coefficients)
            c = tableau[i][0]
            sumA = sum(tableau[i][1:])
            assert math.isclose(c, sumA), "Sum of 'a' coefficients ({}) of butcher tableau row {} don't = 'c' coefficient from the same row {}".format(sumA, i, c)

        lastRowLength = len(tableau[i])

    for i in range(len(tableau)):
        for j in range(len(tableau[i])):
            if tableau[i][j] == 0:
                tableau[i][j] = 1e-16

def integratorFactory(integrationMethod="Euler", simDefinition=None, discardedTimeStepCallback=None):
    ''' 
        Returns a callable integrator object

        Inputs:
            * integrationMethod: (str) Name of integration method: Examples = "Euler", "RK4", "RK23Adaptive", and "RK45Adaptive"
            * simDefinition: (`MAPLEAF.IO.SimDefinition`) for adaptive integration, provide a simdefinition file with time step adaptation parameters
            * discardedTimeStepCallback: (1-argument function reference) for adaptive integration, this function (if provided) is called when a time step is computed,
                but then discarded and re-computed with a smaller timestep to remain below the max estimated error threshold. Used by MAPLEAF to remove
                force calculation logs from those discarded time steps
    '''
    if "Adapt" in integrationMethod:
        if simDefinition == None:
            raise ValueError("SimDefinition object required to initialize adaptive integrator")

        from MAPLEAF.IO import SubDictReader
        adaptDictReader = SubDictReader("SimControl.TimeStepAdaptation", simDefinition)

        # Adaptive Integration
        controller = adaptDictReader.getString("controller")
        if controller == "PID":
            PIDCoeffs = [ float(x) for x in adaptDictReader.getString("PID.coefficients").split() ]
            safetyFactor = None
        elif controller == "elementary":
            safetyFactor = adaptDictReader.getFloat("Elementary.safetyFactor")
            PIDCoeffs = None

        targetError = adaptDictReader.getFloat("targetError")
        minFactor = adaptDictReader.getFloat("minFactor")
        maxFactor = adaptDictReader.getFloat("maxFactor")
        maxTimeStep = adaptDictReader.getFloat("maxTimeStep")
        minTimeStep = adaptDictReader.getFloat("minTimeStep")
        
        return AdaptiveIntegrator(
            method=integrationMethod, 
            controller=controller, 
            targetError=targetError, 
            maxMinSafetyFactors=[maxFactor, minFactor, safetyFactor], 
            PIDCoeffs=PIDCoeffs, 
            maxTimeStep=maxTimeStep, 
            minTimeStep=minTimeStep, 
            discardedTimeStepCallback=discardedTimeStepCallback
        )
    
    else:
        # Constant time step integrator
        return ClassicalIntegrator(method=integrationMethod)


class Integrator(ABC):
    @abstractmethod
    def __call__(self, initVal, initTime:float, derivativeFunc:Callable, dt:float):
        '''
            See `ClassicalIntegrator._integrateEuler` for simplest possible implementation
            
            Inputs:
                initVal: must be addable with other objects of its type
                derivativeFunc: is expected to be a function which accepts the following arguments: initTime, initVal
                    Should return an object representing the derivative of objects of type(initVal)
                        - must be multipliable by objects of type(dt) -> should return an object of type(initVal) from the multiplication operation

                    See `MAPLEAF.Motion.RigidBodies.RigidBody_3DoF.rigidBodyStateDerivative` for an example of derivativeFunc

            Returns (in order):
                An object of type IntegrationResult
                
        '''
        pass

class IntegrationResult():
    __slots__ = [ 'newValue', 'timeStepAdaptationFactor', 'errorMagEstimate', 'dt', 'derivativeEstimate' ]

    def __init__(self, newValue, dt, derivativeEstimate, timeStepAdaptationFactor=1.0, errorMagEstimate=0.0):
        '''
            newValue:                   Value of quantity represented by initVal at time initTime+dt
            dt:                         The size of the time step actually taken (error-limited adaptive integrators can override to shrink the time step)
            derivativeEstimate:         Estimate of the value of the function derivative obtained by the integrator over the last time step
            timeStepAdaptationFactor:   For adaptive methods, suggest time step adaption (otherwise 1)
            errorMagEstimate:           For adaptive methods, provide error estimate (otherwise 0)            
                errorMagEstimate:           For adaptive methods, provide error estimate (otherwise 0)
            errorMagEstimate:           For adaptive methods, provide error estimate (otherwise 0)            
        '''
        self.newValue = newValue
        self.dt = dt
        self.derivativeEstimate = derivativeEstimate
        self.timeStepAdaptationFactor = timeStepAdaptationFactor
        self.errorMagEstimate = errorMagEstimate

class ClassicalIntegrator(Integrator):
    ''' Callable class for constant-dt ODE integration '''

    def __init__(self, method="Euler"):
        # Save integration method and associated Butcher tableau
        self.method = method
        self.tableau = None

        if method == "Euler":
            self.integrate = self._integrateEuler
        elif method == "RK2Midpoint":
            self.integrate = self._integrateByButcherTableau
            self.tableau = [
                [0.5, 0.5],
                [0,   1  ]
            ]
        elif method == "RK2Heun":
            self.integrate = self._integrateByButcherTableau
            self.tableau = [
                [1, 1],
                [0.5, 0.5]
            ]
        elif method == "RK4":
            self.integrate = self._integrateByButcherTableau
            self.tableau = [
                [ 0.5, 0.5 ],
                [ 0.5, 0, 0.5 ],
                [ 1, 0, 0, 1 ],
                [ 1/6, 1/3, 1/3, 1/6 ]
            ]
        elif method == "RK4_3/8":
            self.integrate = self._integrateByButcherTableau
            self.tableau = [
                [ 1/3, 1/3 ],
                [ 2/3, -1/3, 1.0 ],
                [ 1.0, 1.0, -1.0, 1.0 ],
                [ 1/8, 3/8, 3/8, 1/8 ]
            ]
        else:       
            raise ValueError("Integration method: {} not implemented. See SimDefinitionTemplate.txt for options.".format(method))

        # If a butcher tableau is used to define the R-K method, check that it is valid
        if self.tableau != None:
            checkButcherTableau(self.tableau)

    def __call__(self, initVal, initTime, derivativeFunc, dt):
        return self.integrate(initVal, initTime, derivativeFunc, dt)

    #### Constant time step methods ####
    def _integrateEuler(self, initVal, initTime, derivativeFunc, dt):
        yPrime = derivativeFunc(initTime, initVal)
        return IntegrationResult(initVal + yPrime*dt, dt, yPrime)

    def _integrateByButcherTableau(self, initVal, initTime, derivativeFunc, dt):
        '''
            Integrates a function based on a Butcher tableau defined in self.tableau
            Format expected is:
            [
                [ c2, a21 ],
                [ c3, a31, a32 ],
                ...
                [ b1, b2, ... ]

            Then for row i of the tableau, ki = derivativefunc(t + ci*dt, y + dt(ai1*k1 + ai2*k2 + ...))
            Then final result is y + dt*(b1*k1 + b2*k2 + ...)
            Further explanation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#Use
        '''
        tab = self.tableau

        # Initialize array of derivatives (k's) with first k-value from beginning of interval
        k = [ derivativeFunc(initTime, initVal) ]
        # Calculate all other k's - one for each row of the tableau except the last one
        for i in range(len(tab) - 1):
            evalDt = dt*tab[i][0]
            evalTime = initTime + evalDt

            evalK = k[0] / (1/(tab[i][1]*dt)) # Do not start summation with 0, because could be a non-scalar type
            for a in range(2, len(tab[i])):
                evalK = evalK + k[a-1] / (1/(tab[i][a] * dt))
            evalY = initVal + evalK*1

            k.append(derivativeFunc(evalTime, evalY))

        # Calculate final result using last row of coefficients
        # In this summation and the one in the k-calculation loop above, terms are divided by the inverse of the terms they would normally be multiplied by
            # This does not affect the results for scalars
            # But it does prevent rigid body derivative objects from prematurely turning themselves into rigid body state objects (which occurs upon multiplication by a scalar)
        derivative = k[0] / (1/ tab[-1][0]) # Do not start summation with 0, because could be a non-scalar type
        for i in range(1, len(tab[-1])):
            derivative = derivative + k[i]/(1/tab[-1][i])


        return IntegrationResult((initVal + derivative*dt), dt, derivative)

class AdaptiveIntegrator(Integrator):
    ''' Callable class for error-limited adaptive-dt ODE integration '''

    def __init__(self, method="RK45Adaptive", controller="constant", targetError=0.001, maxMinSafetyFactors=[1.5, 0.3, 0.9], PIDCoeffs=[1.0, 0.1, 0.0], maxTimeStep=5, minTimeStep=0.0001, discardedTimeStepCallback=None):
        '''
            RepeatedTimeStepCallback: (function) will be called with (self) when a computed time step is discarded and recompouted (occurs if the estimated error is > targetError * 100)
                                            this callback is currently used to discard log entries associate with discarded time steps
        '''
        # Save integration method and associated
        self.method = method
        self.tableau = None
    
        self.derivativeCache = None # For some adaptive methods, the last derivative of the previous integration step is the same as the first of the following step. This field caches it
        if method == "RK12Adaptive": # Just a test method, already implemented non-butcher tableau version
            self.tableau = [
                [ 0.5, 0.5 ],
                [ 0.0, 1.0 ],
                [ 1.0, 0.0 ]
            ]
            self.firstSameAsLast = False
        elif method == "RK23Adaptive": # Bogacki-Shampine method
            self.tableau = [
                [ 0.5, 0.5 ],
                [ 3/4, 0.0, 3/4 ],
                [ 1.0, 2/9, 1/3, 4/9 ],
                [ 2/9, 1/3, 4/9, 0.0 ],
                [ 7/24, 1/4, 1/3, 1/8 ]
            ]
            self.firstSameAsLast = True
        elif method == "RK45Adaptive": # Dormand-Prince RK5(4)7FM method
            self.tableau = [
                [ 1/5, 1/5 ],
                [ 3/10, 3/40, 9/40 ],
                [ 4/5, 44/45, -56/15, 32/9 ],
                [ 8/9, 19372/6561, -25360/2187, 64448/6561, -212/729 ],
                [ 1.0, 9017/3168, -355/33, 46732/5247, 49/176, -5103/18656 ],
                [ 1.0, 35/384, 0.0, 500/1113, 125/192, -2187/6784, 11/84 ],
                [ 35/384, 0.0, 500/1113, 125/192, -2187/6784, 11/84, 0.0 ], # 5th order
                [ 5179/57600, 0.0, 7571/16695, 393/640, -92097/339200, 187/2100, 1/40 ] # 4th order
            ]
            self.firstSameAsLast = True
        elif method == "RK78Adaptive": # Dormand-Prince RK8(7)13M method (rational approximation)
            self.tableau = [
                [ 1/18, 1/18 ], 
                [ 1/12, 1/48, 1/16 ], 
                [ 1/8, 1/32, 0, 3/32 ], 
                [ 5/16, 5/16, 0, -75/64, 75/64 ], 
                [ 3/8, 3/80, 0, 0, 3/16, 3/20 ], 
                [ 59/400, 29443841/614563906, 0, 0, 77736538/692538347, -28693883/1125000000, 23124283/1800000000 ],
                [ 93/200, 16016141/946692911, 0, 0, 61564180/158732637, 22789713/633445777, 545815736/2771057229, -180193667/1043307555 ],
                [ 5490023248/9719169821, 39632708/573591083, 0, 0, -433636366/683701615, -421739975/2616292301, 100302831/723423059, 790204164/839813087, 800635310/3783071287 ],
                [ 13/20, 246121993/1340847787, 0, 0, -37695042795/15268766246, -309121744/1061227803, -12992083/490766935, 6005943493/2108947869, 393006217/1396673457, 123872331/1001029789 ],
                [ 1201146811/1299019798, -1028468189/846180014, 0, 0, 8478235783/508512852, 1311729495/1432422823, -10304129995/1701304382, -48777925059/3047939560, 15336726248/1032824649, -45442868181/3398467696, 3065993473/597172653 ],
                [ 1, 185892177/718116043, 0, 0, -3185094517/667107341, -477755414/1098053517, -703635378/230739211, 5731566787/1027545527, 5232866602/850066563, -4093664535/808688257, 3962137247/1805957418, 65686358/487910083 ],
                [ 1, 403863854/491063109, 0, 0, -5068492393/434740067, -411421997/543043805, 652783627/914296604, 11173962825/925320556, -13158990841/6184727034, 3936647629/1978049680, -160528059/685178525, 248638103/1413531060, 0 ],
                [ 14005451/335480064, 0, 0, 0, 0, -59238493/1068277825, 181606767/758867731,   561292985/797845732,   -1041891430/1371343529,  760417239/1151165299, 118820643/751138087, -528747749/2220607170,  1/4], # 8th order
                [ 13451932/455176623, 0, 0, 0, 0, -808719846/976000145, 1757004468/5645159321, 656045339/265891186,   -3867574721/1518517206,   465885868/322736535,  53011238/667516719,                  2/45,    0] # 7th order
            ]
            self.firstSameAsLast = False
        else:
            raise ValueError("Integration method: {} not implemented. See SimDefinitionTemplate.txt for options.".format(method))

        checkButcherTableau(self.tableau)

        # Save limiter info and target error, required for any adaptation scheme
        self.maxFactor, self.minFactor, self.safetyFactor = maxMinSafetyFactors
        self.targetError = targetError
        self.maxTimeStep = maxTimeStep
        self.minTimeStep = minTimeStep
        self.discardedTimeStepCallback = discardedTimeStepCallback

        ### Set up the chosen adaptation method ###
        if controller == "elementary":
            # Adjust time step size using an elementary controller
            self.getTimeStepAdjustmentFactor = self._getTimeStepAdjustmentFactor_Elementary

        elif controller == "PID":
            # Adjust time step size using a PID controller
            self.safetyFactor = 1
            
            # Delayed import of GNC.PID to avoid circular import problems
            from MAPLEAF.GNC import PIDController
            self.PIDController = PIDController(*PIDCoeffs, maxIntegral=1)

            self.getTimeStepAdjustmentFactor = self._getTimeStepAdjustmentFactor_PID

        elif controller == "Constant":
            # Do not adjust the time step
            self.getTimeStepAdjustmentFactor = self._getTimeStepAdjustmentFactor_Constant

        else:
            raise ValueError("Adaptive Integrator requires non-constant step size controller such as 'PID' or 'elementary'")

    def __call__(self, initVal, initTime, derivativeFunc, dt):
        # Set up incorrect values to force first iteration, like a do-while loop
        errorMagEstimate = 10000000
        maxErrorMultiple = 20 # Will recompute timestep if estimated error > maxErrorMultiple * targetError
        dt *= 3

        # Loop will lower the actual time step taken if error is more than 20*target
        while errorMagEstimate > maxErrorMultiple*self.targetError:
            if errorMagEstimate != 10000000:
                # This means this is not the first loop. Therefore a previously-computed time step has been discarded, and will be recomputed below with a smaller time step
                if self.discardedTimeStepCallback != None:
                    self.discardedTimeStepCallback(self)

            dt = max(self.minTimeStep, dt/3)
            result, derivative, errorMagEstimate, lastDerivativeEvaluation = self._integrate(initVal, initTime, derivativeFunc, dt, self.derivativeCache)
            if math.isclose(dt,self.minTimeStep):
                break
        
        if self.firstSameAsLast:
            self.derivativeCache = lastDerivativeEvaluation

        # Compute adaptation factor
        desiredAdaptFactor = self.getTimeStepAdjustmentFactor(errorMagEstimate, dt)
        limitedAdaptFactor = self._limitAdaptationFactor(desiredAdaptFactor, dt)

        return IntegrationResult(result, dt, derivative, limitedAdaptFactor, errorMagEstimate)
   
    #### Time Step adjustment ####
    def _limitAdaptationFactor(self, desiredAdaptFactor, dt):
        ''' 
            Apply adaptation limiters 
            Adaptation can be limited in two ways: by self.maxFactor/self.minFactor and by self.minTimeStep/self.maxTimeStep.
            The below checks which limitation is currently most restrictive and applies that one
        '''
        # Calculate min/max adaptation factors based on min/max time step size restrictions
        minFactor2 = self.minTimeStep / dt
        maxFactor2 = self.maxTimeStep / dt

        # Calculate resulting total min/max factors
        minFactor = max(minFactor2, self.minFactor)
        maxFactor = min(maxFactor2, self.maxFactor)

        # Apply limits
        return self.safetyFactor * min(max(desiredAdaptFactor, minFactor), maxFactor)

    def _getTimeStepAdjustmentFactor_Constant(self, errorMag, dt):
        ''' Don't adjust the time step (always 1.0) '''
        return 1

    def _getTimeStepAdjustmentFactor_Elementary(self, errorMag, dt):
        ''' Calculates the time step adjustment factor when using an elementary controller '''
        return (self.targetError / (2*errorMag))**0.5

    def _getTimeStepAdjustmentFactor_PID(self, errorMag, dt):
        ''' Calculates the time step adjustment factor when using a PID controller '''
        errorInErrorMagnitude = (errorMag - self.targetError) / self.targetError
        # Minimum value of the equation above is -1, correspondingly, we limit positive values to 1
        errorInErrorMagnitude = min(1, errorInErrorMagnitude)
        return 1 + self.PIDController.getNewSetPoint(errorInErrorMagnitude, dt)

    #### Adaptive Integration Method ####
    def _integrate(self, initVal, initTime, derivativeFunc, dt, firstSameAsLast=None):
        '''
            Integrates a function based on a Butcher tableau defined in self.tableau
            Format expected is:
            [
                [ c2, a21 ],
                [ c3, a31, a32 ],
                ...
                [ b1, b2, ... ]
                [ b1*, b2*, ... ]

            Then for row i of the tableau, ki = derivativefunc(t + ci*dt, y + dt(ai1*k1 + ai2*k2 + ...))
            Then final result is y + dt*(b1*k1 + b2*k2 + ...)
            For these adaptive methods, two rows of b's (b's and b*'s) produce two estimates of the solution
            Subtracting these gives an error estimate which can be used to adjust the time step size
            Also see comment at the top of this file
            Further explanation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#Use
        '''
        tab = self.tableau

        # Initialize array of derivatives (k's) with first k-value from beginning of interval
        if firstSameAsLast != None:
            # Use first same as last property if possible to save a function evaluation
            k = [ firstSameAsLast ]
        else:
            k = [ derivativeFunc(initTime, initVal) ]

        # Calculate all other k's - one for each row of the tableau except the last two
        for i in range(len(tab) - 2):
            evalDt = dt*tab[i][0]
            evalTime = initTime + evalDt
            
            evalK = k[0] * tab[i][1] # Do not start summation with 0, because could be a non-scalar type
            for a in range(2, len(tab[i])):
                evalK += k[a-1] * tab[i][a]
            evalY = initVal + evalK*dt

            k.append(derivativeFunc(evalTime, evalY))

        lastDerivativeEvaluation = k[-1]

        # Calculate final high/low accuracy results
        # In this summation and the one in the k-calculation loop above, terms are divided by the inverse of the terms they would normally be multiplied by
            # This does not affect the results for scalars
            # But it does prevent rigid body derivative objects from prematurely turning themselves into rigid body state objects (which occurs upon multiplication by a scalar)        
        fineDerivative = k[0] / (1/tab[-2][0]) # Do not start summation with 0, because derivative could be a non-scalar type
        coarseDerivative =  k[0] / (1/tab[-1][0]) # Do not start summation with 0, because derivative could be a non-scalar type
        for i in range(1, len(tab[-1])):
            fineDerivative += k[i] / (1/tab[-2][i])
            coarseDerivative += k[i] / (1/tab[-1][i])

        # Compute error estimate
        finePred = initVal + fineDerivative*dt
        coarsePred = initVal + coarseDerivative*dt
        errorEstimate = (-coarsePred + finePred)
        errorMagEstimate = abs(errorEstimate)

        return finePred, fineDerivative, errorMagEstimate, lastDerivativeEvaluation

Functions

def integratorFactory(integrationMethod='Euler', simDefinition=None, discardedTimeStepCallback=None)

Returns a callable integrator object

Inputs

  • integrationMethod: (str) Name of integration method: Examples = "Euler", "RK4", "RK23Adaptive", and "RK45Adaptive"
  • simDefinition: (SimDefinition) for adaptive integration, provide a simdefinition file with time step adaptation parameters
  • discardedTimeStepCallback: (1-argument function reference) for adaptive integration, this function (if provided) is called when a time step is computed, but then discarded and re-computed with a smaller timestep to remain below the max estimated error threshold. Used by MAPLEAF to remove force calculation logs from those discarded time steps
Expand source code
def integratorFactory(integrationMethod="Euler", simDefinition=None, discardedTimeStepCallback=None):
    ''' 
        Returns a callable integrator object

        Inputs:
            * integrationMethod: (str) Name of integration method: Examples = "Euler", "RK4", "RK23Adaptive", and "RK45Adaptive"
            * simDefinition: (`MAPLEAF.IO.SimDefinition`) for adaptive integration, provide a simdefinition file with time step adaptation parameters
            * discardedTimeStepCallback: (1-argument function reference) for adaptive integration, this function (if provided) is called when a time step is computed,
                but then discarded and re-computed with a smaller timestep to remain below the max estimated error threshold. Used by MAPLEAF to remove
                force calculation logs from those discarded time steps
    '''
    if "Adapt" in integrationMethod:
        if simDefinition == None:
            raise ValueError("SimDefinition object required to initialize adaptive integrator")

        from MAPLEAF.IO import SubDictReader
        adaptDictReader = SubDictReader("SimControl.TimeStepAdaptation", simDefinition)

        # Adaptive Integration
        controller = adaptDictReader.getString("controller")
        if controller == "PID":
            PIDCoeffs = [ float(x) for x in adaptDictReader.getString("PID.coefficients").split() ]
            safetyFactor = None
        elif controller == "elementary":
            safetyFactor = adaptDictReader.getFloat("Elementary.safetyFactor")
            PIDCoeffs = None

        targetError = adaptDictReader.getFloat("targetError")
        minFactor = adaptDictReader.getFloat("minFactor")
        maxFactor = adaptDictReader.getFloat("maxFactor")
        maxTimeStep = adaptDictReader.getFloat("maxTimeStep")
        minTimeStep = adaptDictReader.getFloat("minTimeStep")
        
        return AdaptiveIntegrator(
            method=integrationMethod, 
            controller=controller, 
            targetError=targetError, 
            maxMinSafetyFactors=[maxFactor, minFactor, safetyFactor], 
            PIDCoeffs=PIDCoeffs, 
            maxTimeStep=maxTimeStep, 
            minTimeStep=minTimeStep, 
            discardedTimeStepCallback=discardedTimeStepCallback
        )
    
    else:
        # Constant time step integrator
        return ClassicalIntegrator(method=integrationMethod)