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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamicalnodes
3
- Version: 0.1.5
3
+ Version: 0.2.1
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
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "dynamicalnodes"
10
- version = "0.1.5"
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 dependencies. Install with: pip install dynamicalnodes[ros2]\n"
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.eval`` binds keyword arguments to each
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.eval(xk=(20.0, 0.0), uk=4.0, theta_car=(2.0, 0.0, 1.0))
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.eval(tk=0.0)
67
+ >>> ref.step(tk=0.0)
68
68
  (None, 0.0)
69
- >>> ref.eval(tk=2.0)
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.eval(xk=5)
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 eval(self, **kwargs) -> Tuple[Optional[Any], Optional[Any]]:
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.eval()
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 ``eval()`` directly in Python or Jupyter notebooks to test ros2py and/or py2ros translation functions.
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 ``eval()`` for notebook simulation; use ``write_ROSNode_to_rclpy()`` to
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 eval().
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``. ``eval()`` injects ``self._state`` under this name before
77
- calling ``DynamicalSystem.eval()``.
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.eval(imu=np.array([2.0]))
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 eval(self, **kwargs: Any) -> Any:
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.eval(x=Float64(data=3.0)).data
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.eval(**step_kwargs)
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 ``eval()``
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 when the
369
- subscription buffer is completely empty — i.e. no message has
370
- ever arrived on that topic. Once the first message arrives the
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.eval() and")
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}_buf:")
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.eval(**kwargs)")
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.shutdown()")
1130
+ ln(" if rclpy.ok():")
1131
+ ln(" rclpy.shutdown()")
1126
1132
  blank()
1127
1133
  blank()
1128
1134
  ln('if __name__ == "__main__":')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamicalnodes
3
- Version: 0.1.5
3
+ Version: 0.2.1
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
File without changes
File without changes
File without changes