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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamicalnodes
3
- Version: 0.1.5
3
+ Version: 0.2.0
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.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.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,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.eval() and")
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}_buf:")
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.eval(**kwargs)")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamicalnodes
3
- Version: 0.1.5
3
+ Version: 0.2.0
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