dynamicalnodes 0.1.5__tar.gz → 0.2.1__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.1}/PKG-INFO +1 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/pyproject.toml +1 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes/__init__.py +3 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes/dynamical_system.py +7 -7
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes/rosnode.py +34 -28
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1/src/dynamicalnodes.egg-info}/PKG-INFO +1 -1
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/LICENSE +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/README.md +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/setup.cfg +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes/ros2py_py2ros.py +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes.egg-info/SOURCES.txt +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes.egg-info/dependency_links.txt +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes.egg-info/requires.txt +0 -0
- {dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/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.1
|
|
10
|
+
version = "0.2.1"
|
|
11
11
|
description = "dynamicalnodes is a Python library for modeling general control systems."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -20,7 +20,9 @@ def __getattr__(name):
|
|
|
20
20
|
return mod if attr is None else getattr(mod, attr)
|
|
21
21
|
except ImportError as e:
|
|
22
22
|
raise ImportError(
|
|
23
|
-
f"{name} requires ROS2
|
|
23
|
+
f"{name} requires a ROS2 installation (rclpy and the ROS message packages are "
|
|
24
|
+
f"system packages, not on PyPI). Install ROS2 and source its setup script, "
|
|
25
|
+
f"e.g. `source /opt/ros/jazzy/setup.bash`.\n"
|
|
24
26
|
f"Original error: {e}"
|
|
25
27
|
) from e
|
|
26
28
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -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,10 @@ class ROSNode:
|
|
|
445
450
|
True
|
|
446
451
|
>>> "SingleThreadedExecutor" in content
|
|
447
452
|
True
|
|
453
|
+
>>> "ExternalShutdownException" in content and "if rclpy.ok():" in content
|
|
454
|
+
True
|
|
455
|
+
>>> "_u_k_seen" in content
|
|
456
|
+
True
|
|
448
457
|
>>> os.unlink(path)
|
|
449
458
|
"""
|
|
450
459
|
import importlib
|
|
@@ -744,7 +753,7 @@ class ROSNode:
|
|
|
744
753
|
blank()
|
|
745
754
|
ln("import rclpy")
|
|
746
755
|
ln("from rclpy.node import Node")
|
|
747
|
-
ln("from rclpy.executors import SingleThreadedExecutor")
|
|
756
|
+
ln("from rclpy.executors import ExternalShutdownException, SingleThreadedExecutor")
|
|
748
757
|
ln("from rclpy.callback_groups import ReentrantCallbackGroup")
|
|
749
758
|
ln("from rclpy.qos import QoSProfile, QoSHistoryPolicy, QoSReliabilityPolicy")
|
|
750
759
|
blank()
|
|
@@ -903,6 +912,7 @@ class ROSNode:
|
|
|
903
912
|
ln(" # ── Subscription buffers " + "─" * 47)
|
|
904
913
|
for _, _, _, _, arg, _, buf, _ in subs:
|
|
905
914
|
ln(f" self._{arg}_buf: deque = deque(maxlen={buf})")
|
|
915
|
+
ln(f" self._{arg}_seen: bool = False # any message ever received")
|
|
906
916
|
|
|
907
917
|
blank()
|
|
908
918
|
ln(" # ── Subscriptions " + "─" * 52)
|
|
@@ -952,9 +962,7 @@ class ROSNode:
|
|
|
952
962
|
ln(
|
|
953
963
|
" # sync_mode conditions are satisfied, and pops each value it consumes —"
|
|
954
964
|
)
|
|
955
|
-
ln(
|
|
956
|
-
" # so a stored value can trigger at most one step before it must be"
|
|
957
|
-
)
|
|
965
|
+
ln(" # so a stored value can trigger at most one step before it must be")
|
|
958
966
|
ln(" # refreshed by new data.")
|
|
959
967
|
for topic, msg_type, ros2py, cls, arg, _, _, use_msg_ts in subs:
|
|
960
968
|
ros2py_name = _ref_name(ros2py, cls)
|
|
@@ -975,6 +983,7 @@ class ROSNode:
|
|
|
975
983
|
else:
|
|
976
984
|
ln(f" ts = self.get_clock().now().nanoseconds * 1e-9")
|
|
977
985
|
ln(f" self._{arg}_buf.append((ts, arr))")
|
|
986
|
+
ln(f" self._{arg}_seen = True")
|
|
978
987
|
if timer_hz is None:
|
|
979
988
|
ln(f" self._run_step()")
|
|
980
989
|
|
|
@@ -998,7 +1007,7 @@ class ROSNode:
|
|
|
998
1007
|
ln(" # Control step")
|
|
999
1008
|
ln(" # " + "─" * 68)
|
|
1000
1009
|
ln(" # Builds kwargs from static params, dynamic params, fresh subscription")
|
|
1001
|
-
ln(" # values, and current state, then calls DynamicalSystem.
|
|
1010
|
+
ln(" # values, and current state, then calls DynamicalSystem.step() and")
|
|
1002
1011
|
ln(" # publishes. Skipped entirely when sync_mode conditions are not met.")
|
|
1003
1012
|
blank()
|
|
1004
1013
|
ln(" def _run_step(self) -> None:")
|
|
@@ -1024,7 +1033,7 @@ class ROSNode:
|
|
|
1024
1033
|
" # Cold-start fallbacks — used only before first message arrives"
|
|
1025
1034
|
)
|
|
1026
1035
|
for arg in cold_start_args:
|
|
1027
|
-
ln(f" if {arg} is None and not self._{arg}
|
|
1036
|
+
ln(f" if {arg} is None and not self._{arg}_seen:")
|
|
1028
1037
|
ln(
|
|
1029
1038
|
f" {arg} = {_repr_value(_initial[arg])} # initial input"
|
|
1030
1039
|
)
|
|
@@ -1041,15 +1050,11 @@ class ROSNode:
|
|
|
1041
1050
|
ln(" return # all inputs are stale — skip this tick")
|
|
1042
1051
|
|
|
1043
1052
|
blank()
|
|
1044
|
-
ln(
|
|
1045
|
-
" # Consume: pop each fresh value now that we're committing to a"
|
|
1046
|
-
)
|
|
1053
|
+
ln(" # Consume: pop each fresh value now that we're committing to a")
|
|
1047
1054
|
ln(
|
|
1048
1055
|
" # step, so it cannot re-trigger another step until genuinely new"
|
|
1049
1056
|
)
|
|
1050
|
-
ln(
|
|
1051
|
-
" # data arrives (prevents event-driven feedback loops from"
|
|
1052
|
-
)
|
|
1057
|
+
ln(" # data arrives (prevents event-driven feedback loops from")
|
|
1053
1058
|
ln(" # cascading at an unbounded rate).")
|
|
1054
1059
|
for _, _, _, _, arg, _, _, _ in subs:
|
|
1055
1060
|
ln(f" if {arg} is not None and self._{arg}_buf:")
|
|
@@ -1067,7 +1072,7 @@ class ROSNode:
|
|
|
1067
1072
|
ln(f' kwargs["{self._state_key}"] = self._state')
|
|
1068
1073
|
|
|
1069
1074
|
blank()
|
|
1070
|
-
ln(" x_next, yk = self._system.
|
|
1075
|
+
ln(" x_next, yk = self._system.step(**kwargs)")
|
|
1071
1076
|
ln(" if self._system.f is not None:")
|
|
1072
1077
|
ln(" self._state = x_next # stateful: advance to next state")
|
|
1073
1078
|
|
|
@@ -1118,11 +1123,12 @@ class ROSNode:
|
|
|
1118
1123
|
ln(" executor.add_node(node)")
|
|
1119
1124
|
ln(" try:")
|
|
1120
1125
|
ln(" executor.spin()")
|
|
1121
|
-
ln(" except KeyboardInterrupt:")
|
|
1126
|
+
ln(" except (KeyboardInterrupt, ExternalShutdownException):")
|
|
1122
1127
|
ln(" pass")
|
|
1123
1128
|
ln(" finally:")
|
|
1124
1129
|
ln(" node.destroy_node()")
|
|
1125
|
-
ln(" rclpy.
|
|
1130
|
+
ln(" if rclpy.ok():")
|
|
1131
|
+
ln(" rclpy.shutdown()")
|
|
1126
1132
|
blank()
|
|
1127
1133
|
blank()
|
|
1128
1134
|
ln('if __name__ == "__main__":')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamicalnodes-0.1.5 → dynamicalnodes-0.2.1}/src/dynamicalnodes.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|