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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamicalnodes
3
- Version: 0.1.2
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 -> Python -> ROS -> World
54
+ ## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
55
55
  <table>
56
56
  <tr>
57
57
  <td align="center">
58
- <img src="docs/source/_static/figure_turtlebot_pipeline/turtlebot.svg" alt="TurtleBot Overview" width="400"/><br/>
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/figure_turtlebot_pipeline/turtlesim_software.gif" alt="Turtle Software" width="200"/><br/>
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/figure_turtlebot_pipeline/turtlesim_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
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/figure_turtlebot_pipeline/turtlesim_hardware.gif" alt="Turtle Hardware" width="200"/><br/>
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/figure_turtlebot_pipeline/labels_for_steps_from_theory_to_hardware.png" alt="Step Labels" width="800"/>
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.2"
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 ``step()`` directly in Python or Jupyter notebooks.
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
- the generated file reads exactly like a hand-authored rclpy node and has no runtime
13
- dependency on dynamicalnodes beyond ``DynamicalSystem``.
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 ``step()`` for notebook simulation; use ``write_ROSNode_to_rclpy()`` to
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 step.
89
- subscribes_to : list of dict, optional
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 step().
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
- publishes_to : list of dict, optional
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``. ``step()`` injects ``self._state`` under this name before
125
- calling ``DynamicalSystem.step()``.
77
+ ``None``. ``eval()`` injects ``self._state`` under this name before
78
+ calling ``DynamicalSystem.eval()``.
126
79
 
127
- timer_hz : float, optional
128
- In the generated node: fires ``_run_step`` at this frequency instead
129
- of triggering from subscription callbacks.
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
- ... subscribes_to=[{"topic": "/raw", "msg_type": Float64,
144
- ... "arg": "imu", "ros2py": ros2py_float64}],
145
- ... publishes_to=[{"topic": "/smooth", "msg_type": Float64,
146
- ... "py2ros": py2ros_float64}],
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.step(imu=np.array([2.0]))
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
- subscribes_to: Optional[Sequence[SubDict]] = None,
159
- publishes_to: Optional[Sequence[PubDict]] = None,
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 subscribes_to or []:
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 publishes_to or []:
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 step(self, **kwargs: Any) -> Any:
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
- ... subscribes_to=[{"topic": "/x", "msg_type": Float64,
301
- ... "arg": "x", "ros2py": ros2py_float64}],
302
- ... publishes_to=[{"topic": "/y", "msg_type": Float64,
303
- ... "py2ros": py2ros_float64}],
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.step(x=Float64(data=3.0)).data
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
- result = self._dynamical_system.step(**step_kwargs)
270
+ x_next, yk = self._dynamical_system.eval(**step_kwargs)
332
271
  if self._dynamical_system.f is not None:
333
- self._state, yk = result
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 ``step()``
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
- ... subscribes_to=[{"topic": "/u_k", "msg_type": Float64,
472
- ... "arg": "u_k", "ros2py": ros2py_float64,
473
- ... "stale_after": 0.5}],
474
- ... publishes_to=[{"topic": "/y_k", "msg_type": Float64,
475
- ... "py2ros": py2ros_float64}],
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
- if not self._subs and self._timer_hz is None and self._pubs_cfg:
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 subscribes_to."
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 subscribes_to")
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 publishes_to")
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 self._timer_hz is not None:
867
- ln(f" step trigger: timer at {self._timer_hz} Hz")
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 self._sync_mode == "all"
836
+ if sync_mode == "all"
874
837
  else "step when any subscription receives a fresh value"
875
838
  )
876
- ln(f" sync_mode={self._sync_mode!r}: {sync_desc}.")
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 self._timer_hz is not None:
912
+ if timer_hz is not None:
950
913
  blank()
951
914
  ln(" # ── Timer " + "─" * 60)
952
- period = 1.0 / self._timer_hz
915
+ period = 1.0 / timer_hz
953
916
  ln(f" self._timer = self.create_timer(")
954
- ln(f" {period}, # {self._timer_hz} Hz")
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(f" arr = arr + np.random.normal(0.0, {std!r}, arr.shape) # noise std={std}")
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 self._timer_hz is None:
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.step() and")
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={self._sync_mode!r}")
1051
- if self._sync_mode == "all":
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(" result = self._system.step(**kwargs)")
1037
+ ln(" x_next, yk = self._system.eval(**kwargs)")
1073
1038
  ln(" if self._system.f is not None:")
1074
- ln(" self._state, yk = result # stateful: (next_state, output)")
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(f" _arr = np.asarray(yk[{key!r}], dtype=float).ravel()")
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(f" _arr = _arr + np.random.normal(0.0, {std!r}, _arr.shape) # noise std={std}")
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(f" _arr = _arr + np.random.normal(0.0, {std!r}, _arr.shape) # noise std={std}")
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.2
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 -> Python -> ROS -> World
54
+ ## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
55
55
  <table>
56
56
  <tr>
57
57
  <td align="center">
58
- <img src="docs/source/_static/figure_turtlebot_pipeline/turtlebot.svg" alt="TurtleBot Overview" width="400"/><br/>
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/figure_turtlebot_pipeline/turtlesim_software.gif" alt="Turtle Software" width="200"/><br/>
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/figure_turtlebot_pipeline/turtlesim_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
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/figure_turtlebot_pipeline/turtlesim_hardware.gif" alt="Turtle Hardware" width="200"/><br/>
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/figure_turtlebot_pipeline/labels_for_steps_from_theory_to_hardware.png" alt="Step Labels" width="800"/>
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
 
@@ -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