Hybrid ODE Sim is a Python package designed to solve systems of ordinary differential equations (ODEs) that involve both continuous and discrete dynamics. That is, dynamics of the forms:
This solver is particularly useful for simulations that require handling events or state changes at specific points in time while integrating continuous dynamics in between these events. This commonly occurs in simulating robots and their control/planning systems. The package is meant to be very simple and leverages common numerical integrators to provide accurate and efficient solutions for hybrid systems.
The package contains four main building blocks: ContinuousTimeModel
, DiscreteTimeModel
, ModelGraph
, and Simulator
.
For examples of how to use different kinds of models and the simulator class, please see the test notebooks.
Each DiscreteTimeModel
is evaluated at an integral sample rate, say 5 times per second / 5 Hz. Implementing a DiscreteTimeModel
requires overriding the function:
def discrete_dynamics(self, t: float, y: Any) -> Any:
y_next = ...
return y_next
DiscreteTimeModel
constructors are provided with an initial state y0
, a string name
, their integral evaluation frequency sample_rate
, an optional params
object, and an optional logging level.
Between consecutive evaluations of a DiscreteTimeModel
, all ContinuousTimeModel
instances are integrated simultaneously using a numerical integrator like RK4 (other integrators are available as well). Implementing a ContinuousTimeModel
requires overriding the function:
def continuous_dynamics(t: float, y: np.ndarray) -> np.ndarray:
ydot = ...
return ydot
ContinuousTimeModel
constructors are provided with an initial state y0
, a string name
, an optional params
object, and an optional logging level.
When multiple DiscreteTimeModel
instances are to be evaluated at the same timestep (e.g. two discrete models both run at 100 Hz), the ModelGraph
imposes the evaluation order. To indicate that a particular model model_A
should receive feedback from / have access to the state model_B
, we have two conventions:
-
We use the convention
model_B.inputs_to(model_A)
, wheremodel_B
,model_B
areDiscreteTimeModel
, to indicate that the directed edgemodel_B -> model_A
exists in the topological sorting of theModelGraph
in the case thatmodel_B
andmodel_A
are evaluated during the same timestep. The.inputs_to
function is only to be used whenmodel_A
andmodel_B
are bothDiscreteTimeModel
. -
We use the convention
model_C.feedback_from(model_D)
to get access tomodel_D
's state from the dynamics function ofmodel_C
, but this does not impose any evaluation order.
Think of inputs_to
as a forward edge in a Simulink model, while feedback_from
is a reverse edge.
You may use the self.input_models
instance member to access inputs' states. This is a dict with keys as model names (strings) and values being that model's current/most recent state.
Note: There is a zero-order hold on discrete system states between consecutive evaluations if the discrete model's state is queried by other systems in that in-between time.
The simulator ties everything together into one class which takes as input a model graph and an option for the integrator to be used. The Simulator
class exposes the simulate
function which takes as input the time range (with start and end times as rational numbers via the Fraction
type) to simulate.
You can install hybrid_ode_sim
as a local pip package and use the provided library as you would any other.
Clone the repository locally:
git clone https://github.com/micahreich/hybrid_ode_sim.git # for https
git clone git@github.com:micahreich/hybrid_ode_sim.git # for ssh
Install pip requirements from the requirements.txt
Now, when you wish to use hybrid_ode_sim
, ideally from within a virtual environment, just run:
pip install -e /path/to/hybrid_ode_sim # install `hybrid_ode_sim` as a pip package along with requirements
And that's it!
An instance of an adaptive Runge-Kutta implementation can be provided as the integrator to a simulator. Currently implemented are RK23 and RK54, but adding other methods is as easy as providing the Butcher Tableau. Other integrators can be implemented by inheriting from the Integrator
class.
An instance of a fixed-step Runge-Kutta implementation can be provided as the integrator to a simulator. Currently implemented is RK4, but adding other methods is as easy as providing the Butcher Tableau. Other integrators can be implemented by inheriting from the Integrator
class.
The package also includes some rendering tooling which animates the results of simulations. Matplotlib animations can be created by creating and rendering PlotElements
which grab data from system instances. Multiple plot elements can be combined into one PlotEnvironment
, rendered, and also saved as a video or GIF.
- Implement zero-crossing detection for early stopping
- Benchmark performance against
scipy.solve_ivp