dynamicalnodes 0.1__py3-none-any.whl
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/__init__.py +35 -0
- dynamicalnodes/dynamical_system.py +221 -0
- dynamicalnodes/ros2py_py2ros.py +1034 -0
- dynamicalnodes/rosnode.py +658 -0
- dynamicalnodes/rostools.py +372 -0
- dynamicalnodes-0.1.dist-info/METADATA +82 -0
- dynamicalnodes-0.1.dist-info/RECORD +10 -0
- dynamicalnodes-0.1.dist-info/WHEEL +5 -0
- dynamicalnodes-0.1.dist-info/licenses/LICENSE +21 -0
- dynamicalnodes-0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
)
|