dynamicalnodes 0.1.2__tar.gz → 0.1.4__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.2/src/dynamicalnodes.egg-info → dynamicalnodes-0.1.4}/PKG-INFO +8 -8
- dynamicalnodes-0.1.4/README.md +38 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/pyproject.toml +1 -1
- dynamicalnodes-0.1.4/src/dynamicalnodes/dynamical_system.py +204 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes/rosnode.py +96 -127
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4/src/dynamicalnodes.egg-info}/PKG-INFO +8 -8
- dynamicalnodes-0.1.2/README.md +0 -38
- dynamicalnodes-0.1.2/src/dynamicalnodes/dynamical_system.py +0 -226
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/LICENSE +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/setup.cfg +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes/__init__.py +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes/ros2py_py2ros.py +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes.egg-info/SOURCES.txt +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes.egg-info/dependency_links.txt +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes.egg-info/requires.txt +0 -0
- {dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamicalnodes
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: dynamicalnodes is a Python library for modeling general control systems.
|
|
5
5
|
Author-email: Nehal Singh Mangat <nehalsinghmangat.software@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,33 +44,33 @@ Dynamic: license-file
|
|
|
44
44
|
|
|
45
45
|
> _"Give me a [control system] long enough and a [computer] on which to place it, and I shall move the world."_ - Archimedes
|
|
46
46
|
|
|
47
|
-
**dynamicalnodes** is a Python development framework that bridges the gap between control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
47
|
+
**dynamicalnodes** is a Python development framework that bridges the gap between novel control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
48
48
|
|
|
49
49
|
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://nehalsinghmangat.github.io/dynamicalnodes).
|
|
50
50
|
|
|
51
51
|
For a 22 second video describing this framework's intended workflow for, click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
52
52
|
|
|
53
53
|
---
|
|
54
|
-
## Turtlebot: Whiteboard
|
|
54
|
+
## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
|
|
55
55
|
<table>
|
|
56
56
|
<tr>
|
|
57
57
|
<td align="center">
|
|
58
|
-
<img src="docs/source/_static/
|
|
58
|
+
<img src="docs/source/_static/figures/turtlebot_schematic.svg" alt="TurtleBot Schematic" width="400"/><br/>
|
|
59
59
|
</td>
|
|
60
60
|
<td align="center">
|
|
61
|
-
<img src="docs/source/_static/
|
|
61
|
+
<img src="docs/source/_static/figures/turtlebot_python_simulation.gif" alt="Turtle Software" width="200"/><br/>
|
|
62
62
|
</td>
|
|
63
63
|
<td align="center">
|
|
64
|
-
<img src="docs/source/_static/
|
|
64
|
+
<img src="docs/source/_static/figures/turtlebot_ros_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
65
65
|
</td>
|
|
66
66
|
<td align="center">
|
|
67
|
-
<img src="docs/source/_static/
|
|
67
|
+
<img src="docs/source/_static/figures/turtlebot_mocab.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
68
68
|
</td>
|
|
69
69
|
</tr>
|
|
70
70
|
</table>
|
|
71
71
|
|
|
72
72
|
<p align="center">
|
|
73
|
-
<img src="docs/source/_static/
|
|
73
|
+
<img src="docs/source/_static/figures/pipeline_theory_python_ros_hardware.png" alt="Step Labels" width="800"/>
|
|
74
74
|
</p>
|
|
75
75
|
|
|
76
76
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# dynamicalnodes: Control Systems on ROS 2
|
|
2
|
+
|
|
3
|
+
> _"Give me a [control system] long enough and a [computer] on which to place it, and I shall move the world."_ - Archimedes
|
|
4
|
+
|
|
5
|
+
**dynamicalnodes** is a Python development framework that bridges the gap between novel control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
6
|
+
|
|
7
|
+
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://nehalsinghmangat.github.io/dynamicalnodes).
|
|
8
|
+
|
|
9
|
+
For a 22 second video describing this framework's intended workflow for, click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
|
|
13
|
+
<table>
|
|
14
|
+
<tr>
|
|
15
|
+
<td align="center">
|
|
16
|
+
<img src="docs/source/_static/figures/turtlebot_schematic.svg" alt="TurtleBot Schematic" width="400"/><br/>
|
|
17
|
+
</td>
|
|
18
|
+
<td align="center">
|
|
19
|
+
<img src="docs/source/_static/figures/turtlebot_python_simulation.gif" alt="Turtle Software" width="200"/><br/>
|
|
20
|
+
</td>
|
|
21
|
+
<td align="center">
|
|
22
|
+
<img src="docs/source/_static/figures/turtlebot_ros_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
23
|
+
</td>
|
|
24
|
+
<td align="center">
|
|
25
|
+
<img src="docs/source/_static/figures/turtlebot_mocab.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
</table>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="docs/source/_static/figures/pipeline_theory_python_ros_hardware.png" alt="Step Labels" width="800"/>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "dynamicalnodes"
|
|
10
|
-
version = "0.1.
|
|
10
|
+
version = "0.1.4"
|
|
11
11
|
description = "dynamicalnodes is a Python library for modeling general control systems."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A discrete-time dynamical system (DTDS) is represented as a pair of functions,
|
|
3
|
+
``(f, h)``, following the definition in Mangat et al., *dynamicalnodes: From
|
|
4
|
+
Theory to Python to ROS*::
|
|
5
|
+
|
|
6
|
+
x_{k+1} = f(x_k, u_k, theta)
|
|
7
|
+
y_k = h(x_k, u_k, theta)
|
|
8
|
+
|
|
9
|
+
``f`` is the evolution function that propagates the state forward one step;
|
|
10
|
+
``h`` is the measurement (or output) function that computes an observation
|
|
11
|
+
from the current state. Either function may be omitted -- denoted ``*`` in
|
|
12
|
+
the paper, ``None`` here -- giving three cases:
|
|
13
|
+
|
|
14
|
+
- ``(f, *)`` -- no output function.
|
|
15
|
+
- ``(*, h)`` -- stateless, e.g. a signal generator.
|
|
16
|
+
- ``(f, h)`` -- both state evolution and output are defined.
|
|
17
|
+
|
|
18
|
+
Function signatures are unconstrained in name, type, and number of
|
|
19
|
+
parameters. ``DynamicalSystem.eval`` binds keyword arguments to each
|
|
20
|
+
function's signature by name, so the same keyword argument (e.g. a shared
|
|
21
|
+
state or parameter) can be passed to both ``f`` and ``h``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import inspect
|
|
25
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DynamicalSystem:
|
|
29
|
+
"""
|
|
30
|
+
A discrete-time dynamical system with (optional) evolution and
|
|
31
|
+
(optional) measurement functions.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
f : Callable, optional
|
|
36
|
+
Evolution function ``f(x_k, u_k, theta) -> x_{k+1}``. ``None`` if
|
|
37
|
+
the system has no state transition, i.e. ``(*, h)``.
|
|
38
|
+
h : Callable, optional
|
|
39
|
+
Measurement function ``h(x_k, u_k, theta) -> y_k``. ``None`` if
|
|
40
|
+
the system has no output, i.e. ``(f, *)``.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
A stateful system with both an evolution and a measurement function --
|
|
45
|
+
a car whose state is ``(displacement, velocity)``, measured via its
|
|
46
|
+
speed:
|
|
47
|
+
|
|
48
|
+
>>> def fcar(xk, uk, theta_car):
|
|
49
|
+
... d, v = xk
|
|
50
|
+
... m, b, dt = theta_car
|
|
51
|
+
... return (d + dt * v, v + dt * (uk - b * v) / m)
|
|
52
|
+
>>> def hcar(xk):
|
|
53
|
+
... _, v = xk
|
|
54
|
+
... return abs(v)
|
|
55
|
+
>>> car = DynamicalSystem(f=fcar, h=hcar)
|
|
56
|
+
>>> x_next, y = car.eval(xk=(20.0, 0.0), uk=4.0, theta_car=(2.0, 0.0, 1.0))
|
|
57
|
+
>>> x_next
|
|
58
|
+
(20.0, 2.0)
|
|
59
|
+
>>> y
|
|
60
|
+
0.0
|
|
61
|
+
|
|
62
|
+
A stateless system, e.g. a signal generator -- ``(*, h)``:
|
|
63
|
+
|
|
64
|
+
>>> def h_ref(tk, A=1.0):
|
|
65
|
+
... return A if tk >= 1.0 else 0.0
|
|
66
|
+
>>> ref = DynamicalSystem(h=h_ref)
|
|
67
|
+
>>> ref.eval(tk=0.0)
|
|
68
|
+
(None, 0.0)
|
|
69
|
+
>>> ref.eval(tk=2.0)
|
|
70
|
+
(None, 1.0)
|
|
71
|
+
|
|
72
|
+
A system with no output function -- ``(f, *)``:
|
|
73
|
+
|
|
74
|
+
>>> def f_counter(xk):
|
|
75
|
+
... return xk + 1
|
|
76
|
+
>>> counter = DynamicalSystem(f=f_counter)
|
|
77
|
+
>>> counter.eval(xk=5)
|
|
78
|
+
(6, None)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
f: Optional[Callable] = None,
|
|
85
|
+
h: Optional[Callable] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
f : Callable, optional
|
|
91
|
+
Evolution function ``f(x_k, u_k, theta) -> x_{k+1}``. Signature
|
|
92
|
+
typing is not enforced. ``None`` if the system is stateless.
|
|
93
|
+
h : Callable, optional
|
|
94
|
+
Measurement function ``h(x_k, u_k, theta) -> y_k``. Signature
|
|
95
|
+
typing is not enforced. ``None`` if the system has no output.
|
|
96
|
+
"""
|
|
97
|
+
self._f = f
|
|
98
|
+
self._h = h
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def f(self) -> Optional[Callable]:
|
|
102
|
+
"""Evolution function f(x_k, u_k, theta) -> x_{k+1}, or None if the system is stateless."""
|
|
103
|
+
return self._f
|
|
104
|
+
|
|
105
|
+
@f.setter
|
|
106
|
+
def f(self, f: Optional[Callable]) -> None:
|
|
107
|
+
self._f = f
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def h(self) -> Optional[Callable]:
|
|
111
|
+
"""Measurement function h(x_k, u_k, theta) -> y_k, or None if the system has no output."""
|
|
112
|
+
return self._h
|
|
113
|
+
|
|
114
|
+
@h.setter
|
|
115
|
+
def h(self, h: Optional[Callable]) -> None:
|
|
116
|
+
self._h = h
|
|
117
|
+
|
|
118
|
+
def eval(self, **kwargs) -> Tuple[Optional[Any], Optional[Any]]:
|
|
119
|
+
"""
|
|
120
|
+
Evaluate the system for one discrete time step.
|
|
121
|
+
|
|
122
|
+
Keyword arguments are lexically bound by name to whichever of ``f``
|
|
123
|
+
and/or ``h`` declare them in their signature (see
|
|
124
|
+
``_bind_and_call``), so the same keyword argument can be shared by
|
|
125
|
+
both functions.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
Tuple[Optional[Any], Optional[Any]]
|
|
130
|
+
- ``(None, y_k)`` for a stateless system, ``(*, h)``.
|
|
131
|
+
- ``(x_{k+1}, None)`` for a system with no output, ``(f, *)``.
|
|
132
|
+
- ``(x_{k+1}, y_k)`` for a fully specified system, ``(f, h)``.
|
|
133
|
+
|
|
134
|
+
Notes
|
|
135
|
+
-----
|
|
136
|
+
``h`` is evaluated on the same (pre-update) state passed in via
|
|
137
|
+
keyword arguments, not on the value returned by ``f``, so the
|
|
138
|
+
returned observation is one step behind the returned state. In a
|
|
139
|
+
simulation loop, use the returned ``x_{k+1}`` directly if you need
|
|
140
|
+
the post-update state.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
ValueError
|
|
145
|
+
If neither ``f`` nor ``h`` is defined.
|
|
146
|
+
|
|
147
|
+
Examples
|
|
148
|
+
--------
|
|
149
|
+
See the class docstring for worked examples covering all three
|
|
150
|
+
cases.
|
|
151
|
+
|
|
152
|
+
>>> empty = DynamicalSystem()
|
|
153
|
+
>>> empty.eval()
|
|
154
|
+
Traceback (most recent call last):
|
|
155
|
+
...
|
|
156
|
+
ValueError: Nothing to evaluate; no f or h functions!
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def _bind_and_call(
|
|
160
|
+
func: Callable[..., Any],
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
Call `func` with only the keyword arguments it declares.
|
|
165
|
+
|
|
166
|
+
Inspects `func`'s signature and passes only the entries of
|
|
167
|
+
`kwargs` matching its declared parameter names; if `func`
|
|
168
|
+
accepts `**kwargs`, the remaining pool is passed through too.
|
|
169
|
+
"""
|
|
170
|
+
sig = inspect.signature(func)
|
|
171
|
+
pool: Dict[str, Any] = dict(kwargs)
|
|
172
|
+
|
|
173
|
+
# Filter: only pass arguments the function declares
|
|
174
|
+
call_kwargs: Dict[str, Any] = {}
|
|
175
|
+
for name, p in sig.parameters.items():
|
|
176
|
+
if p.kind in (
|
|
177
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
178
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
179
|
+
):
|
|
180
|
+
if name in pool:
|
|
181
|
+
call_kwargs[name] = pool.pop(name)
|
|
182
|
+
|
|
183
|
+
# Pass remaining kwargs if function has **kwargs
|
|
184
|
+
if any(
|
|
185
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
186
|
+
):
|
|
187
|
+
call_kwargs.update(pool)
|
|
188
|
+
|
|
189
|
+
return func(**call_kwargs)
|
|
190
|
+
|
|
191
|
+
if self.f is None and self.h:
|
|
192
|
+
return (None, _bind_and_call(self.h, **kwargs))
|
|
193
|
+
|
|
194
|
+
elif self.f and self.h is None:
|
|
195
|
+
return (_bind_and_call(self.f, **kwargs), None)
|
|
196
|
+
|
|
197
|
+
elif self.f and self.h:
|
|
198
|
+
return (
|
|
199
|
+
_bind_and_call(self.f, **kwargs),
|
|
200
|
+
_bind_and_call(self.h, **kwargs),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError("Nothing to evaluate; no f or h functions!")
|
|
@@ -3,57 +3,14 @@ ROSNode wrapper for simulation and ROS2 deployment.
|
|
|
3
3
|
|
|
4
4
|
Two workflows:
|
|
5
5
|
|
|
6
|
-
**Simulation** — call ``
|
|
6
|
+
**Simulation** — call ``eval()`` directly in Python or Jupyter notebooks to test ros2py and/or py2ros translation functions.
|
|
7
7
|
No ROS2 installation required.
|
|
8
8
|
|
|
9
9
|
**Deployment** — call ``write_ROSNode_to_rclpy(path)`` to emit a self-contained
|
|
10
10
|
``.py`` file that spins up a native rclpy node. Every ROS2 detail (QoS profiles,
|
|
11
|
-
buffer depths, subscription callbacks, executor setup) is written out explicitly
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Example
|
|
16
|
-
-------
|
|
17
|
-
Develop and simulate in a notebook, then deploy:
|
|
18
|
-
|
|
19
|
-
>>> import numpy as np
|
|
20
|
-
>>> from dynamicalnodes import DynamicalSystem, ROSNode
|
|
21
|
-
>>> from std_msgs.msg import Float64
|
|
22
|
-
>>> from dynamicalnodes.ros2py_py2ros import ros2py_float64, py2ros_float64
|
|
23
|
-
>>>
|
|
24
|
-
>>> def f_plant(xk, u_k):
|
|
25
|
-
... return xk + 0.1 * u_k
|
|
26
|
-
>>> def h_plant(xk):
|
|
27
|
-
... return xk
|
|
28
|
-
>>>
|
|
29
|
-
>>> sys = DynamicalSystem(f=f_plant, h=h_plant)
|
|
30
|
-
>>> node = ROSNode(
|
|
31
|
-
... dynamical_system=sys,
|
|
32
|
-
... subscribes_to=[
|
|
33
|
-
... {"topic": "/u_k", "msg_type": Float64, "arg": "u_k",
|
|
34
|
-
... "ros2py": ros2py_float64, "stale_after": 0.5},
|
|
35
|
-
... ],
|
|
36
|
-
... publishes_to=[
|
|
37
|
-
... {"topic": "/y_k", "msg_type": Float64, "py2ros": py2ros_float64},
|
|
38
|
-
... ],
|
|
39
|
-
... state_name="xk",
|
|
40
|
-
... )
|
|
41
|
-
>>>
|
|
42
|
-
>>> # Simulate
|
|
43
|
-
>>> node._state = np.array([0.0])
|
|
44
|
-
>>> for k in range(3):
|
|
45
|
-
... y = node.step(u_k=np.array([1.0]))
|
|
46
|
-
... print(f"k={k}: x={node.state}")
|
|
47
|
-
k=0: x=[0.1]
|
|
48
|
-
k=1: x=[0.2]
|
|
49
|
-
k=2: x=[0.3]
|
|
50
|
-
>>>
|
|
51
|
-
>>> # Deploy — writes a standalone rclpy node to disk
|
|
52
|
-
>>> import tempfile, os
|
|
53
|
-
>>> with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
54
|
-
... path = f.name
|
|
55
|
-
>>> node.write_ROSNode_to_rclpy(path, node_name="plant", initial_state=np.array([0.0]))
|
|
56
|
-
>>> os.unlink(path)
|
|
11
|
+
buffer depths, subscription callbacks, executor setup, etc...) is written out explicitly and
|
|
12
|
+
can be modified as needed. The ``.py`` file also has no runtime dependency on dynamicalnodes beyond ``DynamicalSystem`` and whatever dependencies the
|
|
13
|
+
f/h functions may have.
|
|
57
14
|
|
|
58
15
|
See Also
|
|
59
16
|
--------
|
|
@@ -79,21 +36,21 @@ class ROSNode:
|
|
|
79
36
|
"""
|
|
80
37
|
Wraps a DynamicalSystem for simulation and ROS2 deployment.
|
|
81
38
|
|
|
82
|
-
Use ``
|
|
39
|
+
Use ``eval()`` for notebook simulation; use ``write_ROSNode_to_rclpy()`` to
|
|
83
40
|
generate a deployable rclpy node.
|
|
84
41
|
|
|
85
42
|
Parameters
|
|
86
43
|
----------
|
|
87
44
|
dynamical_system : DynamicalSystem
|
|
88
|
-
The dynamical system to
|
|
89
|
-
|
|
45
|
+
The dynamical system to evaluate.
|
|
46
|
+
subs : list of dict, optional
|
|
90
47
|
Each dict:
|
|
91
48
|
|
|
92
49
|
- ``"topic"`` (str): ROS topic name.
|
|
93
50
|
- ``"msg_type"`` (type): ROS message class.
|
|
94
|
-
- ``"arg"`` (str): kwarg name passed to
|
|
51
|
+
- ``"arg"`` (str): kwarg name passed to eval().
|
|
95
52
|
- ``"ros2py"`` (Callable): ROS msg → NumPy array converter.
|
|
96
|
-
Import from ``dynamicalnodes.ros2py_py2ros
|
|
53
|
+
Import from ``dynamicalnodes.ros2py_py2ros`` or write your own.
|
|
97
54
|
- ``"stale_after"`` (float, optional): Seconds before data expires.
|
|
98
55
|
- ``"buffer_size"`` (int, optional): Queue depth (default 1).
|
|
99
56
|
- ``"use_msg_timestamp"`` (bool, optional): When ``True``, the
|
|
@@ -104,7 +61,7 @@ class ROSNode:
|
|
|
104
61
|
rather than network-arrival time (e.g. sensor fusion, bag
|
|
105
62
|
replay). Defaults to ``False``.
|
|
106
63
|
|
|
107
|
-
|
|
64
|
+
pubs : list of dict, optional
|
|
108
65
|
Each dict:
|
|
109
66
|
|
|
110
67
|
- ``"topic"`` (str): ROS topic name.
|
|
@@ -114,19 +71,17 @@ class ROSNode:
|
|
|
114
71
|
- ``"key"`` (str, optional): Key into h() return dict. Required when
|
|
115
72
|
h() returns a dict and there are multiple publishers.
|
|
116
73
|
|
|
117
|
-
sync_mode : str, required when ``subscribes_to`` has more than one entry
|
|
118
|
-
``"any"`` — step whenever any subscription has fresh data.
|
|
119
|
-
``"all"`` — step only when every subscription has fresh data.
|
|
120
|
-
|
|
121
74
|
state_name : str, optional
|
|
122
75
|
Name of the state parameter in ``f``'s signature (e.g. ``"ck"`` for
|
|
123
76
|
``f(ck, ...)``) . Required for stateful systems; ignored if ``f`` is
|
|
124
|
-
``None``. ``
|
|
125
|
-
calling ``DynamicalSystem.
|
|
77
|
+
``None``. ``eval()`` injects ``self._state`` under this name before
|
|
78
|
+
calling ``DynamicalSystem.eval()``.
|
|
126
79
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
80
|
+
Notes
|
|
81
|
+
-----
|
|
82
|
+
``sync_mode`` and ``timer_hz`` are not constructor parameters — they only
|
|
83
|
+
affect the generated ROS2 node's triggering behavior, so they're passed to
|
|
84
|
+
``write_ROSNode_to_rclpy()`` instead.
|
|
130
85
|
|
|
131
86
|
Examples
|
|
132
87
|
--------
|
|
@@ -140,12 +95,12 @@ class ROSNode:
|
|
|
140
95
|
>>> smoother = DynamicalSystem(h=lambda imu: imu * 0.5)
|
|
141
96
|
>>> node = ROSNode(
|
|
142
97
|
... dynamical_system=smoother,
|
|
143
|
-
...
|
|
144
|
-
...
|
|
145
|
-
...
|
|
146
|
-
...
|
|
98
|
+
... subs=[{"topic": "/raw", "msg_type": Float64,
|
|
99
|
+
... "arg": "imu", "ros2py": ros2py_float64}],
|
|
100
|
+
... pubs=[{"topic": "/smooth", "msg_type": Float64,
|
|
101
|
+
... "py2ros": py2ros_float64}],
|
|
147
102
|
... )
|
|
148
|
-
>>> out = node.
|
|
103
|
+
>>> out = node.eval(imu=np.array([2.0]))
|
|
149
104
|
>>> out.data
|
|
150
105
|
1.0
|
|
151
106
|
"""
|
|
@@ -155,32 +110,16 @@ class ROSNode:
|
|
|
155
110
|
*,
|
|
156
111
|
dynamical_system: DynamicalSystem,
|
|
157
112
|
state_name: Optional[str] = None,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
sync_mode: Optional[str] = None,
|
|
161
|
-
timer_hz: Optional[float] = None,
|
|
113
|
+
subs: Optional[Sequence[SubDict]] = None,
|
|
114
|
+
pubs: Optional[Sequence[PubDict]] = None,
|
|
162
115
|
) -> None:
|
|
163
|
-
n_subs = len(subscribes_to) if subscribes_to else 0
|
|
164
|
-
if n_subs > 1 and sync_mode is None:
|
|
165
|
-
raise ValueError(
|
|
166
|
-
"sync_mode ('any' or 'all') is required when subscribes_to has "
|
|
167
|
-
"more than one subscription"
|
|
168
|
-
)
|
|
169
|
-
sync_mode = sync_mode or "any"
|
|
170
|
-
if sync_mode not in ("any", "all"):
|
|
171
|
-
raise ValueError(f"sync_mode must be 'any' or 'all', got: {sync_mode!r}")
|
|
172
|
-
if timer_hz is not None and timer_hz <= 0:
|
|
173
|
-
raise ValueError(f"timer_hz must be positive, got {timer_hz}")
|
|
174
|
-
|
|
175
116
|
self._dynamical_system = dynamical_system
|
|
176
117
|
self._state: Optional[Any] = None
|
|
177
118
|
self._state_key: Optional[str] = state_name
|
|
178
|
-
self._sync_mode = sync_mode
|
|
179
|
-
self._timer_hz = timer_hz
|
|
180
119
|
|
|
181
120
|
# -------- Parse subscriptions --------
|
|
182
121
|
self._subs: List[Tuple[str, type, Any, str, Optional[float], int, bool]] = []
|
|
183
|
-
for sub_dict in
|
|
122
|
+
for sub_dict in subs or []:
|
|
184
123
|
if not isinstance(sub_dict, dict):
|
|
185
124
|
raise TypeError("Each subscription must be a dict")
|
|
186
125
|
|
|
@@ -233,7 +172,7 @@ class ROSNode:
|
|
|
233
172
|
|
|
234
173
|
# -------- Parse publications --------
|
|
235
174
|
self._pubs_cfg: List[Tuple[str, type, Any, Optional[str]]] = []
|
|
236
|
-
for pub_dict in
|
|
175
|
+
for pub_dict in pubs or []:
|
|
237
176
|
if not isinstance(pub_dict, dict):
|
|
238
177
|
raise TypeError("Each publication must be a dict")
|
|
239
178
|
|
|
@@ -265,7 +204,7 @@ class ROSNode:
|
|
|
265
204
|
# SIMULATION API
|
|
266
205
|
# --------------------------------------------------------------------------
|
|
267
206
|
|
|
268
|
-
def
|
|
207
|
+
def eval(self, **kwargs: Any) -> Any:
|
|
269
208
|
"""
|
|
270
209
|
Simulate one step without ROS2.
|
|
271
210
|
|
|
@@ -297,12 +236,12 @@ class ROSNode:
|
|
|
297
236
|
>>> double = DynamicalSystem(h=lambda x: x * 2)
|
|
298
237
|
>>> node = ROSNode(
|
|
299
238
|
... dynamical_system=double,
|
|
300
|
-
...
|
|
301
|
-
...
|
|
302
|
-
...
|
|
303
|
-
...
|
|
239
|
+
... subs=[{"topic": "/x", "msg_type": Float64,
|
|
240
|
+
... "arg": "x", "ros2py": ros2py_float64}],
|
|
241
|
+
... pubs=[{"topic": "/y", "msg_type": Float64,
|
|
242
|
+
... "py2ros": py2ros_float64}],
|
|
304
243
|
... )
|
|
305
|
-
>>> node.
|
|
244
|
+
>>> node.eval(x=Float64(data=3.0)).data
|
|
306
245
|
6.0
|
|
307
246
|
"""
|
|
308
247
|
sub_arg_names = {a for _, _, _, a, _, _, _ in self._subs}
|
|
@@ -328,11 +267,9 @@ class ROSNode:
|
|
|
328
267
|
if self._state is not None and self._state_key is not None:
|
|
329
268
|
step_kwargs[self._state_key] = self._state
|
|
330
269
|
|
|
331
|
-
|
|
270
|
+
x_next, yk = self._dynamical_system.eval(**step_kwargs)
|
|
332
271
|
if self._dynamical_system.f is not None:
|
|
333
|
-
self._state
|
|
334
|
-
else:
|
|
335
|
-
yk = result
|
|
272
|
+
self._state = x_next
|
|
336
273
|
|
|
337
274
|
if not self._pubs_cfg:
|
|
338
275
|
return yk
|
|
@@ -365,6 +302,8 @@ class ROSNode:
|
|
|
365
302
|
path: str,
|
|
366
303
|
*,
|
|
367
304
|
node_name: str,
|
|
305
|
+
sync_mode: Optional[str] = None,
|
|
306
|
+
timer_hz: Optional[float] = None,
|
|
368
307
|
deps: Optional[List[Any]] = None,
|
|
369
308
|
initial_state: Optional[Any] = None,
|
|
370
309
|
initial_inputs: Optional[Dict[str, Any]] = None,
|
|
@@ -388,6 +327,13 @@ class ROSNode:
|
|
|
388
327
|
node_name : str
|
|
389
328
|
ROS2 node name (used for the generated class name, ``super().__init__``,
|
|
390
329
|
and logger messages).
|
|
330
|
+
sync_mode : str, optional
|
|
331
|
+
Required when there is more than one subscription. ``"any"`` —
|
|
332
|
+
step whenever any subscription has fresh data. ``"all"`` — step
|
|
333
|
+
only when every subscription has fresh data.
|
|
334
|
+
timer_hz : float, optional
|
|
335
|
+
Fires ``_run_step`` at this frequency instead of triggering from
|
|
336
|
+
subscription callbacks.
|
|
391
337
|
deps : list of callable, optional
|
|
392
338
|
Helper functions that ``f`` or ``h`` depend on. Each function's
|
|
393
339
|
source is inlined before the dynamics functions in the output file.
|
|
@@ -398,7 +344,7 @@ class ROSNode:
|
|
|
398
344
|
``__init__``. Supports NumPy arrays and any ``repr()``-able value.
|
|
399
345
|
static_params : dict, optional
|
|
400
346
|
Parameters baked into the node at generation time. Embedded as
|
|
401
|
-
``self._static_params = {...}`` and merged into every ``
|
|
347
|
+
``self._static_params = {...}`` and merged into every ``eval()``
|
|
402
348
|
call. Use for values that never change after deployment.
|
|
403
349
|
dynamic_params : dict, optional
|
|
404
350
|
Parameters declared on the ROS2 parameter server with their
|
|
@@ -434,6 +380,10 @@ class ROSNode:
|
|
|
434
380
|
------
|
|
435
381
|
ValueError
|
|
436
382
|
If any function is a lambda (cannot be serialized).
|
|
383
|
+
ValueError
|
|
384
|
+
If there is more than one subscription and ``sync_mode`` is not
|
|
385
|
+
given, or ``sync_mode`` is not ``"any"``/``"all"``, or
|
|
386
|
+
``timer_hz`` is not positive.
|
|
437
387
|
ValueError
|
|
438
388
|
If the node has no subscriptions and no ``timer_hz`` — the
|
|
439
389
|
generated node would never step.
|
|
@@ -446,7 +396,8 @@ class ROSNode:
|
|
|
446
396
|
|
|
447
397
|
Notes
|
|
448
398
|
-----
|
|
449
|
-
**Function resolution
|
|
399
|
+
**Function resolution**
|
|
400
|
+
|
|
450
401
|
Functions from ``dynamicalnodes.ros2py_py2ros`` are emitted as import
|
|
451
402
|
statements. Functions from any other importable module (not
|
|
452
403
|
``__main__`` or an IPython cell) are also imported by name. All other
|
|
@@ -468,11 +419,11 @@ class ROSNode:
|
|
|
468
419
|
>>> sys = DynamicalSystem(f=f_plant, h=h_plant)
|
|
469
420
|
>>> node = ROSNode(
|
|
470
421
|
... dynamical_system=sys,
|
|
471
|
-
...
|
|
472
|
-
...
|
|
473
|
-
...
|
|
474
|
-
...
|
|
475
|
-
...
|
|
422
|
+
... subs=[{"topic": "/u_k", "msg_type": Float64,
|
|
423
|
+
... "arg": "u_k", "ros2py": ros2py_float64,
|
|
424
|
+
... "stale_after": 0.5}],
|
|
425
|
+
... pubs=[{"topic": "/y_k", "msg_type": Float64,
|
|
426
|
+
... "py2ros": py2ros_float64}],
|
|
476
427
|
... )
|
|
477
428
|
>>> with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as tmp:
|
|
478
429
|
... path = tmp.name
|
|
@@ -616,10 +567,22 @@ class ROSNode:
|
|
|
616
567
|
|
|
617
568
|
# ── validate ──────────────────────────────────────────────────────────
|
|
618
569
|
|
|
619
|
-
|
|
570
|
+
n_subs = len(self._subs)
|
|
571
|
+
if n_subs > 1 and sync_mode is None:
|
|
572
|
+
raise ValueError(
|
|
573
|
+
"sync_mode ('any' or 'all') is required when subs has "
|
|
574
|
+
"more than one subscription"
|
|
575
|
+
)
|
|
576
|
+
sync_mode = sync_mode or "any"
|
|
577
|
+
if sync_mode not in ("any", "all"):
|
|
578
|
+
raise ValueError(f"sync_mode must be 'any' or 'all', got: {sync_mode!r}")
|
|
579
|
+
if timer_hz is not None and timer_hz <= 0:
|
|
580
|
+
raise ValueError(f"timer_hz must be positive, got {timer_hz}")
|
|
581
|
+
|
|
582
|
+
if not self._subs and timer_hz is None and self._pubs_cfg:
|
|
620
583
|
raise ValueError(
|
|
621
584
|
"Cannot write a node with no subscriptions and no timer_hz — "
|
|
622
|
-
"the generated node would never step. Add timer_hz or
|
|
585
|
+
"the generated node would never step. Add timer_hz or subs."
|
|
623
586
|
)
|
|
624
587
|
|
|
625
588
|
ds = self._dynamical_system
|
|
@@ -636,10 +599,10 @@ class ROSNode:
|
|
|
636
599
|
_valid_pub_topics = {t for t, *_ in self._pubs_cfg}
|
|
637
600
|
for t in _sub_noise:
|
|
638
601
|
if t not in _valid_sub_topics:
|
|
639
|
-
raise ValueError(f"sub_noise topic {t!r} not in
|
|
602
|
+
raise ValueError(f"sub_noise topic {t!r} not in subs")
|
|
640
603
|
for t in _pub_noise:
|
|
641
604
|
if t not in _valid_pub_topics:
|
|
642
|
-
raise ValueError(f"pub_noise topic {t!r} not in
|
|
605
|
+
raise ValueError(f"pub_noise topic {t!r} not in pubs")
|
|
643
606
|
|
|
644
607
|
# ── classify all functions ─────────────────────────────────────────────
|
|
645
608
|
|
|
@@ -863,17 +826,17 @@ class ROSNode:
|
|
|
863
826
|
f" {topic} ({msg_type.__name__}) [{_ref_name(py2ros, py2ros_cls)}{key_str}]"
|
|
864
827
|
)
|
|
865
828
|
ln("")
|
|
866
|
-
if
|
|
867
|
-
ln(f" step trigger: timer at {
|
|
829
|
+
if timer_hz is not None:
|
|
830
|
+
ln(f" step trigger: timer at {timer_hz} Hz")
|
|
868
831
|
else:
|
|
869
832
|
ln(" step trigger: event-driven (fires on each subscription callback)")
|
|
870
833
|
if subs:
|
|
871
834
|
sync_desc = (
|
|
872
835
|
"step only when every subscription has a fresh value"
|
|
873
|
-
if
|
|
836
|
+
if sync_mode == "all"
|
|
874
837
|
else "step when any subscription receives a fresh value"
|
|
875
838
|
)
|
|
876
|
-
ln(f" sync_mode={
|
|
839
|
+
ln(f" sync_mode={sync_mode!r}: {sync_desc}.")
|
|
877
840
|
_dynamic = dict(dynamic_params or {})
|
|
878
841
|
if _dynamic:
|
|
879
842
|
ln("")
|
|
@@ -946,12 +909,12 @@ class ROSNode:
|
|
|
946
909
|
ln(f" {msg_type.__name__}, {topic!r}, QOS,")
|
|
947
910
|
ln(f" )")
|
|
948
911
|
|
|
949
|
-
if
|
|
912
|
+
if timer_hz is not None:
|
|
950
913
|
blank()
|
|
951
914
|
ln(" # ── Timer " + "─" * 60)
|
|
952
|
-
period = 1.0 /
|
|
915
|
+
period = 1.0 / timer_hz
|
|
953
916
|
ln(f" self._timer = self.create_timer(")
|
|
954
|
-
ln(f" {period}, # {
|
|
917
|
+
ln(f" {period}, # {timer_hz} Hz")
|
|
955
918
|
ln(f" self._run_step,")
|
|
956
919
|
ln(f" callback_group=self._cb_group,")
|
|
957
920
|
ln(f" )")
|
|
@@ -984,7 +947,9 @@ class ROSNode:
|
|
|
984
947
|
)
|
|
985
948
|
if topic in _sub_noise:
|
|
986
949
|
std = _sub_noise[topic]
|
|
987
|
-
ln(
|
|
950
|
+
ln(
|
|
951
|
+
f" arr = arr + np.random.normal(0.0, {std!r}, arr.shape) # noise std={std}"
|
|
952
|
+
)
|
|
988
953
|
if use_msg_ts:
|
|
989
954
|
ln(
|
|
990
955
|
f" ts = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9"
|
|
@@ -992,7 +957,7 @@ class ROSNode:
|
|
|
992
957
|
else:
|
|
993
958
|
ln(f" ts = self.get_clock().now().nanoseconds * 1e-9")
|
|
994
959
|
ln(f" self._{arg}_buf.append((ts, arr))")
|
|
995
|
-
if
|
|
960
|
+
if timer_hz is None:
|
|
996
961
|
ln(f" self._run_step()")
|
|
997
962
|
|
|
998
963
|
# _fresh
|
|
@@ -1015,7 +980,7 @@ class ROSNode:
|
|
|
1015
980
|
ln(" # Control step")
|
|
1016
981
|
ln(" # " + "─" * 68)
|
|
1017
982
|
ln(" # Builds kwargs from static params, dynamic params, fresh subscription")
|
|
1018
|
-
ln(" # values, and current state, then calls DynamicalSystem.
|
|
983
|
+
ln(" # values, and current state, then calls DynamicalSystem.eval() and")
|
|
1019
984
|
ln(" # publishes. Skipped entirely when sync_mode conditions are not met.")
|
|
1020
985
|
blank()
|
|
1021
986
|
ln(" def _run_step(self) -> None:")
|
|
@@ -1047,8 +1012,8 @@ class ROSNode:
|
|
|
1047
1012
|
)
|
|
1048
1013
|
|
|
1049
1014
|
blank()
|
|
1050
|
-
ln(f" # sync_mode={
|
|
1051
|
-
if
|
|
1015
|
+
ln(f" # sync_mode={sync_mode!r}")
|
|
1016
|
+
if sync_mode == "all":
|
|
1052
1017
|
cond = " or ".join(f"{a} is None" for _, _, _, _, a, _, _, _ in subs)
|
|
1053
1018
|
ln(f" if {cond}:")
|
|
1054
1019
|
ln(" return # not all inputs are fresh — skip this tick")
|
|
@@ -1069,11 +1034,9 @@ class ROSNode:
|
|
|
1069
1034
|
ln(f' kwargs["{self._state_key}"] = self._state')
|
|
1070
1035
|
|
|
1071
1036
|
blank()
|
|
1072
|
-
ln("
|
|
1037
|
+
ln(" x_next, yk = self._system.eval(**kwargs)")
|
|
1073
1038
|
ln(" if self._system.f is not None:")
|
|
1074
|
-
ln(" self._state
|
|
1075
|
-
ln(" else:")
|
|
1076
|
-
ln(" yk = result # stateless: output only")
|
|
1039
|
+
ln(" self._state = x_next # stateful: advance to next state")
|
|
1077
1040
|
|
|
1078
1041
|
if pubs:
|
|
1079
1042
|
blank()
|
|
@@ -1081,10 +1044,14 @@ class ROSNode:
|
|
|
1081
1044
|
py2ros_name = _ref_name(py2ros, cls)
|
|
1082
1045
|
if key is not None:
|
|
1083
1046
|
ln(f" if isinstance(yk, dict) and {key!r} in yk:")
|
|
1084
|
-
ln(
|
|
1047
|
+
ln(
|
|
1048
|
+
f" _arr = np.asarray(yk[{key!r}], dtype=float).ravel()"
|
|
1049
|
+
)
|
|
1085
1050
|
if topic in _pub_noise:
|
|
1086
1051
|
std = _pub_noise[topic]
|
|
1087
|
-
ln(
|
|
1052
|
+
ln(
|
|
1053
|
+
f" _arr = _arr + np.random.normal(0.0, {std!r}, _arr.shape) # noise std={std}"
|
|
1054
|
+
)
|
|
1088
1055
|
ln(f" msg = {py2ros_name}(_arr)")
|
|
1089
1056
|
ln(f" if hasattr(msg, 'header'):")
|
|
1090
1057
|
ln(
|
|
@@ -1095,7 +1062,9 @@ class ROSNode:
|
|
|
1095
1062
|
ln(f" _arr = np.asarray(yk, dtype=float).ravel()")
|
|
1096
1063
|
if topic in _pub_noise:
|
|
1097
1064
|
std = _pub_noise[topic]
|
|
1098
|
-
ln(
|
|
1065
|
+
ln(
|
|
1066
|
+
f" _arr = _arr + np.random.normal(0.0, {std!r}, _arr.shape) # noise std={std}"
|
|
1067
|
+
)
|
|
1099
1068
|
ln(f" msg = {py2ros_name}(_arr)")
|
|
1100
1069
|
ln(f" if hasattr(msg, 'header'):")
|
|
1101
1070
|
ln(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamicalnodes
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: dynamicalnodes is a Python library for modeling general control systems.
|
|
5
5
|
Author-email: Nehal Singh Mangat <nehalsinghmangat.software@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,33 +44,33 @@ Dynamic: license-file
|
|
|
44
44
|
|
|
45
45
|
> _"Give me a [control system] long enough and a [computer] on which to place it, and I shall move the world."_ - Archimedes
|
|
46
46
|
|
|
47
|
-
**dynamicalnodes** is a Python development framework that bridges the gap between control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
47
|
+
**dynamicalnodes** is a Python development framework that bridges the gap between novel control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
48
48
|
|
|
49
49
|
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://nehalsinghmangat.github.io/dynamicalnodes).
|
|
50
50
|
|
|
51
51
|
For a 22 second video describing this framework's intended workflow for, click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
52
52
|
|
|
53
53
|
---
|
|
54
|
-
## Turtlebot: Whiteboard
|
|
54
|
+
## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
|
|
55
55
|
<table>
|
|
56
56
|
<tr>
|
|
57
57
|
<td align="center">
|
|
58
|
-
<img src="docs/source/_static/
|
|
58
|
+
<img src="docs/source/_static/figures/turtlebot_schematic.svg" alt="TurtleBot Schematic" width="400"/><br/>
|
|
59
59
|
</td>
|
|
60
60
|
<td align="center">
|
|
61
|
-
<img src="docs/source/_static/
|
|
61
|
+
<img src="docs/source/_static/figures/turtlebot_python_simulation.gif" alt="Turtle Software" width="200"/><br/>
|
|
62
62
|
</td>
|
|
63
63
|
<td align="center">
|
|
64
|
-
<img src="docs/source/_static/
|
|
64
|
+
<img src="docs/source/_static/figures/turtlebot_ros_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
65
65
|
</td>
|
|
66
66
|
<td align="center">
|
|
67
|
-
<img src="docs/source/_static/
|
|
67
|
+
<img src="docs/source/_static/figures/turtlebot_mocab.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
68
68
|
</td>
|
|
69
69
|
</tr>
|
|
70
70
|
</table>
|
|
71
71
|
|
|
72
72
|
<p align="center">
|
|
73
|
-
<img src="docs/source/_static/
|
|
73
|
+
<img src="docs/source/_static/figures/pipeline_theory_python_ros_hardware.png" alt="Step Labels" width="800"/>
|
|
74
74
|
</p>
|
|
75
75
|
|
|
76
76
|
|
dynamicalnodes-0.1.2/README.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# dynamicalnodes: Control Systems on ROS 2
|
|
2
|
-
|
|
3
|
-
> _"Give me a [control system] long enough and a [computer] on which to place it, and I shall move the world."_ - Archimedes
|
|
4
|
-
|
|
5
|
-
**dynamicalnodes** is a Python development framework that bridges the gap between control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
6
|
-
|
|
7
|
-
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://nehalsinghmangat.github.io/dynamicalnodes).
|
|
8
|
-
|
|
9
|
-
For a 22 second video describing this framework's intended workflow for, click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
## Turtlebot: Whiteboard -> Python -> ROS -> World
|
|
13
|
-
<table>
|
|
14
|
-
<tr>
|
|
15
|
-
<td align="center">
|
|
16
|
-
<img src="docs/source/_static/figure_turtlebot_pipeline/turtlebot.svg" alt="TurtleBot Overview" width="400"/><br/>
|
|
17
|
-
</td>
|
|
18
|
-
<td align="center">
|
|
19
|
-
<img src="docs/source/_static/figure_turtlebot_pipeline/turtlesim_software.gif" alt="Turtle Software" width="200"/><br/>
|
|
20
|
-
</td>
|
|
21
|
-
<td align="center">
|
|
22
|
-
<img src="docs/source/_static/figure_turtlebot_pipeline/turtlesim_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
23
|
-
</td>
|
|
24
|
-
<td align="center">
|
|
25
|
-
<img src="docs/source/_static/figure_turtlebot_pipeline/turtlesim_hardware.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
26
|
-
</td>
|
|
27
|
-
</tr>
|
|
28
|
-
</table>
|
|
29
|
-
|
|
30
|
-
<p align="center">
|
|
31
|
-
<img src="docs/source/_static/figure_turtlebot_pipeline/labels_for_steps_from_theory_to_hardware.png" alt="Step Labels" width="800"/>
|
|
32
|
-
</p>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
@@ -1,226 +0,0 @@
|
|
|
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
|
-
A reserved key ``_state`` may be passed to inject the current
|
|
190
|
-
state without knowing ``f``'s first parameter name. It is
|
|
191
|
-
removed from kwargs before dispatch and inserted under the
|
|
192
|
-
correct name automatically.
|
|
193
|
-
|
|
194
|
-
Returns
|
|
195
|
-
-------
|
|
196
|
-
Any
|
|
197
|
-
If f is None (stateless system):
|
|
198
|
-
Returns y_k = h(...)
|
|
199
|
-
If f is defined (stateful system):
|
|
200
|
-
Returns (x_{k+1}, y_k) = (f(...), h(...))
|
|
201
|
-
|
|
202
|
-
Examples
|
|
203
|
-
--------
|
|
204
|
-
Stateless system (no f):
|
|
205
|
-
|
|
206
|
-
>>> def h(tk): return tk * 2
|
|
207
|
-
>>> sys = DynamicalSystem(h=h)
|
|
208
|
-
>>> sys.step(tk=5.0)
|
|
209
|
-
10.0
|
|
210
|
-
|
|
211
|
-
Stateful system (with f):
|
|
212
|
-
|
|
213
|
-
>>> def f(x, u): return x + u
|
|
214
|
-
>>> def h(x, u): return x
|
|
215
|
-
>>> sys = DynamicalSystem(f=f, h=h)
|
|
216
|
-
>>> x_next, y = sys.step(x=0.0, u=1.0)
|
|
217
|
-
>>> x_next, y
|
|
218
|
-
(1.0, 0.0)
|
|
219
|
-
"""
|
|
220
|
-
if self.f is None:
|
|
221
|
-
return self._smart_call(self.h, **kwargs)
|
|
222
|
-
|
|
223
|
-
return (
|
|
224
|
-
self._smart_call(self.f, **kwargs),
|
|
225
|
-
self._smart_call(self.h, **kwargs),
|
|
226
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamicalnodes-0.1.2 → dynamicalnodes-0.1.4}/src/dynamicalnodes.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|