![PyVersion](https://img.shields.io/pypi/pyversions/poincare?label=python)
![Package](https://img.shields.io/pypi/v/poincare?label=PyPI)
![Conda Version](https://img.shields.io/conda/vn/conda-forge/poincare)
![License](https://img.shields.io/pypi/l/poincare?label=license)
[![CI](https://github.com/maurosilber/poincare/actions/workflows/ci.yml/badge.svg)](https://github.com/maurosilber/poincare/actions/workflows/ci.yml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/maurosilber/poincare/main.svg)](https://results.pre-commit.ci/latest/github/maurosilber/poincare/main)
# Poincaré: simulation of dynamical systems
Poincaré allows to define and simulate dynamical systems in Python.
### Definition
To define the system
$$ \\frac{dx}{dt} = -x \\quad \\text{with} \\quad x(0) = 1 $$
we can write:
```python
>>> 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
...
```
### Simulation
To simulate that system:
```python
>>> 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()`.
### Changing initial conditions
To change the initial condition,
we can pass a dictionary to the `solve` method:
```python
>>> sim.solve(values={Model.x: 2}, save_at=range(3))
x
time
0 2.000000
1 0.736278
2 0.271002
```
### Transforming the output
We can compute transformations of the output
by passing a dictionary of expressions:
```python
>>> 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
```
### Higher-order systems
To define a higher-order system,
we have to assign an initial condition to the derivative of a variable:
```python
>>> 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
```
### Non-autonomous systems
To use the independent variable,
we create an instance of `Independent`:
```python
>>> 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
```
### Constants, Parameters, and functions
Besides variables,
we can define parameters and constants,
and use functions from [symbolite](https://github.com/hgrecco/symbolite).
#### Constants
Constants allow to define common initial conditions for Variables and Derivatives:
```python
>>> 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:
```python
>>> 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:
```python
>>> 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
Parameters are like Variables,
but their time evolution is given directly as a function of time,
Variables, Constants and other Parameters:
```python
>>> 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
```
#### Functions
Symbolite functions are accessible from the `symbolite.scalar` module:
```python
>>> 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
```
### Units
poincaré also supports functions through
[`pint`](https://github.com/hgrecco/pint)
and [`pint-pandas`](https://github.com/hgrecco/pint-pandas).
```python
>>> 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:
```python
>>> 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:
```python
>>> 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()`.
## Installation
It can be installed from PyPI:
```
pip install -U poincare
```
or conda-forge:
```
conda install -c conda-forge poincare
```
## Development
This project is managed by [pixi](https://pixi.sh).
You can install it for development using:
```sh
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.
### Testing
Run tests using:
```sh
pixi run test
```
Raw data
{
"_id": null,
"home_page": null,
"name": "poincare",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "ODE, differential equations, dynamical systems",
"author": null,
"author_email": "\"Hern\u00e1n E. Grecco\" <hernan.grecco@gmail.com>, Mauro Silberberg <maurosilber@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/c3/b5/d73560e520bb70b346cb680db9bf6a3d8d635e595fdc8ec1f8866beff776/poincare-0.6.1.tar.gz",
"platform": null,
"description": "![PyVersion](https://img.shields.io/pypi/pyversions/poincare?label=python)\n![Package](https://img.shields.io/pypi/v/poincare?label=PyPI)\n![Conda Version](https://img.shields.io/conda/vn/conda-forge/poincare)\n![License](https://img.shields.io/pypi/l/poincare?label=license)\n[![CI](https://github.com/maurosilber/poincare/actions/workflows/ci.yml/badge.svg)](https://github.com/maurosilber/poincare/actions/workflows/ci.yml)\n[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/maurosilber/poincare/main.svg)](https://results.pre-commit.ci/latest/github/maurosilber/poincare/main)\n\n# Poincar\u00e9: simulation of dynamical systems\n\nPoincar\u00e9 allows to define and simulate dynamical systems in Python.\n\n### Definition\n\nTo define the system\n\n$$ \\\\frac{dx}{dt} = -x \\\\quad \\\\text{with} \\\\quad x(0) = 1 $$\n\nwe can write:\n\n```python\n>>> from poincare import Variable, System, initial\n>>> class Model(System):\n... # Define a variable with name `x` with an initial value (t=0) of `1``.\n... x: Variable = initial(default=1)\n... # The rate of change of `x` (i.e. velocity) is assigned (<<) to `-x`.\n... # This relation is assigned to a Python variable (`eq`)\n... eq = x.derive() << -x\n...\n```\n\n### Simulation\n\nTo simulate that system:\n\n```python\n>>> from poincare import Simulator\n>>> sim = Simulator(Model)\n>>> sim.solve(save_at=range(3))\n x\ntime\n0 1.000000\n1 0.368139\n2 0.135501\n```\n\nThe output is a `pandas.DataFrame`,\nwhich can be plotted with `.plot()`.\n\n### Changing initial conditions\n\nTo change the initial condition,\nwe can pass a dictionary to the `solve` method:\n\n```python\n>>> sim.solve(values={Model.x: 2}, save_at=range(3))\n x\ntime\n0 2.000000\n1 0.736278\n2 0.271002\n```\n\n### Transforming the output\n\nWe can compute transformations of the output\nby passing a dictionary of expressions:\n\n```python\n>>> Simulator(Model, transform={\"x\": Model.x, \"2x\": 2 * Model.x}).solve(save_at=range(3))\n x 2x\ntime\n0 1.000000 2.000000\n1 0.368139 0.736278\n2 0.135501 0.271002\n```\n\n### Higher-order systems\n\nTo define a higher-order system,\nwe have to assign an initial condition to the derivative of a variable:\n\n```python\n>>> from poincare import Derivative\n>>> class Oscillator(System):\n... x: Variable = initial(default=1)\n... v: Derivative = x.derive(initial=0)\n... eq = v.derive() << -x\n...\n>>> Simulator(Oscillator).solve(save_at=range(3))\n x v\ntime\n0 1.000000 0.000000\n1 0.540366 -0.841561\n2 -0.416308 -0.909791\n```\n\n### Non-autonomous systems\n\nTo use the independent variable,\nwe create an instance of `Independent`:\n\n```python\n>>> from poincare import Independent\n>>> class NonAutonomous(System):\n... time = Independent()\n... x: Variable = initial(default=0)\n... eq = x.derive() << 2 * time\n...\n>>> Simulator(NonAutonomous).solve(save_at=range(3))\n x\ntime\n0 0.000000\n1 1.000001\n2 4.000001\n```\n\n### Constants, Parameters, and functions\n\nBesides variables,\nwe can define parameters and constants,\nand use functions from [symbolite](https://github.com/hgrecco/symbolite).\n\n#### Constants\n\nConstants allow to define common initial conditions for Variables and Derivatives:\n\n```python\n>>> from poincare import assign, Constant\n>>> class Model(System):\n... c: Constant = assign(default=1, constant=True)\n... x: Variable = initial(default=c)\n... y: Variable = initial(default=2 * c)\n... eq_x = x.derive() << -x\n... eq_y = y.derive() << -y\n...\n>>> Simulator(Model).solve(save_at=range(3))\n x y\ntime\n0 1.000000 2.000000\n1 0.368139 0.736278\n2 0.135501 0.271002\n```\n\nNow, we can vary their initial conditions jointly:\n\n```python\n>>> Simulator(Model).solve(values={Model.c: 2}, save_at=range(3))\n x y\ntime\n0 2.000000 4.000000\n1 0.736278 1.472556\n2 0.271001 0.542003\n```\n\nBut we can break that connection by passing `y`'s initial value directly:\n\n```python\n>>> Simulator(Model).solve(values={Model.c: 2, Model.y: 2}, save_at=range(3))\n x y\ntime\n0 2.000000 2.000000\n1 0.736278 0.736278\n2 0.271002 0.271002\n```\n\n#### Parameters\n\nParameters are like Variables,\nbut their time evolution is given directly as a function of time,\nVariables, Constants and other Parameters:\n\n```python\n>>> from poincare import Parameter\n>>> class Model(System):\n... p: Parameter = assign(default=1)\n... x: Variable = initial(default=1)\n... eq = x.derive() << -p * x\n...\n>>> Simulator(Model).solve(save_at=range(3))\n x\ntime\n0 1.000000\n1 0.368139\n2 0.135501\n```\n\n#### Functions\n\nSymbolite functions are accessible from the `symbolite.scalar` module:\n\n```python\n>>> from symbolite import scalar\n>>> class Model(System):\n... x: Variable = initial(default=1)\n... eq = x.derive() << scalar.sin(x)\n...\n>>> Simulator(Model).solve(save_at=range(3))\n x\ntime\n0 1.000000\n1 1.951464\n2 2.654572\n```\n\n### Units\n\npoincar\u00e9 also supports functions through\n[`pint`](https://github.com/hgrecco/pint)\nand [`pint-pandas`](https://github.com/hgrecco/pint-pandas).\n\n```python\n>>> import pint\n>>> unit = pint.get_application_registry()\n>>> class Model(System):\n... x: Variable = initial(default=1 * unit.m)\n... v: Derivative = x.derive(initial=0 * unit.m/unit.s)\n... w: Parameter = assign(default=1 * unit.Hz)\n... eq = v.derive() << -w**2 * x\n...\n>>> result = Simulator(Model).solve(save_at=range(3))\n```\n\nThe columns have units of `m` and `m/s`, respectively.\n`pint` raises a `DimensionalityError` if we try to add them:\n\n```python\n>>> result[\"x\"] + result[\"v\"]\nTraceback (most recent call last):\n...\npint.errors.DimensionalityError: Cannot convert from 'meter' ([length]) to 'meter / second' ([length] / [time])\n```\n\nWe can remove the units and set them as string metadata with:\n\n```python\n>>> result.pint.dequantify()\n x v\nunit meter meter / second\ntime\n0 1.0 0.0\n1 0.540366 -0.841561\n2 -0.416308 -0.909791\n```\n\nwhich allows to plot the DataFrame with `.plot()`.\n\n## Installation\n\nIt can be installed from PyPI:\n\n```\npip install -U poincare\n```\n\nor conda-forge:\n\n```\nconda install -c conda-forge poincare\n```\n\n## Development\n\nThis project is managed by [pixi](https://pixi.sh).\nYou can install it for development using:\n\n```sh\ngit clone https://github.com/{{ github_username }}/{{ project_name }}\ncd {{ project_name }}\npixi run pre-commit-install\n```\n\nPre-commit hooks are used to lint and format the project.\n\n### Testing\n\nRun tests using:\n\n```sh\npixi run test\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Simulation of dynamical systems.",
"version": "0.6.1",
"project_urls": {
"Documentation": "https://maurosilber.github.io/poincare",
"Homepage": "https://github.com/maurosilber/poincare",
"Issues": "https://github.com/maurosilber/poincare/issues"
},
"split_keywords": [
"ode",
" differential equations",
" dynamical systems"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "d0228a914d715df57cc66784e0b9d97a7a78c059e2f73151faff0655cece9844",
"md5": "642ff53746d723b63dbbc528330c4574",
"sha256": "9a1d4811610a23d40a0d9aa3eaef25ffbac907c533459583bef02f6c02e7bd78"
},
"downloads": -1,
"filename": "poincare-0.6.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "642ff53746d723b63dbbc528330c4574",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 36981,
"upload_time": "2024-12-17T15:11:59",
"upload_time_iso_8601": "2024-12-17T15:11:59.496214Z",
"url": "https://files.pythonhosted.org/packages/d0/22/8a914d715df57cc66784e0b9d97a7a78c059e2f73151faff0655cece9844/poincare-0.6.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "c3b5d73560e520bb70b346cb680db9bf6a3d8d635e595fdc8ec1f8866beff776",
"md5": "c7d0358ab5664a3556b389a15bb9b8bb",
"sha256": "0c4026e3a940f1a40d483f33b70ecd5e1977295e08489c34378a89a4a9161d58"
},
"downloads": -1,
"filename": "poincare-0.6.1.tar.gz",
"has_sig": false,
"md5_digest": "c7d0358ab5664a3556b389a15bb9b8bb",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 76897,
"upload_time": "2024-12-17T15:12:01",
"upload_time_iso_8601": "2024-12-17T15:12:01.787343Z",
"url": "https://files.pythonhosted.org/packages/c3/b5/d73560e520bb70b346cb680db9bf6a3d8d635e595fdc8ec1f8866beff776/poincare-0.6.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-17 15:12:01",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "maurosilber",
"github_project": "poincare",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "poincare"
}