dynamicalnodes 0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dynamicalnodes-0.1/LICENSE +21 -0
- dynamicalnodes-0.1/PKG-INFO +82 -0
- dynamicalnodes-0.1/README.md +50 -0
- dynamicalnodes-0.1/pyproject.toml +57 -0
- dynamicalnodes-0.1/setup.cfg +4 -0
- dynamicalnodes-0.1/src/dynamicalnodes/__init__.py +35 -0
- dynamicalnodes-0.1/src/dynamicalnodes/dynamical_system.py +221 -0
- dynamicalnodes-0.1/src/dynamicalnodes/ros2py_py2ros.py +1034 -0
- dynamicalnodes-0.1/src/dynamicalnodes/rosnode.py +658 -0
- dynamicalnodes-0.1/src/dynamicalnodes/rostools.py +372 -0
- dynamicalnodes-0.1/src/dynamicalnodes.egg-info/PKG-INFO +82 -0
- dynamicalnodes-0.1/src/dynamicalnodes.egg-info/SOURCES.txt +13 -0
- dynamicalnodes-0.1/src/dynamicalnodes.egg-info/dependency_links.txt +1 -0
- dynamicalnodes-0.1/src/dynamicalnodes.egg-info/requires.txt +20 -0
- dynamicalnodes-0.1/src/dynamicalnodes.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nehal Singh Mangat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dynamicalnodes
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: dynamicalnodes is a Python library for modeling general control systems.
|
|
5
|
+
Author-email: Nehal Singh Mangat <nehalsinghmangat.software@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nehalsinghmangat/dynamicalnodes
|
|
8
|
+
Project-URL: Documentation, https://nehalsinghmangat.github.io/dynamicalnodes
|
|
9
|
+
Keywords: control,estimation,ros2
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy>=1.24
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-doctestplus>=1.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
18
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
19
|
+
Requires-Dist: isort>=6.0; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
21
|
+
Requires-Dist: build>=0.10; extra == "dev"
|
|
22
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: sphinx>=8.0; extra == "docs"
|
|
25
|
+
Requires-Dist: sphinx-rtd-theme>=1.3; extra == "docs"
|
|
26
|
+
Requires-Dist: myst_nb; extra == "docs"
|
|
27
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.24.0; extra == "docs"
|
|
28
|
+
Requires-Dist: matplotlib>=3.7; extra == "docs"
|
|
29
|
+
Requires-Dist: scipy>=1.11; extra == "docs"
|
|
30
|
+
Requires-Dist: jupyter; extra == "docs"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
 [](https://github.com/nehalsinghmangat/dynamicalnodes/actions/workflows/doctest.yml)
|
|
34
|
+
|
|
35
|
+
# dynamicalnodes: From Theory to Python to ROS
|
|
36
|
+
|
|
37
|
+
> _"Cross a river once, swim; cross a river a thousand times, build a bridge."_ — Punjabi saying
|
|
38
|
+
|
|
39
|
+
**dynamicalnodes** is a Python development framework that bridges the chasm between theoretical control systems and their implementation in hardware. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make controlling robots easier.
|
|
40
|
+
|
|
41
|
+
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://dynamicalnodes.readthedocs.io).
|
|
42
|
+
|
|
43
|
+
For a 22 second video describing this framework's intended workflow for, please click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
## Turtlebot: A journey of four steps
|
|
47
|
+
<table>
|
|
48
|
+
<tr>
|
|
49
|
+
<td align="center">
|
|
50
|
+
<img src="docs/source/_static/tikz/turtlebot/turtlebot.svg" alt="TurtleBot Overview" width="400"/><br/>
|
|
51
|
+
</td>
|
|
52
|
+
<td align="center">
|
|
53
|
+
<img src="docs/source/_static/gif/turtlesim_software.gif" alt="Turtle Software" width="200"/><br/>
|
|
54
|
+
</td>
|
|
55
|
+
<td align="center">
|
|
56
|
+
<img src="docs/source/_static/gif/turtlesim_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
57
|
+
</td>
|
|
58
|
+
<td align="center">
|
|
59
|
+
<img src="docs/source/_static/gif/turtlesim_hardware.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
</table>
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<img src="docs/source/_static/misc/labels_for_steps_from_theory_to_hardware.png" alt="Step Labels" width="800"/>
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
## 👁️ Watch for Releases
|
|
70
|
+
|
|
71
|
+
To get notified when **dynamicalnodes** is officially released:
|
|
72
|
+
|
|
73
|
+
1. Go to the GitHub repo: [https://github.com/nehalsinghmangat/dynamicalnodes](https://github.com/nehalsinghmangat/dynamicalnodes)
|
|
74
|
+
2. Click the `👁️ Watch` button near the top-right of the page.
|
|
75
|
+
3. Select **"Custom"** or **"Releases only"** from the dropdown.
|
|
76
|
+
4. Ensure **"Releases"** is checked.
|
|
77
|
+
|
|
78
|
+
You’ll now receive notifications when the first release is published!
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
 [](https://github.com/nehalsinghmangat/dynamicalnodes/actions/workflows/doctest.yml)
|
|
2
|
+
|
|
3
|
+
# dynamicalnodes: From Theory to Python to ROS
|
|
4
|
+
|
|
5
|
+
> _"Cross a river once, swim; cross a river a thousand times, build a bridge."_ — Punjabi saying
|
|
6
|
+
|
|
7
|
+
**dynamicalnodes** is a Python development framework that bridges the chasm between theoretical control systems and their implementation in hardware. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make controlling robots easier.
|
|
8
|
+
|
|
9
|
+
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://dynamicalnodes.readthedocs.io).
|
|
10
|
+
|
|
11
|
+
For a 22 second video describing this framework's intended workflow for, please click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
## Turtlebot: A journey of four steps
|
|
15
|
+
<table>
|
|
16
|
+
<tr>
|
|
17
|
+
<td align="center">
|
|
18
|
+
<img src="docs/source/_static/tikz/turtlebot/turtlebot.svg" alt="TurtleBot Overview" width="400"/><br/>
|
|
19
|
+
</td>
|
|
20
|
+
<td align="center">
|
|
21
|
+
<img src="docs/source/_static/gif/turtlesim_software.gif" alt="Turtle Software" width="200"/><br/>
|
|
22
|
+
</td>
|
|
23
|
+
<td align="center">
|
|
24
|
+
<img src="docs/source/_static/gif/turtlesim_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
25
|
+
</td>
|
|
26
|
+
<td align="center">
|
|
27
|
+
<img src="docs/source/_static/gif/turtlesim_hardware.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="docs/source/_static/misc/labels_for_steps_from_theory_to_hardware.png" alt="Step Labels" width="800"/>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
## 👁️ Watch for Releases
|
|
38
|
+
|
|
39
|
+
To get notified when **dynamicalnodes** is officially released:
|
|
40
|
+
|
|
41
|
+
1. Go to the GitHub repo: [https://github.com/nehalsinghmangat/dynamicalnodes](https://github.com/nehalsinghmangat/dynamicalnodes)
|
|
42
|
+
2. Click the `👁️ Watch` button near the top-right of the page.
|
|
43
|
+
3. Select **"Custom"** or **"Releases only"** from the dropdown.
|
|
44
|
+
4. Ensure **"Releases"** is checked.
|
|
45
|
+
|
|
46
|
+
You’ll now receive notifications when the first release is published!
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61.0",
|
|
4
|
+
"wheel"
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "dynamicalnodes"
|
|
10
|
+
version = "0.1"
|
|
11
|
+
description = "dynamicalnodes is a Python library for modeling general control systems."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Nehal Singh Mangat", email = "nehalsinghmangat.software@gmail.com" }
|
|
17
|
+
]
|
|
18
|
+
keywords = ["control", "estimation", "ros2"]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"numpy>=1.24",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0",
|
|
26
|
+
"pytest-doctestplus>=1.0.0",
|
|
27
|
+
"flake8>=6.0",
|
|
28
|
+
"black>=24.0",
|
|
29
|
+
"isort>=6.0",
|
|
30
|
+
"mypy>=1.0",
|
|
31
|
+
"build>=0.10",
|
|
32
|
+
"twine>=4.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
docs = [
|
|
36
|
+
"sphinx>=8.0",
|
|
37
|
+
"sphinx-rtd-theme>=1.3",
|
|
38
|
+
"myst_nb",
|
|
39
|
+
"sphinx-autodoc-typehints>=1.24.0",
|
|
40
|
+
"matplotlib>=3.7",
|
|
41
|
+
"scipy>=1.11",
|
|
42
|
+
"jupyter",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/nehalsinghmangat/dynamicalnodes"
|
|
47
|
+
Documentation = "https://nehalsinghmangat.github.io/dynamicalnodes"
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
addopts = "--doctest-modules"
|
|
51
|
+
doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS"]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools]
|
|
54
|
+
package-dir = {"" = "src"}
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["src"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dynamicalnodes: A modular Python framework for dynamical systems, estimation, and ROS2 integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .dynamical_system import DynamicalSystem
|
|
6
|
+
from . import rostools
|
|
7
|
+
|
|
8
|
+
# Lazy imports for ROS2-dependent symbols — only fail if actually used
|
|
9
|
+
def __getattr__(name):
|
|
10
|
+
if name == "ROSNode":
|
|
11
|
+
try:
|
|
12
|
+
from .rosnode import ROSNode
|
|
13
|
+
return ROSNode
|
|
14
|
+
except ImportError as e:
|
|
15
|
+
raise ImportError(
|
|
16
|
+
f"ROSNode requires ROS2 dependencies. Install with: pip install dynamicalnodes[ros2]\n"
|
|
17
|
+
f"Original error: {e}"
|
|
18
|
+
) from e
|
|
19
|
+
if name == "reset_ros":
|
|
20
|
+
try:
|
|
21
|
+
from .rostools import reset_ros
|
|
22
|
+
return reset_ros
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
raise ImportError(
|
|
25
|
+
f"reset_ros requires ROS2 dependencies. Install with: pip install dynamicalnodes[ros2]\n"
|
|
26
|
+
f"Original error: {e}"
|
|
27
|
+
) from e
|
|
28
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"DynamicalSystem",
|
|
32
|
+
"ROSNode",
|
|
33
|
+
"reset_ros",
|
|
34
|
+
"rostools",
|
|
35
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core abstraction for modeling dynamical systems in dynamicalnodes.
|
|
3
|
+
|
|
4
|
+
A dynamical system is defined by two functions:
|
|
5
|
+
- f: State transition function (optional) - computes next state
|
|
6
|
+
- h: Observation function (required) - computes output from state
|
|
7
|
+
|
|
8
|
+
This abstraction allows any control system component (plants, controllers,
|
|
9
|
+
observers, reference signals) to be modeled uniformly and composed together.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
from typing import Any, Callable, Dict, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DynamicalSystem:
|
|
17
|
+
"""
|
|
18
|
+
A discrete-time dynamical system with state transition and observation functions.
|
|
19
|
+
|
|
20
|
+
This class provides a unified abstraction for modeling control system components.
|
|
21
|
+
Each component is defined by:
|
|
22
|
+
|
|
23
|
+
- **f(x_k, u_k, ...) -> x_{k+1}**: State transition function (optional)
|
|
24
|
+
- **h(x_k, u_k, ...) -> y_k**: Observation/output function (required)
|
|
25
|
+
|
|
26
|
+
The `step()` method executes one timestep: it calls f to update state,
|
|
27
|
+
then calls h to compute the output.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
f : Callable, optional
|
|
32
|
+
State transition function. Signature: f(x_k, u_k, ...) -> x_{k+1}
|
|
33
|
+
If None, the system is stateless (e.g., a reference signal generator).
|
|
34
|
+
h : Callable
|
|
35
|
+
Observation function. Signature: h(x_k, u_k, ...) -> y_k
|
|
36
|
+
This function computes the system output.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
Stateless reference signal (step function):
|
|
41
|
+
|
|
42
|
+
>>> def h_ref(tk, params):
|
|
43
|
+
... u0, t_step = params
|
|
44
|
+
... return u0 if tk >= t_step else 0.0
|
|
45
|
+
>>> ref_signal = DynamicalSystem(h=h_ref)
|
|
46
|
+
>>> ref_signal.step(tk=0.0, params=(100, 15)) # Before step time
|
|
47
|
+
0.0
|
|
48
|
+
>>> ref_signal.step(tk=20.0, params=(100, 15)) # After step time
|
|
49
|
+
100
|
|
50
|
+
|
|
51
|
+
Stateful plant (discrete-time integrator):
|
|
52
|
+
|
|
53
|
+
>>> def f_plant(x_k, u_k, dt):
|
|
54
|
+
... return x_k + u_k * dt
|
|
55
|
+
>>> def h_plant(x_k, u_k, dt):
|
|
56
|
+
... return x_k
|
|
57
|
+
>>> plant = DynamicalSystem(f=f_plant, h=h_plant)
|
|
58
|
+
>>> x_next, y = plant.step(x_k=0.0, u_k=1.0, dt=0.1)
|
|
59
|
+
>>> x_next
|
|
60
|
+
0.1
|
|
61
|
+
|
|
62
|
+
Notes
|
|
63
|
+
-----
|
|
64
|
+
The `_smart_call` mechanism allows flexible parameter binding. Functions
|
|
65
|
+
only receive the parameters they declare in their signature, enabling
|
|
66
|
+
components with different interfaces to be composed together.
|
|
67
|
+
|
|
68
|
+
See Also
|
|
69
|
+
--------
|
|
70
|
+
ROSNode : Wraps a DynamicalSystem as a ROS2 node for deployment.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
f: Optional[Callable] = None,
|
|
77
|
+
h: Callable,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Initialize a DynamicalSystem with state transition and observation functions.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
f : Callable, optional
|
|
85
|
+
State transition function: f(x_k, u_k, ...) -> x_{k+1}
|
|
86
|
+
h : Callable
|
|
87
|
+
Observation function: h(x_k, u_k, ...) -> y_k
|
|
88
|
+
"""
|
|
89
|
+
self._f = f
|
|
90
|
+
self._h = h
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def f(self) -> Optional[Callable]:
|
|
94
|
+
"""State transition function f(x_k, u_k, ...) -> x_{k+1}, or None if stateless."""
|
|
95
|
+
return self._f
|
|
96
|
+
|
|
97
|
+
@f.setter
|
|
98
|
+
def f(self, f: Optional[Callable]) -> None:
|
|
99
|
+
self._f = f
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def h(self) -> Callable:
|
|
103
|
+
"""Observation function h(x_k, u_k, ...) -> y_k."""
|
|
104
|
+
return self._h
|
|
105
|
+
|
|
106
|
+
@h.setter
|
|
107
|
+
def h(self, h: Callable) -> None:
|
|
108
|
+
self._h = h
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _smart_call(
|
|
112
|
+
func: Callable[..., Any],
|
|
113
|
+
**kwargs: Any,
|
|
114
|
+
) -> Any:
|
|
115
|
+
"""
|
|
116
|
+
Call a function with smart parameter binding by name.
|
|
117
|
+
|
|
118
|
+
This method inspects the function signature and passes only the
|
|
119
|
+
parameters that the function declares. This enables flexible
|
|
120
|
+
composition of components with different interfaces.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
func : Callable
|
|
125
|
+
The function to call.
|
|
126
|
+
**kwargs : Any
|
|
127
|
+
Keyword arguments pool. Only arguments matching the function's
|
|
128
|
+
declared parameters are passed.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
Any
|
|
133
|
+
The return value of the function.
|
|
134
|
+
|
|
135
|
+
Notes
|
|
136
|
+
-----
|
|
137
|
+
If the function has a **kwargs parameter, all remaining kwargs
|
|
138
|
+
from the pool are passed through.
|
|
139
|
+
|
|
140
|
+
Examples
|
|
141
|
+
--------
|
|
142
|
+
Functions receive only the parameters they declare:
|
|
143
|
+
|
|
144
|
+
>>> def needs_time(tk, gain): return tk * gain
|
|
145
|
+
>>> def needs_state(xk, gain): return (xk, gain)
|
|
146
|
+
>>> def needs_both(tk, xk, gain): return (tk, xk, gain)
|
|
147
|
+
|
|
148
|
+
>>> DynamicalSystem._smart_call(needs_time, tk=1.0, xk=[0, 0], gain=2.0)
|
|
149
|
+
2.0
|
|
150
|
+
>>> DynamicalSystem._smart_call(needs_state, tk=1.0, xk=[5, 5], gain=2.0)
|
|
151
|
+
([5, 5], 2.0)
|
|
152
|
+
>>> DynamicalSystem._smart_call(needs_both, tk=1.0, xk='state', gain=2.0)
|
|
153
|
+
(1.0, 'state', 2.0)
|
|
154
|
+
"""
|
|
155
|
+
sig = inspect.signature(func)
|
|
156
|
+
pool: Dict[str, Any] = dict(kwargs)
|
|
157
|
+
|
|
158
|
+
# Filter: only pass arguments the function declares
|
|
159
|
+
call_kwargs: Dict[str, Any] = {}
|
|
160
|
+
for name, p in sig.parameters.items():
|
|
161
|
+
if p.kind in (
|
|
162
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
163
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
164
|
+
):
|
|
165
|
+
if name in pool:
|
|
166
|
+
call_kwargs[name] = pool.pop(name)
|
|
167
|
+
|
|
168
|
+
# Pass remaining kwargs if function has **kwargs
|
|
169
|
+
if any(
|
|
170
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
171
|
+
):
|
|
172
|
+
call_kwargs.update(pool)
|
|
173
|
+
|
|
174
|
+
return func(**call_kwargs)
|
|
175
|
+
|
|
176
|
+
def step(self, **kwargs: Any) -> Any:
|
|
177
|
+
"""
|
|
178
|
+
Execute one timestep of the dynamical system.
|
|
179
|
+
|
|
180
|
+
This method calls the state transition function f (if defined) and
|
|
181
|
+
the observation function h with the provided keyword arguments.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
**kwargs : Any
|
|
186
|
+
Keyword arguments passed to f and h. Each function receives
|
|
187
|
+
only the kwargs it declares in its signature via `_smart_call`.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
Any
|
|
192
|
+
If f is None (stateless system):
|
|
193
|
+
Returns y_k = h(...)
|
|
194
|
+
If f is defined (stateful system):
|
|
195
|
+
Returns (x_{k+1}, y_k) = (f(...), h(...))
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
Stateless system (no f):
|
|
200
|
+
|
|
201
|
+
>>> def h(tk): return tk * 2
|
|
202
|
+
>>> sys = DynamicalSystem(h=h)
|
|
203
|
+
>>> sys.step(tk=5.0)
|
|
204
|
+
10.0
|
|
205
|
+
|
|
206
|
+
Stateful system (with f):
|
|
207
|
+
|
|
208
|
+
>>> def f(x, u): return x + u
|
|
209
|
+
>>> def h(x, u): return x
|
|
210
|
+
>>> sys = DynamicalSystem(f=f, h=h)
|
|
211
|
+
>>> x_next, y = sys.step(x=0.0, u=1.0)
|
|
212
|
+
>>> x_next, y
|
|
213
|
+
(1.0, 0.0)
|
|
214
|
+
"""
|
|
215
|
+
if self.f is None:
|
|
216
|
+
return self._smart_call(self.h, **kwargs)
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
self._smart_call(self.f, **kwargs),
|
|
220
|
+
self._smart_call(self.h, **kwargs),
|
|
221
|
+
)
|