dynamicalnodes 0.1.4__tar.gz → 0.2.0__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.4/src/dynamicalnodes.egg-info → dynamicalnodes-0.2.0}/PKG-INFO +1 -1
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/pyproject.toml +1 -1
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes/dynamical_system.py +27 -7
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes/rosnode.py +68 -32
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0/src/dynamicalnodes.egg-info}/PKG-INFO +1 -1
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/LICENSE +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/README.md +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/setup.cfg +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes/__init__.py +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes/ros2py_py2ros.py +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/SOURCES.txt +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/dependency_links.txt +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/requires.txt +0 -0
- {dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "dynamicalnodes"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.2.0"
|
|
11
11
|
description = "dynamicalnodes is a Python library for modeling general control systems."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -16,7 +16,7 @@ the paper, ``None`` here -- giving three cases:
|
|
|
16
16
|
- ``(f, h)`` -- both state evolution and output are defined.
|
|
17
17
|
|
|
18
18
|
Function signatures are unconstrained in name, type, and number of
|
|
19
|
-
parameters. ``DynamicalSystem.
|
|
19
|
+
parameters. ``DynamicalSystem.step`` binds keyword arguments to each
|
|
20
20
|
function's signature by name, so the same keyword argument (e.g. a shared
|
|
21
21
|
state or parameter) can be passed to both ``f`` and ``h``.
|
|
22
22
|
"""
|
|
@@ -53,7 +53,7 @@ class DynamicalSystem:
|
|
|
53
53
|
... _, v = xk
|
|
54
54
|
... return abs(v)
|
|
55
55
|
>>> car = DynamicalSystem(f=fcar, h=hcar)
|
|
56
|
-
>>> x_next, y = car.
|
|
56
|
+
>>> x_next, y = car.step(xk=(20.0, 0.0), uk=4.0, theta_car=(2.0, 0.0, 1.0))
|
|
57
57
|
>>> x_next
|
|
58
58
|
(20.0, 2.0)
|
|
59
59
|
>>> y
|
|
@@ -64,9 +64,9 @@ class DynamicalSystem:
|
|
|
64
64
|
>>> def h_ref(tk, A=1.0):
|
|
65
65
|
... return A if tk >= 1.0 else 0.0
|
|
66
66
|
>>> ref = DynamicalSystem(h=h_ref)
|
|
67
|
-
>>> ref.
|
|
67
|
+
>>> ref.step(tk=0.0)
|
|
68
68
|
(None, 0.0)
|
|
69
|
-
>>> ref.
|
|
69
|
+
>>> ref.step(tk=2.0)
|
|
70
70
|
(None, 1.0)
|
|
71
71
|
|
|
72
72
|
A system with no output function -- ``(f, *)``:
|
|
@@ -74,7 +74,7 @@ class DynamicalSystem:
|
|
|
74
74
|
>>> def f_counter(xk):
|
|
75
75
|
... return xk + 1
|
|
76
76
|
>>> counter = DynamicalSystem(f=f_counter)
|
|
77
|
-
>>> counter.
|
|
77
|
+
>>> counter.step(xk=5)
|
|
78
78
|
(6, None)
|
|
79
79
|
"""
|
|
80
80
|
|
|
@@ -97,6 +97,26 @@ class DynamicalSystem:
|
|
|
97
97
|
self._f = f
|
|
98
98
|
self._h = h
|
|
99
99
|
|
|
100
|
+
def __repr__(self) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Show the system as its ``(f, h)`` pair, using ``*`` for whichever
|
|
103
|
+
half is unset -- matching the paper's notation.
|
|
104
|
+
|
|
105
|
+
Examples
|
|
106
|
+
--------
|
|
107
|
+
>>> def fcar(xk): return xk
|
|
108
|
+
>>> def hcar(xk): return xk
|
|
109
|
+
>>> DynamicalSystem(f=fcar, h=hcar)
|
|
110
|
+
(fcar, hcar)
|
|
111
|
+
>>> DynamicalSystem(f=fcar)
|
|
112
|
+
(fcar, *)
|
|
113
|
+
>>> DynamicalSystem(h=hcar)
|
|
114
|
+
(*, hcar)
|
|
115
|
+
"""
|
|
116
|
+
f_name = self._f.__name__ if self._f is not None else "*"
|
|
117
|
+
h_name = self._h.__name__ if self._h is not None else "*"
|
|
118
|
+
return f"({f_name}, {h_name})"
|
|
119
|
+
|
|
100
120
|
@property
|
|
101
121
|
def f(self) -> Optional[Callable]:
|
|
102
122
|
"""Evolution function f(x_k, u_k, theta) -> x_{k+1}, or None if the system is stateless."""
|
|
@@ -115,7 +135,7 @@ class DynamicalSystem:
|
|
|
115
135
|
def h(self, h: Optional[Callable]) -> None:
|
|
116
136
|
self._h = h
|
|
117
137
|
|
|
118
|
-
def
|
|
138
|
+
def step(self, **kwargs) -> Tuple[Optional[Any], Optional[Any]]:
|
|
119
139
|
"""
|
|
120
140
|
Evaluate the system for one discrete time step.
|
|
121
141
|
|
|
@@ -150,7 +170,7 @@ class DynamicalSystem:
|
|
|
150
170
|
cases.
|
|
151
171
|
|
|
152
172
|
>>> empty = DynamicalSystem()
|
|
153
|
-
>>> empty.
|
|
173
|
+
>>> empty.step()
|
|
154
174
|
Traceback (most recent call last):
|
|
155
175
|
...
|
|
156
176
|
ValueError: Nothing to evaluate; no f or h functions!
|
|
@@ -3,7 +3,7 @@ ROSNode wrapper for simulation and ROS2 deployment.
|
|
|
3
3
|
|
|
4
4
|
Two workflows:
|
|
5
5
|
|
|
6
|
-
**Simulation** — call ``
|
|
6
|
+
**Simulation** — call ``step()`` 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
|
|
@@ -36,7 +36,7 @@ class ROSNode:
|
|
|
36
36
|
"""
|
|
37
37
|
Wraps a DynamicalSystem for simulation and ROS2 deployment.
|
|
38
38
|
|
|
39
|
-
Use ``
|
|
39
|
+
Use ``step()`` for notebook simulation; use ``write_ROSNode_to_rclpy()`` to
|
|
40
40
|
generate a deployable rclpy node.
|
|
41
41
|
|
|
42
42
|
Parameters
|
|
@@ -48,10 +48,9 @@ class ROSNode:
|
|
|
48
48
|
|
|
49
49
|
- ``"topic"`` (str): ROS topic name.
|
|
50
50
|
- ``"msg_type"`` (type): ROS message class.
|
|
51
|
-
- ``"arg"`` (str): kwarg name passed to
|
|
51
|
+
- ``"arg"`` (str): kwarg name passed to step().
|
|
52
52
|
- ``"ros2py"`` (Callable): ROS msg → NumPy array converter.
|
|
53
53
|
Import from ``dynamicalnodes.ros2py_py2ros`` or write your own.
|
|
54
|
-
- ``"stale_after"`` (float, optional): Seconds before data expires.
|
|
55
54
|
- ``"buffer_size"`` (int, optional): Queue depth (default 1).
|
|
56
55
|
- ``"use_msg_timestamp"`` (bool, optional): When ``True``, the
|
|
57
56
|
subscription buffer stores the message's own ``header.stamp``
|
|
@@ -74,14 +73,14 @@ class ROSNode:
|
|
|
74
73
|
state_name : str, optional
|
|
75
74
|
Name of the state parameter in ``f``'s signature (e.g. ``"ck"`` for
|
|
76
75
|
``f(ck, ...)``) . Required for stateful systems; ignored if ``f`` is
|
|
77
|
-
``None``. ``
|
|
78
|
-
calling ``DynamicalSystem.
|
|
76
|
+
``None``. ``step()`` injects ``self._state`` under this name before
|
|
77
|
+
calling ``DynamicalSystem.step()``.
|
|
79
78
|
|
|
80
79
|
Notes
|
|
81
80
|
-----
|
|
82
|
-
``sync_mode`` and ``
|
|
83
|
-
affect the generated ROS2 node's triggering
|
|
84
|
-
``write_ROSNode_to_rclpy()`` instead.
|
|
81
|
+
``sync_mode``, ``timer_hz``, and ``stale_after`` are not constructor
|
|
82
|
+
parameters — they only affect the generated ROS2 node's triggering
|
|
83
|
+
behavior, so they're passed to ``write_ROSNode_to_rclpy()`` instead.
|
|
85
84
|
|
|
86
85
|
Examples
|
|
87
86
|
--------
|
|
@@ -100,7 +99,7 @@ class ROSNode:
|
|
|
100
99
|
... pubs=[{"topic": "/smooth", "msg_type": Float64,
|
|
101
100
|
... "py2ros": py2ros_float64}],
|
|
102
101
|
... )
|
|
103
|
-
>>> out = node.
|
|
102
|
+
>>> out = node.step(imu=np.array([2.0]))
|
|
104
103
|
>>> out.data
|
|
105
104
|
1.0
|
|
106
105
|
"""
|
|
@@ -118,7 +117,7 @@ class ROSNode:
|
|
|
118
117
|
self._state_key: Optional[str] = state_name
|
|
119
118
|
|
|
120
119
|
# -------- Parse subscriptions --------
|
|
121
|
-
self._subs: List[Tuple[str, type, Any, str,
|
|
120
|
+
self._subs: List[Tuple[str, type, Any, str, int, bool]] = []
|
|
122
121
|
for sub_dict in subs or []:
|
|
123
122
|
if not isinstance(sub_dict, dict):
|
|
124
123
|
raise TypeError("Each subscription must be a dict")
|
|
@@ -127,7 +126,6 @@ class ROSNode:
|
|
|
127
126
|
msg_type = sub_dict.get("msg_type")
|
|
128
127
|
arg_name = sub_dict.get("arg")
|
|
129
128
|
ros2py = sub_dict.get("ros2py")
|
|
130
|
-
stale_after = sub_dict.get("stale_after")
|
|
131
129
|
buffer_size = sub_dict.get("buffer_size", 1)
|
|
132
130
|
use_msg_timestamp = bool(sub_dict.get("use_msg_timestamp", False))
|
|
133
131
|
|
|
@@ -159,7 +157,6 @@ class ROSNode:
|
|
|
159
157
|
msg_type,
|
|
160
158
|
ros2py,
|
|
161
159
|
arg_name,
|
|
162
|
-
float(stale_after) if stale_after else None,
|
|
163
160
|
int(buffer_size),
|
|
164
161
|
use_msg_timestamp,
|
|
165
162
|
)
|
|
@@ -204,7 +201,7 @@ class ROSNode:
|
|
|
204
201
|
# SIMULATION API
|
|
205
202
|
# --------------------------------------------------------------------------
|
|
206
203
|
|
|
207
|
-
def
|
|
204
|
+
def step(self, **kwargs: Any) -> Any:
|
|
208
205
|
"""
|
|
209
206
|
Simulate one step without ROS2.
|
|
210
207
|
|
|
@@ -241,10 +238,10 @@ class ROSNode:
|
|
|
241
238
|
... pubs=[{"topic": "/y", "msg_type": Float64,
|
|
242
239
|
... "py2ros": py2ros_float64}],
|
|
243
240
|
... )
|
|
244
|
-
>>> node.
|
|
241
|
+
>>> node.step(x=Float64(data=3.0)).data
|
|
245
242
|
6.0
|
|
246
243
|
"""
|
|
247
|
-
sub_arg_names = {a for _, _, _, a, _, _
|
|
244
|
+
sub_arg_names = {a for _, _, _, a, _, _ in self._subs}
|
|
248
245
|
step_kwargs: Dict[str, Any] = {}
|
|
249
246
|
|
|
250
247
|
for arg_name, value in kwargs.items():
|
|
@@ -252,7 +249,7 @@ class ROSNode:
|
|
|
252
249
|
continue
|
|
253
250
|
if arg_name in sub_arg_names:
|
|
254
251
|
ros2py = next(
|
|
255
|
-
(c for _, _, c, a, _, _
|
|
252
|
+
(c for _, _, c, a, _, _ in self._subs if a == arg_name), None
|
|
256
253
|
)
|
|
257
254
|
assert ros2py is not None
|
|
258
255
|
if hasattr(value, "__slots__") or hasattr(
|
|
@@ -267,7 +264,7 @@ class ROSNode:
|
|
|
267
264
|
if self._state is not None and self._state_key is not None:
|
|
268
265
|
step_kwargs[self._state_key] = self._state
|
|
269
266
|
|
|
270
|
-
x_next, yk = self._dynamical_system.
|
|
267
|
+
x_next, yk = self._dynamical_system.step(**step_kwargs)
|
|
271
268
|
if self._dynamical_system.f is not None:
|
|
272
269
|
self._state = x_next
|
|
273
270
|
|
|
@@ -304,6 +301,7 @@ class ROSNode:
|
|
|
304
301
|
node_name: str,
|
|
305
302
|
sync_mode: Optional[str] = None,
|
|
306
303
|
timer_hz: Optional[float] = None,
|
|
304
|
+
stale_after: Optional[Dict[str, float]] = None,
|
|
307
305
|
deps: Optional[List[Any]] = None,
|
|
308
306
|
initial_state: Optional[Any] = None,
|
|
309
307
|
initial_inputs: Optional[Dict[str, Any]] = None,
|
|
@@ -334,6 +332,15 @@ class ROSNode:
|
|
|
334
332
|
timer_hz : float, optional
|
|
335
333
|
Fires ``_run_step`` at this frequency instead of triggering from
|
|
336
334
|
subscription callbacks.
|
|
335
|
+
stale_after : dict, optional
|
|
336
|
+
Maximum age, in seconds, a buffered subscription value may have
|
|
337
|
+
before it's treated as stale — dropped to ``None``, the same as
|
|
338
|
+
an empty buffer, instead of being reused indefinitely. Keys must
|
|
339
|
+
match subscription ``arg`` names; a subscription not listed here
|
|
340
|
+
never expires. Use this to bound how long a value may go
|
|
341
|
+
unrefreshed before a step must skip (``sync_mode="all"``) or
|
|
342
|
+
ignore (``sync_mode="any"``) it.
|
|
343
|
+
Example: ``{"rk": 0.2, "xhatk": 0.2}``.
|
|
337
344
|
deps : list of callable, optional
|
|
338
345
|
Helper functions that ``f`` or ``h`` depend on. Each function's
|
|
339
346
|
source is inlined before the dynamics functions in the output file.
|
|
@@ -344,7 +351,7 @@ class ROSNode:
|
|
|
344
351
|
``__init__``. Supports NumPy arrays and any ``repr()``-able value.
|
|
345
352
|
static_params : dict, optional
|
|
346
353
|
Parameters baked into the node at generation time. Embedded as
|
|
347
|
-
``self._static_params = {...}`` and merged into every ``
|
|
354
|
+
``self._static_params = {...}`` and merged into every ``step()``
|
|
348
355
|
call. Use for values that never change after deployment.
|
|
349
356
|
dynamic_params : dict, optional
|
|
350
357
|
Parameters declared on the ROS2 parameter server with their
|
|
@@ -358,9 +365,9 @@ class ROSNode:
|
|
|
358
365
|
restarting the node (e.g. controller gains).
|
|
359
366
|
initial_inputs : dict, optional
|
|
360
367
|
Cold-start fallback values for subscription inputs. Keys must
|
|
361
|
-
match subscription ``arg`` names. A value is used only
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
match subscription ``arg`` names. A value is used only while no
|
|
369
|
+
message has ever arrived on that topic (consuming a buffered
|
|
370
|
+
value in a step does not count as "never arrived"). Once the first message arrives the
|
|
364
371
|
subscription value takes over permanently. If the subscription
|
|
365
372
|
later goes stale the step is skipped (``None`` is not replaced),
|
|
366
373
|
preventing silent operation on dead data. Use this to break
|
|
@@ -394,6 +401,11 @@ class ROSNode:
|
|
|
394
401
|
If the generated file contains invalid Python (indicates a bug
|
|
395
402
|
in the generator).
|
|
396
403
|
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
None
|
|
407
|
+
The generated node is written to ``path``.
|
|
408
|
+
|
|
397
409
|
Notes
|
|
398
410
|
-----
|
|
399
411
|
**Function resolution**
|
|
@@ -420,15 +432,15 @@ class ROSNode:
|
|
|
420
432
|
>>> node = ROSNode(
|
|
421
433
|
... dynamical_system=sys,
|
|
422
434
|
... subs=[{"topic": "/u_k", "msg_type": Float64,
|
|
423
|
-
... "arg": "u_k", "ros2py": ros2py_float64,
|
|
424
|
-
... "stale_after": 0.5}],
|
|
435
|
+
... "arg": "u_k", "ros2py": ros2py_float64}],
|
|
425
436
|
... pubs=[{"topic": "/y_k", "msg_type": Float64,
|
|
426
437
|
... "py2ros": py2ros_float64}],
|
|
427
438
|
... )
|
|
428
439
|
>>> with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as tmp:
|
|
429
440
|
... path = tmp.name
|
|
430
441
|
>>> node.write_ROSNode_to_rclpy(path, node_name="plant",
|
|
431
|
-
... initial_state=np.array([0.0])
|
|
442
|
+
... initial_state=np.array([0.0]),
|
|
443
|
+
... stale_after={"u_k": 0.5})
|
|
432
444
|
>>> content = open(path).read()
|
|
433
445
|
>>> "def f_plant" in content and "def h_plant" in content
|
|
434
446
|
True
|
|
@@ -438,6 +450,8 @@ class ROSNode:
|
|
|
438
450
|
True
|
|
439
451
|
>>> "SingleThreadedExecutor" in content
|
|
440
452
|
True
|
|
453
|
+
>>> "_u_k_seen" in content
|
|
454
|
+
True
|
|
441
455
|
>>> os.unlink(path)
|
|
442
456
|
"""
|
|
443
457
|
import importlib
|
|
@@ -588,21 +602,26 @@ class ROSNode:
|
|
|
588
602
|
ds = self._dynamical_system
|
|
589
603
|
_check_lambda(ds.f, "DynamicalSystem.f")
|
|
590
604
|
_check_lambda(ds.h, "DynamicalSystem.h")
|
|
591
|
-
for _, _, ros2py, arg, _, _
|
|
605
|
+
for _, _, ros2py, arg, _, _ in self._subs:
|
|
592
606
|
_check_lambda(ros2py, f"ros2py for '{arg}'")
|
|
593
607
|
for _, _, py2ros, key in self._pubs_cfg:
|
|
594
608
|
_check_lambda(py2ros, f"py2ros for key={key!r}")
|
|
595
609
|
|
|
596
610
|
_sub_noise: Dict[str, float] = dict(sub_noise or {})
|
|
597
611
|
_pub_noise: Dict[str, float] = dict(pub_noise or {})
|
|
612
|
+
_stale_after: Dict[str, float] = dict(stale_after or {})
|
|
598
613
|
_valid_sub_topics = {t for t, *_ in self._subs}
|
|
599
614
|
_valid_pub_topics = {t for t, *_ in self._pubs_cfg}
|
|
615
|
+
_valid_sub_args = {a for _, _, _, a, _, _ in self._subs}
|
|
600
616
|
for t in _sub_noise:
|
|
601
617
|
if t not in _valid_sub_topics:
|
|
602
618
|
raise ValueError(f"sub_noise topic {t!r} not in subs")
|
|
603
619
|
for t in _pub_noise:
|
|
604
620
|
if t not in _valid_pub_topics:
|
|
605
621
|
raise ValueError(f"pub_noise topic {t!r} not in pubs")
|
|
622
|
+
for a in _stale_after:
|
|
623
|
+
if a not in _valid_sub_args:
|
|
624
|
+
raise ValueError(f"stale_after key {a!r} not a subscription 'arg'")
|
|
606
625
|
|
|
607
626
|
# ── classify all functions ─────────────────────────────────────────────
|
|
608
627
|
|
|
@@ -611,7 +630,7 @@ class ROSNode:
|
|
|
611
630
|
|
|
612
631
|
# subs: [(topic, msg_type, ros2py, cls, arg, stale, buf, use_msg_ts), ...]
|
|
613
632
|
subs = []
|
|
614
|
-
for topic, msg_type, ros2py, arg,
|
|
633
|
+
for topic, msg_type, ros2py, arg, buf, use_msg_ts in self._subs:
|
|
615
634
|
subs.append(
|
|
616
635
|
(
|
|
617
636
|
topic,
|
|
@@ -619,7 +638,7 @@ class ROSNode:
|
|
|
619
638
|
ros2py,
|
|
620
639
|
_classify(ros2py),
|
|
621
640
|
arg,
|
|
622
|
-
|
|
641
|
+
_stale_after.get(arg),
|
|
623
642
|
buf,
|
|
624
643
|
use_msg_ts,
|
|
625
644
|
)
|
|
@@ -891,6 +910,7 @@ class ROSNode:
|
|
|
891
910
|
ln(" # ── Subscription buffers " + "─" * 47)
|
|
892
911
|
for _, _, _, _, arg, _, buf, _ in subs:
|
|
893
912
|
ln(f" self._{arg}_buf: deque = deque(maxlen={buf})")
|
|
913
|
+
ln(f" self._{arg}_seen: bool = False # any message ever received")
|
|
894
914
|
|
|
895
915
|
blank()
|
|
896
916
|
ln(" # ── Subscriptions " + "─" * 52)
|
|
@@ -937,7 +957,11 @@ class ROSNode:
|
|
|
937
957
|
ln(
|
|
938
958
|
" # enables the staleness check in _fresh(). _run_step() proceeds only when"
|
|
939
959
|
)
|
|
940
|
-
ln(
|
|
960
|
+
ln(
|
|
961
|
+
" # sync_mode conditions are satisfied, and pops each value it consumes —"
|
|
962
|
+
)
|
|
963
|
+
ln(" # so a stored value can trigger at most one step before it must be")
|
|
964
|
+
ln(" # refreshed by new data.")
|
|
941
965
|
for topic, msg_type, ros2py, cls, arg, _, _, use_msg_ts in subs:
|
|
942
966
|
ros2py_name = _ref_name(ros2py, cls)
|
|
943
967
|
blank()
|
|
@@ -957,6 +981,7 @@ class ROSNode:
|
|
|
957
981
|
else:
|
|
958
982
|
ln(f" ts = self.get_clock().now().nanoseconds * 1e-9")
|
|
959
983
|
ln(f" self._{arg}_buf.append((ts, arr))")
|
|
984
|
+
ln(f" self._{arg}_seen = True")
|
|
960
985
|
if timer_hz is None:
|
|
961
986
|
ln(f" self._run_step()")
|
|
962
987
|
|
|
@@ -980,7 +1005,7 @@ class ROSNode:
|
|
|
980
1005
|
ln(" # Control step")
|
|
981
1006
|
ln(" # " + "─" * 68)
|
|
982
1007
|
ln(" # Builds kwargs from static params, dynamic params, fresh subscription")
|
|
983
|
-
ln(" # values, and current state, then calls DynamicalSystem.
|
|
1008
|
+
ln(" # values, and current state, then calls DynamicalSystem.step() and")
|
|
984
1009
|
ln(" # publishes. Skipped entirely when sync_mode conditions are not met.")
|
|
985
1010
|
blank()
|
|
986
1011
|
ln(" def _run_step(self) -> None:")
|
|
@@ -1006,7 +1031,7 @@ class ROSNode:
|
|
|
1006
1031
|
" # Cold-start fallbacks — used only before first message arrives"
|
|
1007
1032
|
)
|
|
1008
1033
|
for arg in cold_start_args:
|
|
1009
|
-
ln(f" if {arg} is None and not self._{arg}
|
|
1034
|
+
ln(f" if {arg} is None and not self._{arg}_seen:")
|
|
1010
1035
|
ln(
|
|
1011
1036
|
f" {arg} = {_repr_value(_initial[arg])} # initial input"
|
|
1012
1037
|
)
|
|
@@ -1022,6 +1047,17 @@ class ROSNode:
|
|
|
1022
1047
|
ln(f" if {cond}:")
|
|
1023
1048
|
ln(" return # all inputs are stale — skip this tick")
|
|
1024
1049
|
|
|
1050
|
+
blank()
|
|
1051
|
+
ln(" # Consume: pop each fresh value now that we're committing to a")
|
|
1052
|
+
ln(
|
|
1053
|
+
" # step, so it cannot re-trigger another step until genuinely new"
|
|
1054
|
+
)
|
|
1055
|
+
ln(" # data arrives (prevents event-driven feedback loops from")
|
|
1056
|
+
ln(" # cascading at an unbounded rate).")
|
|
1057
|
+
for _, _, _, _, arg, _, _, _ in subs:
|
|
1058
|
+
ln(f" if {arg} is not None and self._{arg}_buf:")
|
|
1059
|
+
ln(f" self._{arg}_buf.pop()")
|
|
1060
|
+
|
|
1025
1061
|
blank()
|
|
1026
1062
|
ln(' kwargs: dict = {"tk": tk, **self._static_params}')
|
|
1027
1063
|
for name in _dynamic:
|
|
@@ -1034,7 +1070,7 @@ class ROSNode:
|
|
|
1034
1070
|
ln(f' kwargs["{self._state_key}"] = self._state')
|
|
1035
1071
|
|
|
1036
1072
|
blank()
|
|
1037
|
-
ln(" x_next, yk = self._system.
|
|
1073
|
+
ln(" x_next, yk = self._system.step(**kwargs)")
|
|
1038
1074
|
ln(" if self._system.f is not None:")
|
|
1039
1075
|
ln(" self._state = x_next # stateful: advance to next state")
|
|
1040
1076
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamicalnodes-0.1.4 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|