dynamicalnodes 0.1__tar.gz → 0.1.3__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.3/PKG-INFO +80 -0
- dynamicalnodes-0.1.3/README.md +38 -0
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/pyproject.toml +21 -2
- dynamicalnodes-0.1.3/src/dynamicalnodes/__init__.py +29 -0
- dynamicalnodes-0.1.3/src/dynamicalnodes/dynamical_system.py +204 -0
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/src/dynamicalnodes/ros2py_py2ros.py +42 -3
- dynamicalnodes-0.1.3/src/dynamicalnodes/rosnode.py +1103 -0
- dynamicalnodes-0.1.3/src/dynamicalnodes.egg-info/PKG-INFO +80 -0
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/src/dynamicalnodes.egg-info/SOURCES.txt +0 -1
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/src/dynamicalnodes.egg-info/requires.txt +4 -1
- dynamicalnodes-0.1/PKG-INFO +0 -82
- dynamicalnodes-0.1/README.md +0 -50
- dynamicalnodes-0.1/src/dynamicalnodes/__init__.py +0 -35
- dynamicalnodes-0.1/src/dynamicalnodes/dynamical_system.py +0 -221
- dynamicalnodes-0.1/src/dynamicalnodes/rosnode.py +0 -658
- dynamicalnodes-0.1/src/dynamicalnodes/rostools.py +0 -372
- dynamicalnodes-0.1/src/dynamicalnodes.egg-info/PKG-INFO +0 -82
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/LICENSE +0 -0
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/setup.cfg +0 -0
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/src/dynamicalnodes.egg-info/dependency_links.txt +0 -0
- {dynamicalnodes-0.1 → dynamicalnodes-0.1.3}/src/dynamicalnodes.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dynamicalnodes
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: dynamicalnodes is a Python library for modeling general control systems.
|
|
5
|
+
Author-email: Nehal Singh Mangat <nehalsinghmangat.software@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nehalsinghmangat/dynamicalnodes
|
|
8
|
+
Project-URL: Documentation, https://nehalsinghmangat.github.io/dynamicalnodes
|
|
9
|
+
Project-URL: Issues, https://github.com/nehalsinghmangat/dynamicalnodes/issues
|
|
10
|
+
Keywords: control,estimation,ros2
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.24
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-doctestplus>=1.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
25
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
26
|
+
Requires-Dist: isort>=6.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: build>=0.10; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: sphinx>=8.0; extra == "docs"
|
|
32
|
+
Requires-Dist: sphinx-rtd-theme>=1.3; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
34
|
+
Requires-Dist: myst_nb; extra == "docs"
|
|
35
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.24.0; extra == "docs"
|
|
36
|
+
Requires-Dist: matplotlib>=3.9; extra == "docs"
|
|
37
|
+
Requires-Dist: scipy>=1.11; extra == "docs"
|
|
38
|
+
Requires-Dist: jupyter; extra == "docs"
|
|
39
|
+
Requires-Dist: jaxtyping>=0.2; extra == "docs"
|
|
40
|
+
Requires-Dist: beartype>=0.15; extra == "docs"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# dynamicalnodes: Control Systems on ROS 2
|
|
44
|
+
|
|
45
|
+
> _"Give me a [control system] long enough and a [computer] on which to place it, and I shall move the world."_ - Archimedes
|
|
46
|
+
|
|
47
|
+
**dynamicalnodes** is a Python development framework that bridges the gap between novel control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
48
|
+
|
|
49
|
+
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://nehalsinghmangat.github.io/dynamicalnodes).
|
|
50
|
+
|
|
51
|
+
For a 22 second video describing this framework's intended workflow for, click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
|
|
55
|
+
<table>
|
|
56
|
+
<tr>
|
|
57
|
+
<td align="center">
|
|
58
|
+
<img src="docs/source/_static/figures/turtlebot_schematic.svg" alt="TurtleBot Schematic" width="400"/><br/>
|
|
59
|
+
</td>
|
|
60
|
+
<td align="center">
|
|
61
|
+
<img src="docs/source/_static/figures/turtlebot_python_simulation.gif" alt="Turtle Software" width="200"/><br/>
|
|
62
|
+
</td>
|
|
63
|
+
<td align="center">
|
|
64
|
+
<img src="docs/source/_static/figures/turtlebot_ros_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
65
|
+
</td>
|
|
66
|
+
<td align="center">
|
|
67
|
+
<img src="docs/source/_static/figures/turtlebot_mocab.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
</table>
|
|
71
|
+
|
|
72
|
+
<p align="center">
|
|
73
|
+
<img src="docs/source/_static/figures/pipeline_theory_python_ros_hardware.png" alt="Step Labels" width="800"/>
|
|
74
|
+
</p>
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# dynamicalnodes: Control Systems on ROS 2
|
|
2
|
+
|
|
3
|
+
> _"Give me a [control system] long enough and a [computer] on which to place it, and I shall move the world."_ - Archimedes
|
|
4
|
+
|
|
5
|
+
**dynamicalnodes** is a Python development framework that bridges the gap between novel control systems and their implementation onto ROS 2. Designed for hobbyists, students, and academics alike, this framework won't cure cancer, but it can do the next best thing: make robotics easier.
|
|
6
|
+
|
|
7
|
+
To get started, or to explore what this framework has to offer, click here: [dynamicalnodes Documentation](https://nehalsinghmangat.github.io/dynamicalnodes).
|
|
8
|
+
|
|
9
|
+
For a 22 second video describing this framework's intended workflow for, click here: [SAIL 2025 -- Presenting dynamicalnodes](https://youtu.be/5GbVHo6QZrw).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
## Turtlebot: Whiteboard ⟶ Python ⟶ ROS 2 ⟶ World
|
|
13
|
+
<table>
|
|
14
|
+
<tr>
|
|
15
|
+
<td align="center">
|
|
16
|
+
<img src="docs/source/_static/figures/turtlebot_schematic.svg" alt="TurtleBot Schematic" width="400"/><br/>
|
|
17
|
+
</td>
|
|
18
|
+
<td align="center">
|
|
19
|
+
<img src="docs/source/_static/figures/turtlebot_python_simulation.gif" alt="Turtle Software" width="200"/><br/>
|
|
20
|
+
</td>
|
|
21
|
+
<td align="center">
|
|
22
|
+
<img src="docs/source/_static/figures/turtlebot_ros_simulation.gif" alt="Turtle Simulation" width="200"/><br/>
|
|
23
|
+
</td>
|
|
24
|
+
<td align="center">
|
|
25
|
+
<img src="docs/source/_static/figures/turtlebot_mocab.gif" alt="Turtle Hardware" width="200"/><br/>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
</table>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="docs/source/_static/figures/pipeline_theory_python_ros_hardware.png" alt="Step Labels" width="800"/>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
@@ -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.1.3"
|
|
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,6 +16,14 @@ authors = [
|
|
|
16
16
|
{ name = "Nehal Singh Mangat", email = "nehalsinghmangat.software@gmail.com" }
|
|
17
17
|
]
|
|
18
18
|
keywords = ["control", "estimation", "ros2"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
19
27
|
dependencies = [
|
|
20
28
|
"numpy>=1.24",
|
|
21
29
|
]
|
|
@@ -35,21 +43,32 @@ dev = [
|
|
|
35
43
|
docs = [
|
|
36
44
|
"sphinx>=8.0",
|
|
37
45
|
"sphinx-rtd-theme>=1.3",
|
|
46
|
+
"sphinx-copybutton>=0.5",
|
|
38
47
|
"myst_nb",
|
|
39
48
|
"sphinx-autodoc-typehints>=1.24.0",
|
|
40
|
-
"matplotlib>=3.
|
|
49
|
+
"matplotlib>=3.9",
|
|
41
50
|
"scipy>=1.11",
|
|
42
51
|
"jupyter",
|
|
52
|
+
"jaxtyping>=0.2",
|
|
53
|
+
"beartype>=0.15",
|
|
43
54
|
]
|
|
44
55
|
|
|
45
56
|
[project.urls]
|
|
46
57
|
Homepage = "https://github.com/nehalsinghmangat/dynamicalnodes"
|
|
47
58
|
Documentation = "https://nehalsinghmangat.github.io/dynamicalnodes"
|
|
59
|
+
Issues = "https://github.com/nehalsinghmangat/dynamicalnodes/issues"
|
|
48
60
|
|
|
49
61
|
[tool.pytest.ini_options]
|
|
50
62
|
addopts = "--doctest-modules"
|
|
51
63
|
doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS"]
|
|
52
64
|
|
|
65
|
+
[tool.isort]
|
|
66
|
+
profile = "black"
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.12"
|
|
70
|
+
strict = true
|
|
71
|
+
|
|
53
72
|
[tool.setuptools]
|
|
54
73
|
package-dir = {"" = "src"}
|
|
55
74
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dynamicalnodes: A modular Python framework for dynamical systems, estimation, and ROS2 integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .dynamical_system import DynamicalSystem
|
|
6
|
+
from .rosnode import ROSNode
|
|
7
|
+
|
|
8
|
+
# Lazy import — requires a live ROS2 installation
|
|
9
|
+
_ROS2_MODULES = {
|
|
10
|
+
"ros2py_py2ros": ("ros2py_py2ros", None),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __getattr__(name):
|
|
15
|
+
if name in _ROS2_MODULES:
|
|
16
|
+
module_name, attr = _ROS2_MODULES[name]
|
|
17
|
+
try:
|
|
18
|
+
import importlib
|
|
19
|
+
mod = importlib.import_module(f".{module_name}", package=__name__)
|
|
20
|
+
return mod if attr is None else getattr(mod, attr)
|
|
21
|
+
except ImportError as e:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
f"{name} requires ROS2 dependencies. Install with: pip install dynamicalnodes[ros2]\n"
|
|
24
|
+
f"Original error: {e}"
|
|
25
|
+
) from e
|
|
26
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["DynamicalSystem", "ROSNode"]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A discrete-time dynamical system (DTDS) is represented as a pair of functions,
|
|
3
|
+
``(f, h)``, following the definition in Mangat et al., *dynamicalnodes: From
|
|
4
|
+
Theory to Python to ROS*::
|
|
5
|
+
|
|
6
|
+
x_{k+1} = f(x_k, u_k, theta)
|
|
7
|
+
y_k = h(x_k, u_k, theta)
|
|
8
|
+
|
|
9
|
+
``f`` is the evolution function that propagates the state forward one step;
|
|
10
|
+
``h`` is the measurement (or output) function that computes an observation
|
|
11
|
+
from the current state. Either function may be omitted -- denoted ``*`` in
|
|
12
|
+
the paper, ``None`` here -- giving three cases:
|
|
13
|
+
|
|
14
|
+
- ``(f, *)`` -- no output function.
|
|
15
|
+
- ``(*, h)`` -- stateless, e.g. a signal generator.
|
|
16
|
+
- ``(f, h)`` -- both state evolution and output are defined.
|
|
17
|
+
|
|
18
|
+
Function signatures are unconstrained in name, type, and number of
|
|
19
|
+
parameters. ``DynamicalSystem.eval`` binds keyword arguments to each
|
|
20
|
+
function's signature by name, so the same keyword argument (e.g. a shared
|
|
21
|
+
state or parameter) can be passed to both ``f`` and ``h``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import inspect
|
|
25
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DynamicalSystem:
|
|
29
|
+
"""
|
|
30
|
+
A discrete-time dynamical system with (optional) evolution and
|
|
31
|
+
(optional) measurement functions.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
f : Callable, optional
|
|
36
|
+
Evolution function ``f(x_k, u_k, theta) -> x_{k+1}``. ``None`` if
|
|
37
|
+
the system has no state transition, i.e. ``(*, h)``.
|
|
38
|
+
h : Callable, optional
|
|
39
|
+
Measurement function ``h(x_k, u_k, theta) -> y_k``. ``None`` if
|
|
40
|
+
the system has no output, i.e. ``(f, *)``.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
A stateful system with both an evolution and a measurement function --
|
|
45
|
+
a car whose state is ``(displacement, velocity)``, measured via its
|
|
46
|
+
speed:
|
|
47
|
+
|
|
48
|
+
>>> def fcar(xk, uk, theta_car):
|
|
49
|
+
... d, v = xk
|
|
50
|
+
... m, b, dt = theta_car
|
|
51
|
+
... return (d + dt * v, v + dt * (uk - b * v) / m)
|
|
52
|
+
>>> def hcar(xk):
|
|
53
|
+
... _, v = xk
|
|
54
|
+
... return abs(v)
|
|
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))
|
|
57
|
+
>>> x_next
|
|
58
|
+
(20.0, 2.0)
|
|
59
|
+
>>> y
|
|
60
|
+
0.0
|
|
61
|
+
|
|
62
|
+
A stateless system, e.g. a signal generator -- ``(*, h)``:
|
|
63
|
+
|
|
64
|
+
>>> def h_ref(tk, A=1.0):
|
|
65
|
+
... return A if tk >= 1.0 else 0.0
|
|
66
|
+
>>> ref = DynamicalSystem(h=h_ref)
|
|
67
|
+
>>> ref.eval(tk=0.0)
|
|
68
|
+
(None, 0.0)
|
|
69
|
+
>>> ref.eval(tk=2.0)
|
|
70
|
+
(None, 1.0)
|
|
71
|
+
|
|
72
|
+
A system with no output function -- ``(f, *)``:
|
|
73
|
+
|
|
74
|
+
>>> def f_counter(xk):
|
|
75
|
+
... return xk + 1
|
|
76
|
+
>>> counter = DynamicalSystem(f=f_counter)
|
|
77
|
+
>>> counter.eval(xk=5)
|
|
78
|
+
(6, None)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
f: Optional[Callable] = None,
|
|
85
|
+
h: Optional[Callable] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
f : Callable, optional
|
|
91
|
+
Evolution function ``f(x_k, u_k, theta) -> x_{k+1}``. Signature
|
|
92
|
+
typing is not enforced. ``None`` if the system is stateless.
|
|
93
|
+
h : Callable, optional
|
|
94
|
+
Measurement function ``h(x_k, u_k, theta) -> y_k``. Signature
|
|
95
|
+
typing is not enforced. ``None`` if the system has no output.
|
|
96
|
+
"""
|
|
97
|
+
self._f = f
|
|
98
|
+
self._h = h
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def f(self) -> Optional[Callable]:
|
|
102
|
+
"""Evolution function f(x_k, u_k, theta) -> x_{k+1}, or None if the system is stateless."""
|
|
103
|
+
return self._f
|
|
104
|
+
|
|
105
|
+
@f.setter
|
|
106
|
+
def f(self, f: Optional[Callable]) -> None:
|
|
107
|
+
self._f = f
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def h(self) -> Optional[Callable]:
|
|
111
|
+
"""Measurement function h(x_k, u_k, theta) -> y_k, or None if the system has no output."""
|
|
112
|
+
return self._h
|
|
113
|
+
|
|
114
|
+
@h.setter
|
|
115
|
+
def h(self, h: Optional[Callable]) -> None:
|
|
116
|
+
self._h = h
|
|
117
|
+
|
|
118
|
+
def eval(self, **kwargs) -> Tuple[Optional[Any], Optional[Any]]:
|
|
119
|
+
"""
|
|
120
|
+
Evaluate the system for one discrete time step.
|
|
121
|
+
|
|
122
|
+
Keyword arguments are lexically bound by name to whichever of ``f``
|
|
123
|
+
and/or ``h`` declare them in their signature (see
|
|
124
|
+
``_bind_and_call``), so the same keyword argument can be shared by
|
|
125
|
+
both functions.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
Tuple[Optional[Any], Optional[Any]]
|
|
130
|
+
- ``(None, y_k)`` for a stateless system, ``(*, h)``.
|
|
131
|
+
- ``(x_{k+1}, None)`` for a system with no output, ``(f, *)``.
|
|
132
|
+
- ``(x_{k+1}, y_k)`` for a fully specified system, ``(f, h)``.
|
|
133
|
+
|
|
134
|
+
Notes
|
|
135
|
+
-----
|
|
136
|
+
``h`` is evaluated on the same (pre-update) state passed in via
|
|
137
|
+
keyword arguments, not on the value returned by ``f``, so the
|
|
138
|
+
returned observation is one step behind the returned state. In a
|
|
139
|
+
simulation loop, use the returned ``x_{k+1}`` directly if you need
|
|
140
|
+
the post-update state.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
ValueError
|
|
145
|
+
If neither ``f`` nor ``h`` is defined.
|
|
146
|
+
|
|
147
|
+
Examples
|
|
148
|
+
--------
|
|
149
|
+
See the class docstring for worked examples covering all three
|
|
150
|
+
cases.
|
|
151
|
+
|
|
152
|
+
>>> empty = DynamicalSystem()
|
|
153
|
+
>>> empty.eval()
|
|
154
|
+
Traceback (most recent call last):
|
|
155
|
+
...
|
|
156
|
+
ValueError: Nothing to evaluate; no f or h functions!
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def _bind_and_call(
|
|
160
|
+
func: Callable[..., Any],
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
Call `func` with only the keyword arguments it declares.
|
|
165
|
+
|
|
166
|
+
Inspects `func`'s signature and passes only the entries of
|
|
167
|
+
`kwargs` matching its declared parameter names; if `func`
|
|
168
|
+
accepts `**kwargs`, the remaining pool is passed through too.
|
|
169
|
+
"""
|
|
170
|
+
sig = inspect.signature(func)
|
|
171
|
+
pool: Dict[str, Any] = dict(kwargs)
|
|
172
|
+
|
|
173
|
+
# Filter: only pass arguments the function declares
|
|
174
|
+
call_kwargs: Dict[str, Any] = {}
|
|
175
|
+
for name, p in sig.parameters.items():
|
|
176
|
+
if p.kind in (
|
|
177
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
178
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
179
|
+
):
|
|
180
|
+
if name in pool:
|
|
181
|
+
call_kwargs[name] = pool.pop(name)
|
|
182
|
+
|
|
183
|
+
# Pass remaining kwargs if function has **kwargs
|
|
184
|
+
if any(
|
|
185
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
186
|
+
):
|
|
187
|
+
call_kwargs.update(pool)
|
|
188
|
+
|
|
189
|
+
return func(**call_kwargs)
|
|
190
|
+
|
|
191
|
+
if self.f is None and self.h:
|
|
192
|
+
return (None, _bind_and_call(self.h, **kwargs))
|
|
193
|
+
|
|
194
|
+
elif self.f and self.h is None:
|
|
195
|
+
return (_bind_and_call(self.f, **kwargs), None)
|
|
196
|
+
|
|
197
|
+
elif self.f and self.h:
|
|
198
|
+
return (
|
|
199
|
+
_bind_and_call(self.f, **kwargs),
|
|
200
|
+
_bind_and_call(self.h, **kwargs),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError("Nothing to evaluate; no f or h functions!")
|
|
@@ -24,6 +24,7 @@ from turtlesim.msg import Pose
|
|
|
24
24
|
from nav_msgs.msg import Odometry, Path
|
|
25
25
|
from sensor_msgs.msg import (
|
|
26
26
|
Imu,
|
|
27
|
+
NavSatFix,
|
|
27
28
|
LaserScan,
|
|
28
29
|
Image,
|
|
29
30
|
CompressedImage,
|
|
@@ -558,6 +559,42 @@ def py2ros_imu(arr: NDArray) -> Imu:
|
|
|
558
559
|
return msg
|
|
559
560
|
|
|
560
561
|
|
|
562
|
+
# ---------------------------------------------------------------------------
|
|
563
|
+
# sensor_msgs/NavSatFix <-> np.ndarray(3,) [latitude, longitude, altitude]
|
|
564
|
+
# ---------------------------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
def ros2py_nav_sat_fix(msg: NavSatFix) -> NDArray:
|
|
567
|
+
"""
|
|
568
|
+
NavSatFix → [latitude, longitude, altitude]
|
|
569
|
+
|
|
570
|
+
>>> from sensor_msgs.msg import NavSatFix
|
|
571
|
+
>>> msg = NavSatFix()
|
|
572
|
+
>>> msg.latitude, msg.longitude, msg.altitude = 37.5, -122.0, 10.0
|
|
573
|
+
>>> ros2py_nav_sat_fix(msg).tolist()
|
|
574
|
+
[37.5, -122.0, 10.0]
|
|
575
|
+
"""
|
|
576
|
+
return np.array([msg.latitude, msg.longitude, msg.altitude], dtype=float)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def py2ros_nav_sat_fix(arr: NDArray) -> NavSatFix:
|
|
580
|
+
"""
|
|
581
|
+
[latitude, longitude, altitude] → NavSatFix
|
|
582
|
+
|
|
583
|
+
>>> import numpy as np
|
|
584
|
+
>>> msg = py2ros_nav_sat_fix(np.array([37.5, -122.0, 10.0]))
|
|
585
|
+
>>> msg.latitude, msg.longitude, msg.altitude
|
|
586
|
+
(37.5, -122.0, 10.0)
|
|
587
|
+
"""
|
|
588
|
+
flat = np.asarray(arr, dtype=float).ravel()
|
|
589
|
+
if flat.size != 3:
|
|
590
|
+
raise ValueError(f"py2ros_nav_sat_fix expected length 3, got {flat.size}")
|
|
591
|
+
msg = NavSatFix()
|
|
592
|
+
msg.latitude = float(flat[0])
|
|
593
|
+
msg.longitude = float(flat[1])
|
|
594
|
+
msg.altitude = float(flat[2])
|
|
595
|
+
return msg
|
|
596
|
+
|
|
597
|
+
|
|
561
598
|
# ---------------------------------------------------------------------------
|
|
562
599
|
# nav_msgs/Odometry <-> np.ndarray(13,)
|
|
563
600
|
# (pose + twist; covariances ignored)
|
|
@@ -907,9 +944,9 @@ def py2ros_uint8_multiarray(arr: NDArray) -> UInt8MultiArray:
|
|
|
907
944
|
# std_msgs/Float64 <-> scalar float (as 1D array)
|
|
908
945
|
# ---------------------------------------------------------------------------
|
|
909
946
|
|
|
910
|
-
def ros2py_float64(msg: Float64) ->
|
|
911
|
-
"""Convert Float64 message to
|
|
912
|
-
return
|
|
947
|
+
def ros2py_float64(msg: Float64) -> float:
|
|
948
|
+
"""Convert Float64 message to Python float."""
|
|
949
|
+
return float(msg.data)
|
|
913
950
|
|
|
914
951
|
|
|
915
952
|
def py2ros_float64(arr: NDArray) -> Float64:
|
|
@@ -994,6 +1031,7 @@ ROS2PY_DEFAULT = {
|
|
|
994
1031
|
AccelStamped: ros2py_accel_stamped,
|
|
995
1032
|
Vector3Stamped: ros2py_vector3_stamped,
|
|
996
1033
|
Imu: ros2py_imu,
|
|
1034
|
+
NavSatFix: ros2py_nav_sat_fix,
|
|
997
1035
|
Odometry: ros2py_odometry,
|
|
998
1036
|
Path: ros2py_path,
|
|
999
1037
|
JointState: ros2py_joint_state,
|
|
@@ -1019,6 +1057,7 @@ PY2ROS_DEFAULT = {
|
|
|
1019
1057
|
AccelStamped: py2ros_accel_stamped,
|
|
1020
1058
|
Vector3Stamped: py2ros_vector3_stamped,
|
|
1021
1059
|
Imu: py2ros_imu,
|
|
1060
|
+
NavSatFix: py2ros_nav_sat_fix,
|
|
1022
1061
|
Odometry: py2ros_odometry,
|
|
1023
1062
|
Path: py2ros_path,
|
|
1024
1063
|
JointState: py2ros_joint_state,
|