dynamicalnodes 0.1.5__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.5/src/dynamicalnodes.egg-info → dynamicalnodes-0.2.0}/PKG-INFO +1 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/pyproject.toml +1 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes/dynamical_system.py +7 -7
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes/rosnode.py +28 -25
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0/src/dynamicalnodes.egg-info}/PKG-INFO +1 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/LICENSE +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/README.md +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/setup.cfg +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes/__init__.py +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes/ros2py_py2ros.py +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/SOURCES.txt +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/dependency_links.txt +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/requires.txt +0 -0
- {dynamicalnodes-0.1.5 → 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
|
|
|
@@ -135,7 +135,7 @@ class DynamicalSystem:
|
|
|
135
135
|
def h(self, h: Optional[Callable]) -> None:
|
|
136
136
|
self._h = h
|
|
137
137
|
|
|
138
|
-
def
|
|
138
|
+
def step(self, **kwargs) -> Tuple[Optional[Any], Optional[Any]]:
|
|
139
139
|
"""
|
|
140
140
|
Evaluate the system for one discrete time step.
|
|
141
141
|
|
|
@@ -170,7 +170,7 @@ class DynamicalSystem:
|
|
|
170
170
|
cases.
|
|
171
171
|
|
|
172
172
|
>>> empty = DynamicalSystem()
|
|
173
|
-
>>> empty.
|
|
173
|
+
>>> empty.step()
|
|
174
174
|
Traceback (most recent call last):
|
|
175
175
|
...
|
|
176
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,7 +48,7 @@ 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
54
|
- ``"buffer_size"`` (int, optional): Queue depth (default 1).
|
|
@@ -73,8 +73,8 @@ class ROSNode:
|
|
|
73
73
|
state_name : str, optional
|
|
74
74
|
Name of the state parameter in ``f``'s signature (e.g. ``"ck"`` for
|
|
75
75
|
``f(ck, ...)``) . Required for stateful systems; ignored if ``f`` is
|
|
76
|
-
``None``. ``
|
|
77
|
-
calling ``DynamicalSystem.
|
|
76
|
+
``None``. ``step()`` injects ``self._state`` under this name before
|
|
77
|
+
calling ``DynamicalSystem.step()``.
|
|
78
78
|
|
|
79
79
|
Notes
|
|
80
80
|
-----
|
|
@@ -99,7 +99,7 @@ class ROSNode:
|
|
|
99
99
|
... pubs=[{"topic": "/smooth", "msg_type": Float64,
|
|
100
100
|
... "py2ros": py2ros_float64}],
|
|
101
101
|
... )
|
|
102
|
-
>>> out = node.
|
|
102
|
+
>>> out = node.step(imu=np.array([2.0]))
|
|
103
103
|
>>> out.data
|
|
104
104
|
1.0
|
|
105
105
|
"""
|
|
@@ -201,7 +201,7 @@ class ROSNode:
|
|
|
201
201
|
# SIMULATION API
|
|
202
202
|
# --------------------------------------------------------------------------
|
|
203
203
|
|
|
204
|
-
def
|
|
204
|
+
def step(self, **kwargs: Any) -> Any:
|
|
205
205
|
"""
|
|
206
206
|
Simulate one step without ROS2.
|
|
207
207
|
|
|
@@ -238,7 +238,7 @@ class ROSNode:
|
|
|
238
238
|
... pubs=[{"topic": "/y", "msg_type": Float64,
|
|
239
239
|
... "py2ros": py2ros_float64}],
|
|
240
240
|
... )
|
|
241
|
-
>>> node.
|
|
241
|
+
>>> node.step(x=Float64(data=3.0)).data
|
|
242
242
|
6.0
|
|
243
243
|
"""
|
|
244
244
|
sub_arg_names = {a for _, _, _, a, _, _ in self._subs}
|
|
@@ -264,7 +264,7 @@ class ROSNode:
|
|
|
264
264
|
if self._state is not None and self._state_key is not None:
|
|
265
265
|
step_kwargs[self._state_key] = self._state
|
|
266
266
|
|
|
267
|
-
x_next, yk = self._dynamical_system.
|
|
267
|
+
x_next, yk = self._dynamical_system.step(**step_kwargs)
|
|
268
268
|
if self._dynamical_system.f is not None:
|
|
269
269
|
self._state = x_next
|
|
270
270
|
|
|
@@ -351,7 +351,7 @@ class ROSNode:
|
|
|
351
351
|
``__init__``. Supports NumPy arrays and any ``repr()``-able value.
|
|
352
352
|
static_params : dict, optional
|
|
353
353
|
Parameters baked into the node at generation time. Embedded as
|
|
354
|
-
``self._static_params = {...}`` and merged into every ``
|
|
354
|
+
``self._static_params = {...}`` and merged into every ``step()``
|
|
355
355
|
call. Use for values that never change after deployment.
|
|
356
356
|
dynamic_params : dict, optional
|
|
357
357
|
Parameters declared on the ROS2 parameter server with their
|
|
@@ -365,9 +365,9 @@ class ROSNode:
|
|
|
365
365
|
restarting the node (e.g. controller gains).
|
|
366
366
|
initial_inputs : dict, optional
|
|
367
367
|
Cold-start fallback values for subscription inputs. Keys must
|
|
368
|
-
match subscription ``arg`` names. A value is used only
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
371
371
|
subscription value takes over permanently. If the subscription
|
|
372
372
|
later goes stale the step is skipped (``None`` is not replaced),
|
|
373
373
|
preventing silent operation on dead data. Use this to break
|
|
@@ -401,6 +401,11 @@ class ROSNode:
|
|
|
401
401
|
If the generated file contains invalid Python (indicates a bug
|
|
402
402
|
in the generator).
|
|
403
403
|
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
None
|
|
407
|
+
The generated node is written to ``path``.
|
|
408
|
+
|
|
404
409
|
Notes
|
|
405
410
|
-----
|
|
406
411
|
**Function resolution**
|
|
@@ -445,6 +450,8 @@ class ROSNode:
|
|
|
445
450
|
True
|
|
446
451
|
>>> "SingleThreadedExecutor" in content
|
|
447
452
|
True
|
|
453
|
+
>>> "_u_k_seen" in content
|
|
454
|
+
True
|
|
448
455
|
>>> os.unlink(path)
|
|
449
456
|
"""
|
|
450
457
|
import importlib
|
|
@@ -903,6 +910,7 @@ class ROSNode:
|
|
|
903
910
|
ln(" # ── Subscription buffers " + "─" * 47)
|
|
904
911
|
for _, _, _, _, arg, _, buf, _ in subs:
|
|
905
912
|
ln(f" self._{arg}_buf: deque = deque(maxlen={buf})")
|
|
913
|
+
ln(f" self._{arg}_seen: bool = False # any message ever received")
|
|
906
914
|
|
|
907
915
|
blank()
|
|
908
916
|
ln(" # ── Subscriptions " + "─" * 52)
|
|
@@ -952,9 +960,7 @@ class ROSNode:
|
|
|
952
960
|
ln(
|
|
953
961
|
" # sync_mode conditions are satisfied, and pops each value it consumes —"
|
|
954
962
|
)
|
|
955
|
-
ln(
|
|
956
|
-
" # so a stored value can trigger at most one step before it must be"
|
|
957
|
-
)
|
|
963
|
+
ln(" # so a stored value can trigger at most one step before it must be")
|
|
958
964
|
ln(" # refreshed by new data.")
|
|
959
965
|
for topic, msg_type, ros2py, cls, arg, _, _, use_msg_ts in subs:
|
|
960
966
|
ros2py_name = _ref_name(ros2py, cls)
|
|
@@ -975,6 +981,7 @@ class ROSNode:
|
|
|
975
981
|
else:
|
|
976
982
|
ln(f" ts = self.get_clock().now().nanoseconds * 1e-9")
|
|
977
983
|
ln(f" self._{arg}_buf.append((ts, arr))")
|
|
984
|
+
ln(f" self._{arg}_seen = True")
|
|
978
985
|
if timer_hz is None:
|
|
979
986
|
ln(f" self._run_step()")
|
|
980
987
|
|
|
@@ -998,7 +1005,7 @@ class ROSNode:
|
|
|
998
1005
|
ln(" # Control step")
|
|
999
1006
|
ln(" # " + "─" * 68)
|
|
1000
1007
|
ln(" # Builds kwargs from static params, dynamic params, fresh subscription")
|
|
1001
|
-
ln(" # values, and current state, then calls DynamicalSystem.
|
|
1008
|
+
ln(" # values, and current state, then calls DynamicalSystem.step() and")
|
|
1002
1009
|
ln(" # publishes. Skipped entirely when sync_mode conditions are not met.")
|
|
1003
1010
|
blank()
|
|
1004
1011
|
ln(" def _run_step(self) -> None:")
|
|
@@ -1024,7 +1031,7 @@ class ROSNode:
|
|
|
1024
1031
|
" # Cold-start fallbacks — used only before first message arrives"
|
|
1025
1032
|
)
|
|
1026
1033
|
for arg in cold_start_args:
|
|
1027
|
-
ln(f" if {arg} is None and not self._{arg}
|
|
1034
|
+
ln(f" if {arg} is None and not self._{arg}_seen:")
|
|
1028
1035
|
ln(
|
|
1029
1036
|
f" {arg} = {_repr_value(_initial[arg])} # initial input"
|
|
1030
1037
|
)
|
|
@@ -1041,15 +1048,11 @@ class ROSNode:
|
|
|
1041
1048
|
ln(" return # all inputs are stale — skip this tick")
|
|
1042
1049
|
|
|
1043
1050
|
blank()
|
|
1044
|
-
ln(
|
|
1045
|
-
" # Consume: pop each fresh value now that we're committing to a"
|
|
1046
|
-
)
|
|
1051
|
+
ln(" # Consume: pop each fresh value now that we're committing to a")
|
|
1047
1052
|
ln(
|
|
1048
1053
|
" # step, so it cannot re-trigger another step until genuinely new"
|
|
1049
1054
|
)
|
|
1050
|
-
ln(
|
|
1051
|
-
" # data arrives (prevents event-driven feedback loops from"
|
|
1052
|
-
)
|
|
1055
|
+
ln(" # data arrives (prevents event-driven feedback loops from")
|
|
1053
1056
|
ln(" # cascading at an unbounded rate).")
|
|
1054
1057
|
for _, _, _, _, arg, _, _, _ in subs:
|
|
1055
1058
|
ln(f" if {arg} is not None and self._{arg}_buf:")
|
|
@@ -1067,7 +1070,7 @@ class ROSNode:
|
|
|
1067
1070
|
ln(f' kwargs["{self._state_key}"] = self._state')
|
|
1068
1071
|
|
|
1069
1072
|
blank()
|
|
1070
|
-
ln(" x_next, yk = self._system.
|
|
1073
|
+
ln(" x_next, yk = self._system.step(**kwargs)")
|
|
1071
1074
|
ln(" if self._system.f is not None:")
|
|
1072
1075
|
ln(" self._state = x_next # stateful: advance to next state")
|
|
1073
1076
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamicalnodes-0.1.5 → dynamicalnodes-0.2.0}/src/dynamicalnodes.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|