Poincaré allows to define and simulate dynamical systems in Python.
To define the system
we can write:
>>> from poincare import Variable, System, initial
>>> class Model(System):
... # Define a variable with name `x` with an initial value (t=0) of `1``.
... x: Variable = initial(default=1)
... # The rate of change of `x` (i.e. velocity) is assigned (<<) to `-x`.
... # This relation is assigned to a Python variable (`eq`)
... eq = x.derive() << -x
...
To simulate that system:
>>> from poincare import Simulator
>>> sim = Simulator(Model)
>>> sim.solve(save_at=range(3))
x
time
0 1.000000
1 0.368139
2 0.135501
The output is a pandas.DataFrame
,
which can be plotted with .plot()
.
To change the initial condition,
we can pass a dictionary to the solve
method:
>>> sim.solve(values={Model.x: 2}, save_at=range(3))
x
time
0 2.000000
1 0.736278
2 0.271002
We can compute transformations of the output by passing a dictionary of expressions:
>>> Simulator(Model, transform={"x": Model.x, "2x": 2 * Model.x}).solve(save_at=range(3))
x 2x
time
0 1.000000 2.000000
1 0.368139 0.736278
2 0.135501 0.271002
To define a higher-order system, we have to assign an initial condition to the derivative of a variable:
>>> from poincare import Derivative
>>> class Oscillator(System):
... x: Variable = initial(default=1)
... v: Derivative = x.derive(initial=0)
... eq = v.derive() << -x
...
>>> Simulator(Oscillator).solve(save_at=range(3))
x v
time
0 1.000000 0.000000
1 0.540366 -0.841561
2 -0.416308 -0.909791
To use the independent variable,
we create an instance of Independent
:
>>> from poincare import Independent
>>> class NonAutonomous(System):
... time = Independent()
... x: Variable = initial(default=0)
... eq = x.derive() << 2 * time
...
>>> Simulator(NonAutonomous).solve(save_at=range(3))
x
time
0 0.000000
1 1.000001
2 4.000001
Besides variables, we can define parameters and constants, and use functions from symbolite.
Constants allow to define common initial conditions for Variables and Derivatives:
>>> from poincare import assign, Constant
>>> class Model(System):
... c: Constant = assign(default=1, constant=True)
... x: Variable = initial(default=c)
... y: Variable = initial(default=2 * c)
... eq_x = x.derive() << -x
... eq_y = y.derive() << -y
...
>>> Simulator(Model).solve(save_at=range(3))
x y
time
0 1.000000 2.000000
1 0.368139 0.736278
2 0.135501 0.271002
Now, we can vary their initial conditions jointly:
>>> Simulator(Model).solve(values={Model.c: 2}, save_at=range(3))
x y
time
0 2.000000 4.000000
1 0.736278 1.472556
2 0.271001 0.542003
But we can break that connection by passing y
's initial value directly:
>>> Simulator(Model).solve(values={Model.c: 2, Model.y: 2}, save_at=range(3))
x y
time
0 2.000000 2.000000
1 0.736278 0.736278
2 0.271002 0.271002
Parameters are like Variables, but their time evolution is given directly as a function of time, Variables, Constants and other Parameters:
>>> from poincare import Parameter
>>> class Model(System):
... p: Parameter = assign(default=1)
... x: Variable = initial(default=1)
... eq = x.derive() << -p * x
...
>>> Simulator(Model).solve(save_at=range(3))
x
time
0 1.000000
1 0.368139
2 0.135501
Symbolite functions are accessible from the symbolite.scalar
module:
>>> from symbolite import scalar
>>> class Model(System):
... x: Variable = initial(default=1)
... eq = x.derive() << scalar.sin(x)
...
>>> Simulator(Model).solve(save_at=range(3))
x
time
0 1.000000
1 1.951464
2 2.654572
poincaré also supports functions through
pint
and pint-pandas
.
>>> import pint
>>> unit = pint.get_application_registry()
>>> class Model(System):
... x: Variable = initial(default=1 * unit.m)
... v: Derivative = x.derive(initial=0 * unit.m/unit.s)
... w: Parameter = assign(default=1 * unit.Hz)
... eq = v.derive() << -w**2 * x
...
>>> result = Simulator(Model).solve(save_at=range(3))
The columns have units of m
and m/s
, respectively.
pint
raises a DimensionalityError
if we try to add them:
>>> result["x"] + result["v"]
Traceback (most recent call last):
...
pint.errors.DimensionalityError: Cannot convert from 'meter' ([length]) to 'meter / second' ([length] / [time])
We can remove the units and set them as string metadata with:
>>> result.pint.dequantify()
x v
unit meter meter / second
time
0 1.0 0.0
1 0.540366 -0.841561
2 -0.416308 -0.909791
which allows to plot the DataFrame with .plot()
.
It can be installed from PyPI:
pip install -U poincare
or conda-forge:
conda install -c conda-forge poincare
This project is managed by pixi. You can install it for development using:
git clone https://github.com/{{ github_username }}/{{ project_name }}
cd {{ project_name }}
pixi run pre-commit-install
Pre-commit hooks are used to lint and format the project.
Run tests using:
pixi run test