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.
@@ -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
+ )