Module MAPLEAF.GNC
Guidance, Navigation, and Control functionality for 3D navigation (air vehicles).
Main module is MAPLEAF.GNC.ControlSystems
![]()
Expand source code
'''
Guidance, Navigation, and Control functionality for 3D navigation (air vehicles).
Main module is `MAPLEAF.GNC.ControlSystems`
.. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Homing_pigeon.jpg/1272px-Homing_pigeon.jpg
'''
# Make the classes in all submodules importable directly from MAPLEAF.Rocket
from .Actuators import *
from .PID import *
from .Navigation import *
from .actuatedSystem import *
from .MomentControllers import *
from .ControlSystems import *
subModules = [ actuatedSystem, Actuators, ControlSystems, MomentControllers, Navigation, PID ]
__all__ = []
for subModule in subModules:
__all__ += subModule.__all__
Sub-modules
MAPLEAF.GNC.Actuators-
Modelling of actuators, which can control things like the position of rocket fin angles…
MAPLEAF.GNC.ControlSystems-
Classes that implement air vehicle control systems by tying together a
Navigator, a … MAPLEAF.GNC.MomentControllers-
Moment controllers determine what overall moments should be applied to the rocket to remain on the course provided by the …
MAPLEAF.GNC.Navigation-
Navigators determine where to go
MAPLEAF.GNC.PID-
PID controllers control parts of the control system and adaptive simulation timestepping
MAPLEAF.GNC.actuatedSystem-
The actuated system class initializes actuators, which are controlled by a
ControlSystemRocket component classes need …
Classes
class ActuatedSystem (nActuators)-
Mixin-type class
Inherit from this class to add its functionality to a class- ActuatedSystem.__init__ initializes actuators defined in the component's 'Actuator' subdictionary
- These actuators are then stored in self.actuatorList
- Child classes must call ActuatedSystem.__init__() to actually instantiate the actuators
- Expects that child classes also inherit from SubDictReader, and have already initialized the SubDictReader functionality
- Expects that child classes have a self.rocket attribute
Expand source code
class ActuatedSystem(abc.ABC): ''' **Mixin-type class** Inherit from this class to add its functionality to a class * ActuatedSystem.\_\_init\_\_ initializes actuators defined in the component's 'Actuator' subdictionary * These actuators are then stored in self.actuatorList * Child classes must call ActuatedSystem.\_\_init\_\_() to actually instantiate the actuators * Expects that child classes also inherit from SubDictReader, and have already initialized the SubDictReader functionality * Expects that child classes have a self.rocket attribute ''' def __init__(self, nActuators): # Initialize Actuator Models actuatorList = [] for i in range(nActuators): actuatorResponseModel = self.componentDictReader.getString("Actuators.responseModel") if actuatorResponseModel == "FirstOrder": responseTime = self.componentDictReader.getFloat("Actuators.responseTime") maxDeflection = self.componentDictReader.tryGetFloat("Actuators.maxDeflection", defaultValue=None) minDeflection = self.componentDictReader.tryGetFloat("Actuators.minDeflection", defaultValue=None) actuator = FirstOrderActuator(responseTime, maxDeflection=maxDeflection, minDeflection=minDeflection) actuatorList.append(actuator) else: raise ValueError("Actuator response model {} not implemented. Try 'FirstOrder'".format(actuatorResponseModel)) self.actuatorList = actuatorList # Initialize Actuator controller controllerType = self.componentDictReader.getString("Actuators.controller") if controllerType == "TableInterpolating": deflectionTablePath = self.componentDictReader.getString("Actuators.deflectionTablePath") # Get list of 'key' columns deflectionKeyColumns = self.componentDictReader.getString("Actuators.deflectionKeyColumns").split() noDesiredKeys = [] for key in deflectionKeyColumns: if "Desired" not in key: noDesiredKeys.append(key) else: break # Check that no non-'desired' key columns after desired ones for i in range(len(noDesiredKeys), len(deflectionKeyColumns)): if "Desired" not in deflectionKeyColumns[i]: raise ValueError("'Desired' columns such as DesiredMx must come last in deflectionKeyColumns") deflectionKeyFunctionVector = [ AeroParameters.stringToAeroFunctionMap[key] for key in noDesiredKeys ] self.actuatorController = TableInterpolatingActuatorController(deflectionTablePath, len(deflectionKeyColumns), deflectionKeyFunctionVector, actuatorList) else: raise ValueError("Actuator controller: {} not implemented. Try TableInterpolating.") @abc.abstractmethod def initializeActuators(self, controlSystem): ''' Concrete implementations of this function should call ActuatedSystem.\_\_init\_\_(self, nActuators) - implementations of this function only need to determine and pass in nActuators ''' passAncestors
- abc.ABC
Subclasses
Methods
def initializeActuators(self, controlSystem)-
Concrete implementations of this function should call ActuatedSystem.__init__(self, nActuators) - implementations of this function only need to determine and pass in nActuators
Expand source code
@abc.abstractmethod def initializeActuators(self, controlSystem): ''' Concrete implementations of this function should call ActuatedSystem.\_\_init\_\_(self, nActuators) - implementations of this function only need to determine and pass in nActuators ''' pass
class Actuator-
Interface for actuators. Actuators model the movement of a physical actuator (ex. servo, hydraulics). Target deflections are usually provided by the rocket's
ControlSystem, usually obtained from aActuatorControllerExpand source code
class Actuator(abc.ABC): ''' Interface for actuators. Actuators model the movement of a physical actuator (ex. servo, hydraulics). Target deflections are usually provided by the rocket's `MAPLEAF.GNC.ControlSystems.ControlSystem`, usually obtained from a `ActuatorController` ''' @abc.abstractmethod def setTargetDeflection(self, newTarget, time): ''' Should return nothing, update setPoint for the actuator ''' pass @abc.abstractmethod def getDeflection(self, time): ''' Should return the current deflection ''' pass @abc.abstractmethod def getDeflectionDerivative(self, state, environment, time): ''' Should return the current deflection derivative ''' passAncestors
- abc.ABC
Subclasses
Methods
def getDeflection(self, time)-
Should return the current deflection
Expand source code
@abc.abstractmethod def getDeflection(self, time): ''' Should return the current deflection ''' pass def getDeflectionDerivative(self, state, environment, time)-
Should return the current deflection derivative
Expand source code
@abc.abstractmethod def getDeflectionDerivative(self, state, environment, time): ''' Should return the current deflection derivative ''' pass def setTargetDeflection(self, newTarget, time)-
Should return nothing, update setPoint for the actuator
Expand source code
@abc.abstractmethod def setTargetDeflection(self, newTarget, time): ''' Should return nothing, update setPoint for the actuator ''' pass
class ActuatorController-
Interface for actuator controllers.
Actuator controllers are in charge of determining what actuator deflections are required to produce moments desired by the rocket'sControlSystemExpand source code
class ActuatorController(abc.ABC): ''' Interface for actuator controllers. Actuator controllers are in charge of determining what actuator deflections are required to produce moments desired by the rocket's `MAPLEAF.GNC.ControlSystems.ControlSystem` ''' @abc.abstractmethod def setTargetActuatorDeflections(self, desiredMoments, state, environment, time): ''' Should return nothing, control the appropriate rocket system to generate the desired moments ''' passAncestors
- abc.ABC
Subclasses
Methods
def setTargetActuatorDeflections(self, desiredMoments, state, environment, time)-
Should return nothing, control the appropriate rocket system to generate the desired moments
Expand source code
@abc.abstractmethod def setTargetActuatorDeflections(self, desiredMoments, state, environment, time): ''' Should return nothing, control the appropriate rocket system to generate the desired moments ''' pass
class ConstantGainPIDController (P=0, I=0, D=0, initialError=0, maxIntegral=None)-
Inputs
P: (int) Proportional Gain I: (int) Integral Gain D: (int) Derivative Gain DCol: (int) zero-indexed column number of D Coefficient
Note: It is assumed that PCol, ICol, and DCol exist one after another in the table
Inputs passed through to parent class (PICController): initialError, maxIntegral
Expand source code
class ConstantGainPIDController(PIDController): def __init__(self, P=0, I=0, D=0, initialError=0, maxIntegral=None): ''' Inputs: P: (int) Proportional Gain I: (int) Integral Gain D: (int) Derivative Gain DCol: (int) zero-indexed column number of D Coefficient Note: It is assumed that PCol, ICol, and DCol exist one after another in the table Inputs passed through to parent class (PICController): initialError, maxIntegral ''' PIDController.__init__(self, P,I,D, initialError=initialError, maxIntegral=maxIntegral)Ancestors
Subclasses
class ConstantGainPIDRocketMomentController (Pxy, Ixy, Dxy, Pz, Iz, Dz)-
Helper class that provides a standard way to create an ABC using inheritance.
Constant PID coefficient controller
Expand source code
class ConstantGainPIDRocketMomentController(MomentController, ConstantGainPIDController): def __init__(self, Pxy, Ixy, Dxy, Pz, Iz, Dz): ''' Constant PID coefficient controller ''' ConstantGainPIDController.__init__(self) P = np.array([Pxy, Pxy, Pz]) I = np.array([Ixy, Ixy, Iz]) D = np.array([Dxy, Dxy, Dz]) self.updateCoefficients(P,I,D) def _getOrientationError(self, rocketState, targetOrientation): return np.array((targetOrientation / rocketState.orientation).toRotationVector()) def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system ''' orientationError = self._getOrientationError(rocketState, targetOrientation) return self.getNewSetPoint(orientationError, dt)Ancestors
Methods
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt)-
Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system
Expand source code
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system ''' orientationError = self._getOrientationError(rocketState, targetOrientation) return self.getNewSetPoint(orientationError, dt)
class ControlSystem-
Interface for control systems
Expand source code
class ControlSystem(abc.ABC): ''' Interface for control systems ''' @abc.abstractmethod def runControlLoopIfRequired(self, currentTime, rocketState, environment): ''' Should check if the control loop needs to be run and run it if so. Should return a list of actuator deflections (for logging) Reference to controlled system expected to be self.controlledSystem Controlled system expected to inherit from ActuatedSystem If the control system ran: Should return an iterable of new actuator position targets (in order consistent with log headers) If the control system didn't run: Should return False ''' def calculateAngularError(self, currentOrientation, targetOrientation): errors = np.array((targetOrientation / currentOrientation).toRotationVector()) pitchError = errors[0] yawError = errors[1] rollError = errors[2] return pitchError, yawError, rollErrorAncestors
- abc.ABC
Subclasses
Methods
def calculateAngularError(self, currentOrientation, targetOrientation)-
Expand source code
def calculateAngularError(self, currentOrientation, targetOrientation): errors = np.array((targetOrientation / currentOrientation).toRotationVector()) pitchError = errors[0] yawError = errors[1] rollError = errors[2] return pitchError, yawError, rollError def runControlLoopIfRequired(self, currentTime, rocketState, environment)-
Should check if the control loop needs to be run and run it if so. Should return a list of actuator deflections (for logging)
Reference to controlled system expected to be self.controlledSystem Controlled system expected to inherit from ActuatedSystem
If the control system ran: Should return an iterable of new actuator position targets (in order consistent with log headers) If the control system didn't run: Should return False
Expand source code
@abc.abstractmethod def runControlLoopIfRequired(self, currentTime, rocketState, environment): ''' Should check if the control loop needs to be run and run it if so. Should return a list of actuator deflections (for logging) Reference to controlled system expected to be self.controlledSystem Controlled system expected to inherit from ActuatedSystem If the control system ran: Should return an iterable of new actuator position targets (in order consistent with log headers) If the control system didn't run: Should return False '''
class FirstOrderActuator (responseTime, initialDeflection=0, initialTime=0, maxDeflection=None, minDeflection=None)-
First-order system - simple enought that its motion between control loops can be determined analytically. Motion does not need to be integrated
Basically just a version of the FirstOrderSystem class that internally stores state information
Expand source code
class FirstOrderActuator(Actuator, FirstOrderSystem): ''' First-order system - simple enought that its motion between control loops can be determined analytically. Motion does not need to be integrated Basically just a version of the FirstOrderSystem class that internally stores state information ''' def __init__(self, responseTime, initialDeflection=0, initialTime=0, maxDeflection=None, minDeflection=None): self.responseTime = responseTime self.lastDeflection = initialDeflection self.lastTime = 0 self.targetDeflection = 0 self.maxDeflection = maxDeflection self.minDeflection = minDeflection def setTargetDeflection(self, newTarget, time): # Record current deflection and time self.lastDeflection = self.getDeflection(time) self.lastTime = time # Apply limiters to new target if self.maxDeflection != None: newTarget = min(newTarget, self.maxDeflection) if self.minDeflection != None: newTarget = max(newTarget, self.minDeflection) # Set new target self.targetDeflection = newTarget def getDeflection(self, time): return self.getPosition(time, self.lastTime, self.lastDeflection, self.targetDeflection) def getDeflectionDerivative(self, _, _2, _3): ''' Not required for first-order actuator - deflections can be integrated analytically ''' passAncestors
- Actuator
- abc.ABC
- FirstOrderSystem
Methods
def getDeflectionDerivative(self, _, _2, _3)-
Not required for first-order actuator - deflections can be integrated analytically
Expand source code
def getDeflectionDerivative(self, _, _2, _3): ''' Not required for first-order actuator - deflections can be integrated analytically ''' pass
Inherited members
class FirstOrderSystem (responseTime)-
Expand source code
class FirstOrderSystem(): def __init__(self, responseTime): self.responseTime = responseTime def getPosition(self, currentTime, lastTime, lastPosition, lastTarget): dt = currentTime - lastTime if dt < -1e-17: # Exception check here because this would pass through the exponent expression below without causing an error raise ValueError("Current time ({}) must be after lastTime ({})".format(currentTime, lastTime)) if dt < 0: dt = 0 lastError = lastTarget - lastPosition errorFractionRemoved = (1 - e**(-dt/self.responseTime)) newPosition = lastPosition + lastError*errorFractionRemoved return newPositionSubclasses
Methods
def getPosition(self, currentTime, lastTime, lastPosition, lastTarget)-
Expand source code
def getPosition(self, currentTime, lastTime, lastPosition, lastTarget): dt = currentTime - lastTime if dt < -1e-17: # Exception check here because this would pass through the exponent expression below without causing an error raise ValueError("Current time ({}) must be after lastTime ({})".format(currentTime, lastTime)) if dt < 0: dt = 0 lastError = lastTarget - lastPosition errorFractionRemoved = (1 - e**(-dt/self.responseTime)) newPosition = lastPosition + lastError*errorFractionRemoved return newPosition
class IdealMomentController (rocket)-
Expand source code
class IdealMomentController(): def __init__(self, rocket): self.rocket = rocket def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): """ Exactly match the desired target orientation and set the rockets angular velocity to zero """ self.rocket.rigidBody.state.orientation = targetOrientation self.rocket.rigidBody.state.angularVelocity = AngularVelocity(rotationVector=Vector(0,0,0))Methods
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt)-
Exactly match the desired target orientation and set the rockets angular velocity to zero
Expand source code
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): """ Exactly match the desired target orientation and set the rockets angular velocity to zero """ self.rocket.rigidBody.state.orientation = targetOrientation self.rocket.rigidBody.state.angularVelocity = AngularVelocity(rotationVector=Vector(0,0,0))
class MomentController-
Helper class that provides a standard way to create an ABC using inheritance.
Expand source code
class MomentController(abc.ABC): @abc.abstractmethod def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Should return a list [ desired x-axis, y-axis, and z-axis ] moments '''Ancestors
- abc.ABC
Subclasses
Methods
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt)-
Should return a list [ desired x-axis, y-axis, and z-axis ] moments
Expand source code
@abc.abstractmethod def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Should return a list [ desired x-axis, y-axis, and z-axis ] moments '''
-
Helper class that provides a standard way to create an ABC using inheritance.
Expand source code
class Navigator(abc.ABC): @abc.abstractmethod def getTargetOrientation(self, rocketState, targetState, time): ''' Should return a target orientation quaternion ''' passAncestors
- abc.ABC
Subclasses
Methods
-
Should return a target orientation quaternion
Expand source code
@abc.abstractmethod def getTargetOrientation(self, rocketState, targetState, time): ''' Should return a target orientation quaternion ''' pass
class PIDController (P, I, D, initialError=0, maxIntegral=None)-
To make this PID controller work for vectors of values, pass in np arrays - they are added elementwise by default
Expand source code
class PIDController(): def __init__(self, P, I, D, initialError=0, maxIntegral=None): ''' To make this PID controller work for vectors of values, pass in np arrays - they are added elementwise by default ''' self.P = P self.I = I self.D = D self.lastError = initialError self.errorIntegral = initialError * 0 # Initialized like this in case value passed in is an array self.updateMaxIntegral(maxIntegral) def updateMaxIntegral(self, maxIntegral): if maxIntegral is not None: self.maxIntegralMagnitude = abs(maxIntegral) else: self.maxIntegralMagnitude = maxIntegral def getNewSetPoint(self, currentError, dt): # Calculate derivative derivative = (currentError - self.lastError) / dt # Calculate integral term self.errorIntegral = self.errorIntegral + (currentError + self.lastError)*dt / 2 # Cap the magnitude of the integral term if necessary if self.maxIntegralMagnitude is not None: try: # Check if vector valued testIter = iter(self.errorIntegral) for i in range(len(self.errorIntegral)): if abs(self.errorIntegral[i]) > self.maxIntegralMagnitude[i]: self.errorIntegral[i] = self.errorIntegral[i] * self.maxIntegralMagnitude[i] / abs(self.errorIntegral[i]) except TypeError: # Scalar valued if abs(self.errorIntegral) > self.maxIntegralMagnitude: self.errorIntegral = self.errorIntegral * self.maxIntegralMagnitude / abs(self.errorIntegral) # Store error for next calculation self.lastError = currentError return self.P*currentError + self.I*self.errorIntegral + self.D*derivative def updateCoefficients(self, P, I, D, maxIntegral=None): self.P = P self.I = I self.D = D self.updateMaxIntegral(maxIntegral) def resetIntegral(self): self.errorIntegral = self.lastError * 0 # Done to handle arbitrary size np arraysSubclasses
Methods
def getNewSetPoint(self, currentError, dt)-
Expand source code
def getNewSetPoint(self, currentError, dt): # Calculate derivative derivative = (currentError - self.lastError) / dt # Calculate integral term self.errorIntegral = self.errorIntegral + (currentError + self.lastError)*dt / 2 # Cap the magnitude of the integral term if necessary if self.maxIntegralMagnitude is not None: try: # Check if vector valued testIter = iter(self.errorIntegral) for i in range(len(self.errorIntegral)): if abs(self.errorIntegral[i]) > self.maxIntegralMagnitude[i]: self.errorIntegral[i] = self.errorIntegral[i] * self.maxIntegralMagnitude[i] / abs(self.errorIntegral[i]) except TypeError: # Scalar valued if abs(self.errorIntegral) > self.maxIntegralMagnitude: self.errorIntegral = self.errorIntegral * self.maxIntegralMagnitude / abs(self.errorIntegral) # Store error for next calculation self.lastError = currentError return self.P*currentError + self.I*self.errorIntegral + self.D*derivative def resetIntegral(self)-
Expand source code
def resetIntegral(self): self.errorIntegral = self.lastError * 0 # Done to handle arbitrary size np arrays def updateCoefficients(self, P, I, D, maxIntegral=None)-
Expand source code
def updateCoefficients(self, P, I, D, maxIntegral=None): self.P = P self.I = I self.D = D self.updateMaxIntegral(maxIntegral) def updateMaxIntegral(self, maxIntegral)-
Expand source code
def updateMaxIntegral(self, maxIntegral): if maxIntegral is not None: self.maxIntegralMagnitude = abs(maxIntegral) else: self.maxIntegralMagnitude = maxIntegral
class RocketControlSystem (controlSystemDictReader, rocket, initTime=0, log=False, silent=False)-
Simplest possible control system for a rocket
Expand source code
class RocketControlSystem(ControlSystem, SubDictReader): ''' Simplest possible control system for a rocket ''' def __init__(self, controlSystemDictReader, rocket, initTime=0, log=False, silent=False): self.rocket = rocket self.controlSystemDictReader = controlSystemDictReader self.lastControlLoopRunTime = initTime self.timeSteppingModified = False # set to True if the sim's time stepping has been constrained by the control system update rate (in self._checkSimTimeStepping) self.silent = silent ### Create Navigator ### desiredFlightDirection = controlSystemDictReader.getVector("desiredFlightDirection") self.navigator = Stabilizer(desiredFlightDirection) ### Create Moment Controller ### momentControllerType = controlSystemDictReader.getString("MomentController.Type") if momentControllerType == "ConstantGainPIDRocket": Pxy = controlSystemDictReader.getFloat("MomentController.Pxy") Ixy = controlSystemDictReader.getFloat("MomentController.Ixy") Dxy = controlSystemDictReader.getFloat("MomentController.Dxy") Pz = controlSystemDictReader.getFloat("MomentController.Pz") Iz = controlSystemDictReader.getFloat("MomentController.Iz") Dz = controlSystemDictReader.getFloat("MomentController.Dz") self.momentController = ConstantGainPIDRocketMomentController(Pxy,Ixy,Dxy,Pz,Iz,Dz) elif momentControllerType == "ScheduledGainPIDRocket": gainTableFilePath = controlSystemDictReader.getString("MomentController.gainTableFilePath") keyColumnNames = controlSystemDictReader.getString("MomentController.scheduledBy").split() self.momentController = ScheduledGainPIDRocketMomentController(gainTableFilePath, keyColumnNames) elif momentControllerType == "IdealMomentController": self.momentController = IdealMomentController(self.rocket) else: raise ValueError("Moment Controller Type: {} not implemented. Try ScheduledGainPIDRocket or IdealMomentController".format(momentControllerType)) ### Set update rate ### if momentControllerType == "IdealMomentController": # When using the ideal controller, update the rocket's orientation as often as possible self.updateRate = 0.0 else: # Otherwise model a real control system self.updateRate = controlSystemDictReader.getFloat("updateRate") self.controlledSystem = None if momentControllerType != "IdealMomentController": ### Get reference to the controlled system ### controlledSystemPath = controlSystemDictReader.getString("controlledSystem") for stage in rocket.stages: for rocketComponent in stage.components: if rocketComponent.componentDictReader.simDefDictPathToReadFrom == controlledSystemPath: self.controlledSystem = rocketComponent break if self.controlledSystem == None: simDefinitionFile = controlSystemDictReader.simDefinition.fileName raise ValueError("Rocket Component: {} not found in {}".format(controlledSystemPath, simDefinitionFile)) self.controlledSystem.initializeActuators(self) self._checkSimTimeStepping() ### Set up logging if requested ### if log: self.log = Log() self.log.addColumns(["PitchError", "YawError", "RollError"]) if self.controlledSystem != None: nActuators = len(self.controlledSystem.actuatorList) targetDeflectionColumnNames = [ "Actuator_{}_TargetDeflection".format(i) for i in range(nActuators) ] self.log.addColumns(targetDeflectionColumnNames) deflectionColumnNames = [ "Actuator_{}_Deflection".format(i) for i in range(nActuators) ] self.log.addColumns(deflectionColumnNames) else: self.log = None def _checkSimTimeStepping(self): # If the update rate is zero, control system is simply run once per time step if self.updateRate > 0: # Since a discrete update rate has been specified, we need to ensure that the simulation time step is an integer divisor of the control system time step # Disable adaptive time stepping during the ascent portion of the flight (if it's enabled) timeDiscretization = self.controlSystemDictReader.getString("SimControl.timeDiscretization") if "Adaptive" in timeDiscretization: if not self.silent: print("WARNING: Time stepping conflict between adaptive-time-stepping Runge-Kutta method and fixed control system update rate") print("Disabling adaptive time stepping for ascent portion of flight. Will re-enable if/when recovery system deploys.") print("Switching to RK4 time stepping") self.rocket.rigidBody.integrate = integratorFactory(integrationMethod='RK4') self.timeSteppingModified = True # Adjust the sim time step to be an even divisor of the control system time step controlTimeStep = 1/self.updateRate self.controlTimeStep = controlTimeStep originalSimTimeStep = self.controlSystemDictReader.getFloat("SimControl.timeStep") self.originalSimTimeStep = originalSimTimeStep if controlTimeStep / originalSimTimeStep != round(controlTimeStep / originalSimTimeStep): recommendedSimTimeStep = controlTimeStep / max(1, round(controlTimeStep / originalSimTimeStep)) if not self.silent: print("WARNING: Selected time step ({}) does not divide the control system time step ({}) Into an integer number of time steps.".format(originalSimTimeStep, controlTimeStep)) print("Adjusting time step to: {}".format(recommendedSimTimeStep)) print("") # This relies on the SimRunner reading the time step size from the sim definition after intializing the rocket # To make this more robust, have the rocket perform a timestep size check every single time step self.controlSystemDictReader.simDefinition.setValue("SimControl.timeStep", str(recommendedSimTimeStep)) self.timeSteppingModified = True else: self.controlTimeStep = 0 def restoreOriginalTimeStepping(self): ''' Should get called whenever the control system is disabled: 1. If a stage containing the controlled system is dropped 2. If the rocket recovery system is deployed ''' if self.timeSteppingModified: # This currently won't do anything because the time step is saved in an array by the Simulation - might be useful in the future self.simDefinition.setValue("SimControl.timeStep", str(self.originalSimTimeStep)) # This will re-initialize the integrator to match that originally selected in the sim definition file originalTimeIntegrationMethod = self.rocketDictReader.getValue("SimControl.timeDiscretization") print("Restoring original time discretization method ({})".format(originalTimeIntegrationMethod)) self.rocket.rigidBody.integrate = integratorFactory(integrationMethod=originalTimeIntegrationMethod, simDefinition=self.controlDctReaders.simDefinition) def runControlLoopIfRequired(self, currentTime, rocketState, environment): # Run control loop if enough time has passed if (currentTime - self.lastControlLoopRunTime) + 0.0000000001 >= self.controlTimeStep: dt = currentTime - self.lastControlLoopRunTime # Figure out where to go/point targetOrientation = self.navigator.getTargetOrientation(rocketState, "notRequired", currentTime) # Figure out what moment needs to be applied desiredMoments = self.momentController.getDesiredMoments(rocketState, environment, targetOrientation, currentTime, dt) if self.controlledSystem != None: # Apply it by actuating the controlled system newActuatorPositionTargets = self.controlledSystem.actuatorController.setTargetActuatorDeflections(desiredMoments, rocketState, environment, currentTime) else: # End up here if using the ideal moment controller, no actuators to control # Desired moments will be applied instantly without modeling the dynamics of the system that applies them newActuatorPositionTargets = [0] self.lastControlLoopRunTime = currentTime pitchError, yawError, rollError = self.calculateAngularError(rocketState.orientation,targetOrientation) if self.log is not None: self.log.newLogRow(currentTime) self.log.logValue("PitchError", pitchError) self.log.logValue("YawError", yawError) self.log.logValue("RollError", rollError) if self.controlledSystem != None: for i in range(len(newActuatorPositionTargets)): targetDeflectionColumnName = "Actuator_{}_TargetDeflection".format(i) self.log.logValue(targetDeflectionColumnName, newActuatorPositionTargets[i]) deflectionColumnName = "Actuator_{}_Deflection".format(i) currentDeflection = self.controlledSystem.actuatorController.actuatorList[i].getDeflection(currentTime) self.log.logValue(deflectionColumnName, currentDeflection) return newActuatorPositionTargets else: return False def writeLogsToFile(self, directory="."): controlSystemPath = self.controlSystemDictReader.simDefDictPathToReadFrom.replace(".", "_") path = os.path.join(directory, "{}_Log.csv".format(controlSystemPath)) self.log.writeToCSV(path) return pathAncestors
- ControlSystem
- abc.ABC
- SubDictReader
Methods
def restoreOriginalTimeStepping(self)-
Should get called whenever the control system is disabled: 1. If a stage containing the controlled system is dropped 2. If the rocket recovery system is deployed
Expand source code
def restoreOriginalTimeStepping(self): ''' Should get called whenever the control system is disabled: 1. If a stage containing the controlled system is dropped 2. If the rocket recovery system is deployed ''' if self.timeSteppingModified: # This currently won't do anything because the time step is saved in an array by the Simulation - might be useful in the future self.simDefinition.setValue("SimControl.timeStep", str(self.originalSimTimeStep)) # This will re-initialize the integrator to match that originally selected in the sim definition file originalTimeIntegrationMethod = self.rocketDictReader.getValue("SimControl.timeDiscretization") print("Restoring original time discretization method ({})".format(originalTimeIntegrationMethod)) self.rocket.rigidBody.integrate = integratorFactory(integrationMethod=originalTimeIntegrationMethod, simDefinition=self.controlDctReaders.simDefinition) def writeLogsToFile(self, directory='.')-
Expand source code
def writeLogsToFile(self, directory="."): controlSystemPath = self.controlSystemDictReader.simDefDictPathToReadFrom.replace(".", "_") path = os.path.join(directory, "{}_Log.csv".format(controlSystemPath)) self.log.writeToCSV(path) return path
Inherited members
class ScheduledGainPIDController (gainTableFilePath, nKeyColumns=2, PCol=3, DCol=5, initialError=0, maxIntegral=None)-
Inputs
gainTableFilePath: (string) Path to gain table text file ex: './MAPLEAF/Examples/TabulatedData/constPIDCoeffs.txt' nKeyColumns: (int) Number of 'key' columns (independent variables). Key columns are assumed to be the nKeyColumns leftmost ones PCol: (int) zero-indexed column number of P Coefficient DCol: (int) zero-indexed column number of D Coefficient
Note: It is assumed that PCol, ICol, and DCol exist one after another in the table
Inputs passed through to parent class (PICController): initialError, maxIntegral
Expand source code
class ScheduledGainPIDController(PIDController): def __init__(self, gainTableFilePath, nKeyColumns=2, PCol=3, DCol=5, initialError=0, maxIntegral=None): ''' Inputs: gainTableFilePath: (string) Path to gain table text file ex: './MAPLEAF/Examples/TabulatedData/constPIDCoeffs.txt' nKeyColumns: (int) Number of 'key' columns (independent variables). Key columns are assumed to be the nKeyColumns leftmost ones PCol: (int) zero-indexed column number of P Coefficient DCol: (int) zero-indexed column number of D Coefficient Note: It is assumed that PCol, ICol, and DCol exist one after another in the table Inputs passed through to parent class (PICController): initialError, maxIntegral ''' PIDController.__init__(self, 0,0,0, initialError=initialError, maxIntegral=maxIntegral) # Columns are: Mach, Altitude(ASL), P1, I1, D1 pidData = np.loadtxt(gainTableFilePath, skiprows=1) keys = pidData[:,0:nKeyColumns] pidData = pidData[:,PCol:DCol+1] #Create interpolation function for PID coefficients self._getPIDCoeffs = NoNaNLinearNDInterpolator(keys, pidData) def updateCoefficientsFromGainTable(self, keyList): P, I, D = self._getPIDCoeffs(keyList) self.updateCoefficients(P, I, D)Ancestors
Subclasses
Methods
def updateCoefficientsFromGainTable(self, keyList)-
Expand source code
def updateCoefficientsFromGainTable(self, keyList): P, I, D = self._getPIDCoeffs(keyList) self.updateCoefficients(P, I, D)
class ScheduledGainPIDRocketMomentController (gainTableFilePath, keyColumnNames)-
Helper class that provides a standard way to create an ABC using inheritance.
Assumes the longitudinal (Pitch/Yaw) PID coefficients are in columns nKeyColumns:nKeyColumns+2 Assumes the roll PID coefficients are in columns nKeyColumns+3:nKeyColumns+5 Sample gain file: MAPLEAF/Examples/TabulatedData/testPIDCoeffs.txt
Expand source code
class ScheduledGainPIDRocketMomentController(MomentController, ScheduledGainPIDController): def __init__(self, gainTableFilePath, keyColumnNames): ''' Assumes the longitudinal (Pitch/Yaw) PID coefficients are in columns nKeyColumns:nKeyColumns+2 Assumes the roll PID coefficients are in columns nKeyColumns+3:nKeyColumns+5 Sample gain file: MAPLEAF/Examples/TabulatedData/testPIDCoeffs.txt ''' self.keyFunctionList = [ AeroParameters.stringToAeroFunctionMap[x] for x in keyColumnNames ] nKeyColumns = len(keyColumnNames) ScheduledGainPIDController.__init__(self, gainTableFilePath, nKeyColumns, PCol=nKeyColumns, DCol=nKeyColumns+5) def updateCoefficientsFromGainTable(self, keyList): ''' Overriding parent class method to enable separate longitudinal and roll coefficients in a single controller ''' Pxy, Ixy, Dxy, Pz, Iz, Dz = self._getPIDCoeffs(*keyList) # Coefficient for each of P, I, and D are: Longitudinal, Longitudinal, Roll # Operates the internal PID controller in vector-mode with numpy arrays (using their elementwise operations) P = np.array([Pxy, Pxy, Pz]) I = np.array([Ixy, Ixy, Iz]) D = np.array([Dxy, Dxy, Dz]) self.updateCoefficients(P, I, D) def _getOrientationError(self, rocketState, targetOrientation): return np.array((targetOrientation / rocketState.orientation).toRotationVector()) def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system ''' orientationError = self._getOrientationError(rocketState, targetOrientation) gainKeyList = AeroParameters.getAeroPropertiesList(self.keyFunctionList, rocketState, environment) self.updateCoefficientsFromGainTable(gainKeyList) return self.getNewSetPoint(orientationError, dt)Ancestors
Methods
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt)-
Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system
Expand source code
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system ''' orientationError = self._getOrientationError(rocketState, targetOrientation) gainKeyList = AeroParameters.getAeroPropertiesList(self.keyFunctionList, rocketState, environment) self.updateCoefficientsFromGainTable(gainKeyList) return self.getNewSetPoint(orientationError, dt) def updateCoefficientsFromGainTable(self, keyList)-
Overriding parent class method to enable separate longitudinal and roll coefficients in a single controller
Expand source code
def updateCoefficientsFromGainTable(self, keyList): ''' Overriding parent class method to enable separate longitudinal and roll coefficients in a single controller ''' Pxy, Ixy, Dxy, Pz, Iz, Dz = self._getPIDCoeffs(*keyList) # Coefficient for each of P, I, and D are: Longitudinal, Longitudinal, Roll # Operates the internal PID controller in vector-mode with numpy arrays (using their elementwise operations) P = np.array([Pxy, Pxy, Pz]) I = np.array([Ixy, Ixy, Iz]) D = np.array([Dxy, Dxy, Dz]) self.updateCoefficients(P, I, D)
class Stabilizer (desiredFlightDirection)-
Helper class that provides a standard way to create an ABC using inheritance.
Expand source code
class Stabilizer(Navigator): def __init__(self, desiredFlightDirection): # Calculate target orientation direction = desiredFlightDirection.normalize() angleFromVertical = Vector(0,0,1).angle(direction) rotationAxis = Vector(0,0,1).crossProduct(direction) self.targetOrientation = Quaternion(rotationAxis, angleFromVertical) def getTargetOrientation(self, rocketState, targetState, time): return self.targetOrientationAncestors
- Navigator
- abc.ABC
Inherited members
class TableInterpolatingActuatorController (deflectionTableFilePath, nKeyColumns, keyFunctionList, actuatorList)-
Controls actuators
Expand source code
class TableInterpolatingActuatorController(ActuatorController): ''' Controls actuators ''' def __init__(self, deflectionTableFilePath, nKeyColumns, keyFunctionList, actuatorList): self.actuatorList = actuatorList self.keyFunctionList = keyFunctionList self.deflectionTablePath = deflectionTableFilePath # Load actuator-deflection data table deflData = np.loadtxt(deflectionTableFilePath, skiprows=1) keys = deflData[:,0:nKeyColumns] deflData = deflData[:,nKeyColumns:] # Check that number of actuators matches number of deflection entries in the deflection tables nActuators = len(actuatorList) nDeflectionTableEntries = len(deflData[0,:]) if nActuators != nDeflectionTableEntries: raise ValueError("Number of actuators: {}, must match number of actuator deflection columns in deflection table: {}".format(nActuators, nDeflectionTableEntries)) # Create interpolation function for fin deflections self._getPositionTargets = NoNaNLinearNDInterpolator(keys, deflData) def setTargetActuatorDeflections(self, desiredMoments, state, environment, time): ''' Inputs: desiredMoments: (3-length iterable) contains desired moments about the x,y,and z axes time: time at which the moments are requested (time of current control loop execution) ''' # Construct key vector, starting with non-moment components: keyVector = AeroParameters.getAeroPropertiesList(self.keyFunctionList, state, environment) for desiredMoment in desiredMoments: keyVector.append(desiredMoment) newActuatorPositionTargets = self._getPositionTargets(*keyVector) for i in range(len(self.actuatorList)): self.actuatorList[i].setTargetDeflection(newActuatorPositionTargets[i], time) return list(newActuatorPositionTargets)Ancestors
- ActuatorController
- abc.ABC
Methods
def setTargetActuatorDeflections(self, desiredMoments, state, environment, time)-
Inputs
desiredMoments: (3-length iterable) contains desired moments about the x,y,and z axes time: time at which the moments are requested (time of current control loop execution)
Expand source code
def setTargetActuatorDeflections(self, desiredMoments, state, environment, time): ''' Inputs: desiredMoments: (3-length iterable) contains desired moments about the x,y,and z axes time: time at which the moments are requested (time of current control loop execution) ''' # Construct key vector, starting with non-moment components: keyVector = AeroParameters.getAeroPropertiesList(self.keyFunctionList, state, environment) for desiredMoment in desiredMoments: keyVector.append(desiredMoment) newActuatorPositionTargets = self._getPositionTargets(*keyVector) for i in range(len(self.actuatorList)): self.actuatorList[i].setTargetDeflection(newActuatorPositionTargets[i], time) return list(newActuatorPositionTargets)