virtualmodelcontrol 0.1.0__py3-none-any.whl

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.
Files changed (49) hide show
  1. virtualmodelcontrol/__init__.py +75 -0
  2. virtualmodelcontrol/_version.py +24 -0
  3. virtualmodelcontrol/compiler.py +158 -0
  4. virtualmodelcontrol/control/__init__.py +5 -0
  5. virtualmodelcontrol/control/controller.py +76 -0
  6. virtualmodelcontrol/core/__init__.py +24 -0
  7. virtualmodelcontrol/core/params.py +244 -0
  8. virtualmodelcontrol/core/registry.py +54 -0
  9. virtualmodelcontrol/core/signals.py +46 -0
  10. virtualmodelcontrol/core/space.py +162 -0
  11. virtualmodelcontrol/core/symbolic.py +67 -0
  12. virtualmodelcontrol/core/units.py +34 -0
  13. virtualmodelcontrol/dynamics.py +149 -0
  14. virtualmodelcontrol/mechanisms/__init__.py +67 -0
  15. virtualmodelcontrol/mechanisms/components/__init__.py +33 -0
  16. virtualmodelcontrol/mechanisms/components/base.py +71 -0
  17. virtualmodelcontrol/mechanisms/components/dissipation.py +50 -0
  18. virtualmodelcontrol/mechanisms/components/inertance.py +56 -0
  19. virtualmodelcontrol/mechanisms/components/sources.py +63 -0
  20. virtualmodelcontrol/mechanisms/components/storage.py +272 -0
  21. virtualmodelcontrol/mechanisms/coordinates/__init__.py +24 -0
  22. virtualmodelcontrol/mechanisms/coordinates/base.py +117 -0
  23. virtualmodelcontrol/mechanisms/coordinates/frames.py +62 -0
  24. virtualmodelcontrol/mechanisms/coordinates/joints.py +51 -0
  25. virtualmodelcontrol/mechanisms/coordinates/ops.py +146 -0
  26. virtualmodelcontrol/mechanisms/coordinates/references.py +43 -0
  27. virtualmodelcontrol/mechanisms/mechanism.py +88 -0
  28. virtualmodelcontrol/models/__init__.py +22 -0
  29. virtualmodelcontrol/models/actuation.py +196 -0
  30. virtualmodelcontrol/models/assembly.py +154 -0
  31. virtualmodelcontrol/models/continuum/__init__.py +5 -0
  32. virtualmodelcontrol/models/continuum/pcc.py +120 -0
  33. virtualmodelcontrol/models/kinematic.py +41 -0
  34. virtualmodelcontrol/models/rigid/__init__.py +6 -0
  35. virtualmodelcontrol/models/rigid/couplings.py +64 -0
  36. virtualmodelcontrol/models/rigid/poe.py +95 -0
  37. virtualmodelcontrol/py.typed +0 -0
  38. virtualmodelcontrol/robots/__init__.py +5 -0
  39. virtualmodelcontrol/robots/adapt.py +98 -0
  40. virtualmodelcontrol/robots/helyx.py +79 -0
  41. virtualmodelcontrol/sim/__init__.py +7 -0
  42. virtualmodelcontrol/sim/model_plant.py +79 -0
  43. virtualmodelcontrol/sim/plant.py +40 -0
  44. virtualmodelcontrol/sim/run.py +73 -0
  45. virtualmodelcontrol/system.py +55 -0
  46. virtualmodelcontrol-0.1.0.dist-info/METADATA +82 -0
  47. virtualmodelcontrol-0.1.0.dist-info/RECORD +49 -0
  48. virtualmodelcontrol-0.1.0.dist-info/WHEEL +4 -0
  49. virtualmodelcontrol-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,56 @@
1
+ """Inertance components: masses and inertias, with kinetic energy ½ ẏᵀ M ẏ."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import casadi as ca
8
+ import numpy as np
9
+
10
+ from ...core.registry import register
11
+ from ...core.units import KG, inertance_unit
12
+ from ..coordinates.base import Context, Coordinate
13
+ from .base import Component
14
+
15
+
16
+ @register("component", "point_mass")
17
+ class PointMass(Component):
18
+ """A mass m [kg] at a point coordinate; a ``design`` Param (a physical property)."""
19
+
20
+ kind = "inertance"
21
+
22
+ def __init__(self, coord: Coordinate, mass: Any) -> None:
23
+ super().__init__(coord)
24
+ self.mass = self._param("mass", mass, unit=KG, scope="design")
25
+
26
+ def inertance(self, ctx: Context, y: Any) -> Any:
27
+ """m I."""
28
+ return ctx.param(self.mass) * ca.DM.eye(self.coord.dim)
29
+
30
+
31
+ @register("component", "inertance")
32
+ class Inertance(Component):
33
+ """Constant inertance M on a coordinate: a scalar, one value per axis, or a matrix.
34
+
35
+ An ``episode`` Param: a virtual flywheel's inertia, say [kg·m² on angles].
36
+ """
37
+
38
+ kind = "inertance"
39
+
40
+ def __init__(self, coord: Coordinate, inertance: Any) -> None:
41
+ super().__init__(coord)
42
+ matrix = np.ndim(getattr(inertance, "value", inertance)) == 2
43
+ self.inertia = self._param(
44
+ "inertance",
45
+ inertance,
46
+ unit=inertance_unit(coord.unit),
47
+ scope="episode",
48
+ bounds=(-np.inf, np.inf) if matrix else (0.0, np.inf),
49
+ )
50
+
51
+ def inertance(self, ctx: Context, y: Any) -> Any:
52
+ """M as a (dim, dim) matrix."""
53
+ M = ctx.param(self.inertia)
54
+ if M.shape[1] == 1:
55
+ return ca.diag(M * ca.DM.ones(self.coord.dim, 1))
56
+ return M
@@ -0,0 +1,63 @@
1
+ """Source components: forces whose power is metered in the energy accounting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import casadi as ca
8
+ import numpy as np
9
+
10
+ from ...core.registry import register
11
+ from ...core.units import M_S2, force_unit
12
+ from ..coordinates.base import Context, Coordinate
13
+ from ..coordinates.ops import Stack
14
+ from .base import Component
15
+ from .inertance import PointMass
16
+
17
+
18
+ @register("component", "force_source")
19
+ class ForceSource(Component):
20
+ """A force f on a coordinate, held in a live Param; its power f·ẏ is metered."""
21
+
22
+ kind = "source"
23
+
24
+ def __init__(self, coord: Coordinate, force: Any) -> None:
25
+ super().__init__(coord)
26
+ free = (-np.inf, np.inf)
27
+ self.force_value = self._param(
28
+ "force", force, unit=force_unit(coord.unit), scope="stage", bounds=free
29
+ )
30
+
31
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
32
+ """The Param, as a column."""
33
+ return ca.reshape(ctx.param(self.force_value), self.coord.dim, 1)
34
+
35
+
36
+ @register("component", "gravity_compensation")
37
+ class GravityCompensation(Component):
38
+ """Forces cancelling gravity on a robot's point masses, f_i = −m_i g (a metered source).
39
+
40
+ ``gravity`` g is a vector in the robot's base frame [m/s²]; by default the robot's own
41
+ ``gravity`` Param, shared so both always agree.
42
+ """
43
+
44
+ kind = "source"
45
+
46
+ def __init__(self, robot: Any, gravity: Any = None) -> None:
47
+ masses = [c for c in robot.components.values() if isinstance(c, PointMass)]
48
+ if not masses:
49
+ raise ValueError(f"robot {robot.name!r} has no PointMass components")
50
+ super().__init__(Stack(*[m.coord for m in masses]))
51
+ self.masses = masses
52
+ if gravity is None:
53
+ if "gravity" not in robot.params:
54
+ raise ValueError(f"robot {robot.name!r} has no 'gravity' Param; pass gravity=...")
55
+ gravity = robot.params["gravity"]
56
+ self.gravity = self._param(
57
+ "gravity", gravity, unit=M_S2, scope="design", bounds=(-np.inf, np.inf)
58
+ )
59
+
60
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
61
+ """−m_i g on each mass, stacked."""
62
+ g = ca.reshape(ctx.param(self.gravity), 3, 1)
63
+ return ca.vertcat(*[-ctx.param(m.mass) * g for m in self.masses])
@@ -0,0 +1,272 @@
1
+ """Storage components: springs with an energy V(y) ≥ 0 and the force f = −∂V/∂y.
2
+
3
+ The deflection is y = x − x_ref, so a spring pulls x towards x_ref. Gains are Params: ``stage``
4
+ scope (live) for stiffnesses and shapes, ``episode`` for limits and exponents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import casadi as ca
12
+ import numpy as np
13
+
14
+ from ...core.registry import register
15
+ from ...core.symbolic import logcosh, quad, smooth_norm
16
+ from ...core.units import stiffness_unit
17
+ from ..coordinates.base import Context, Coordinate
18
+ from .base import Component, scaled
19
+
20
+
21
+ @register("component", "linear_spring")
22
+ class LinearSpring(Component):
23
+ """Linear spring, V = ½ yᵀ K y and f = −K y.
24
+
25
+ ``stiffness`` K is a scalar, one value per axis, or a matrix (its symmetric part acts)
26
+ [N/m, or N·m/rad on angles].
27
+ """
28
+
29
+ kind = "storage"
30
+
31
+ def __init__(self, coord: Coordinate, stiffness: Any) -> None:
32
+ super().__init__(coord)
33
+ matrix = np.ndim(getattr(stiffness, "value", stiffness)) == 2
34
+ self.stiffness = self._param(
35
+ "stiffness",
36
+ stiffness,
37
+ unit=stiffness_unit(coord.unit),
38
+ scope="stage",
39
+ bounds=(-np.inf, np.inf) if matrix else (0.0, np.inf),
40
+ )
41
+
42
+ def energy(self, ctx: Context, y: Any) -> Any:
43
+ """½ yᵀ K y."""
44
+ return 0.5 * ca.dot(y, scaled(ctx.param(self.stiffness), y))
45
+
46
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
47
+ """−K y (symmetric part of K for a matrix)."""
48
+ K = ctx.param(self.stiffness)
49
+ if K.shape[1] == 1:
50
+ return -K * y
51
+ return -0.5 * (ca.mtimes(K, y) + ca.mtimes(K.T, y))
52
+
53
+
54
+ @register("component", "tanh_spring")
55
+ class TanhSpring(Component):
56
+ """Saturating spring, per axis: f = −F tanh(k y / F), within ±F on each axis.
57
+
58
+ ``stiffness`` k is the slope at y = 0, a scalar or one value per axis; ``max_force`` F [N].
59
+ """
60
+
61
+ kind = "storage"
62
+
63
+ def __init__(self, coord: Coordinate, stiffness: Any, max_force: Any) -> None:
64
+ super().__init__(coord)
65
+ self.stiffness = self._param(
66
+ "stiffness", stiffness, unit=stiffness_unit(coord.unit), scope="stage"
67
+ )
68
+ self.max_force = self._param("max_force", max_force, unit="N", scope="stage")
69
+
70
+ def energy(self, ctx: Context, y: Any) -> Any:
71
+ """Σ (F²/k) log cosh(k y / F); zero on axes with k = 0."""
72
+ k = ctx.param(self.stiffness) * ca.DM.ones(self.coord.dim, 1)
73
+ F = ctx.param(self.max_force)
74
+ zero = ca.DM.zeros(self.coord.dim, 1)
75
+ return ca.sum1(ca.if_else(k > 0, F**2 / k * logcosh(k * y / F), zero))
76
+
77
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
78
+ """−F tanh(k y / F)."""
79
+ k, F = ctx.param(self.stiffness), ctx.param(self.max_force)
80
+ return -F * ca.tanh(k * y / F)
81
+
82
+
83
+ @register("component", "gaussian_spring")
84
+ class GaussianSpring(Component):
85
+ """Repulsive Gaussian, V = A σ² exp(−‖y‖²/2σ²) and f = A exp(−‖y‖²/2σ²) y.
86
+
87
+ With y = x − x_obstacle it pushes x away from the obstacle. ``strength`` A [N/m], ``sigma``
88
+ σ in the coordinate's unit.
89
+ """
90
+
91
+ kind = "storage"
92
+
93
+ def __init__(self, coord: Coordinate, strength: Any, sigma: Any) -> None:
94
+ super().__init__(coord)
95
+ self.strength = self._param(
96
+ "strength", strength, unit=stiffness_unit(coord.unit), scope="stage"
97
+ )
98
+ self.sigma = self._param("sigma", sigma, unit=coord.unit, scope="stage")
99
+
100
+ def energy(self, ctx: Context, y: Any) -> Any:
101
+ """A σ² exp(−‖y‖²/2σ²)."""
102
+ A, s = ctx.param(self.strength), ctx.param(self.sigma)
103
+ return A * s**2 * ca.exp(-ca.sumsqr(y) / (2 * s**2))
104
+
105
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
106
+ """A exp(−‖y‖²/2σ²) y."""
107
+ A, s = ctx.param(self.strength), ctx.param(self.sigma)
108
+ return A * ca.exp(-ca.sumsqr(y) / (2 * s**2)) * y
109
+
110
+
111
+ @register("component", "sigmoid_spring")
112
+ class SigmoidSpring(Component):
113
+ """Spring stiffening with distance: f = −k(d) y, k(d) = k_min + (k_max − k_min) σ(α(d − d₀)).
114
+
115
+ d is each axis' magnitude (``element_wise``) or the norm ‖y‖. The energy has no closed form
116
+ and is integrated by Gauss–Legendre quadrature with ``nodes`` points; the force is exact.
117
+ """
118
+
119
+ kind = "storage"
120
+
121
+ def __init__(
122
+ self,
123
+ coord: Coordinate,
124
+ k_min: Any,
125
+ k_max: Any,
126
+ threshold: Any,
127
+ alpha: Any,
128
+ *,
129
+ element_wise: bool = True,
130
+ nodes: int = 32,
131
+ ) -> None:
132
+ super().__init__(coord)
133
+ unit = stiffness_unit(coord.unit)
134
+ self.k_min = self._param("k_min", k_min, unit=unit, scope="stage")
135
+ self.k_max = self._param("k_max", k_max, unit=unit, scope="stage")
136
+ self.threshold = self._param("threshold", threshold, unit=coord.unit, scope="stage")
137
+ inv = f"1/{coord.unit}" if coord.unit else ""
138
+ self.alpha = self._param("alpha", alpha, unit=inv, scope="stage")
139
+ self.element_wise = element_wise
140
+ self.nodes = nodes
141
+
142
+ def _stiffness(self, ctx: Context, d: Any) -> Any:
143
+ k0, k1 = ctx.param(self.k_min), ctx.param(self.k_max)
144
+ d0, a = ctx.param(self.threshold), ctx.param(self.alpha)
145
+ return k0 + (k1 - k0) / (1 + ca.exp(-a * (d - d0)))
146
+
147
+ def _distance(self, y: Any) -> Any:
148
+ return ca.fabs(y) if self.element_wise else smooth_norm(y)
149
+
150
+ def energy(self, ctx: Context, y: Any) -> Any:
151
+ """Σ ∫₀^d k(r) r dr, by quadrature."""
152
+ d = self._distance(y)
153
+ integral = quad(lambda t: self._stiffness(ctx, d * t) * t, 0.0, 1.0, self.nodes)
154
+ return ca.sum1(d**2 * integral)
155
+
156
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
157
+ """−k(d) y."""
158
+ return -self._stiffness(ctx, self._distance(y)) * y
159
+
160
+
161
+ @register("component", "polynomial_spring")
162
+ class PolynomialSpring(Component):
163
+ """Spring with stiffness k = K (d / d₀)^n: f = −k y, V = K d^(n+2) / ((n+2) d₀ⁿ).
164
+
165
+ d is each axis' magnitude (``element_wise``) or the norm ‖y‖. ``stiffness`` K is the
166
+ stiffness at d = d₀.
167
+ """
168
+
169
+ kind = "storage"
170
+
171
+ def __init__(
172
+ self,
173
+ coord: Coordinate,
174
+ stiffness: Any,
175
+ order: Any,
176
+ dist_norm: Any,
177
+ *,
178
+ element_wise: bool = True,
179
+ ) -> None:
180
+ super().__init__(coord)
181
+ self.stiffness = self._param(
182
+ "stiffness", stiffness, unit=stiffness_unit(coord.unit), scope="stage"
183
+ )
184
+ self.order = self._param("order", order, unit="", scope="episode")
185
+ self.dist_norm = self._param("dist_norm", dist_norm, unit=coord.unit, scope="stage")
186
+ self.element_wise = element_wise
187
+
188
+ def _distance(self, y: Any) -> Any:
189
+ return ca.fabs(y) if self.element_wise else smooth_norm(y)
190
+
191
+ def energy(self, ctx: Context, y: Any) -> Any:
192
+ """Σ K d^(n+2) / ((n+2) d₀ⁿ)."""
193
+ K, n, d0 = ctx.param(self.stiffness), ctx.param(self.order), ctx.param(self.dist_norm)
194
+ return ca.sum1(K * self._distance(y) ** (n + 2) / ((n + 2) * d0**n))
195
+
196
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
197
+ """−K (d / d₀)ⁿ y."""
198
+ K, n, d0 = ctx.param(self.stiffness), ctx.param(self.order), ctx.param(self.dist_norm)
199
+ return -K * (self._distance(y) / d0) ** n * y
200
+
201
+
202
+ @register("component", "limit_spring")
203
+ class LimitSpring(Component):
204
+ """Zero force inside [lower, upper] and linear outside, per axis (a joint-limit spring).
205
+
206
+ V = ½ k (max(0, lower − y)² + max(0, y − upper)²).
207
+ """
208
+
209
+ kind = "storage"
210
+
211
+ def __init__(self, coord: Coordinate, stiffness: Any, lower: Any, upper: Any) -> None:
212
+ super().__init__(coord)
213
+ self.stiffness = self._param(
214
+ "stiffness", stiffness, unit=stiffness_unit(coord.unit), scope="stage"
215
+ )
216
+ free = (-np.inf, np.inf)
217
+ self.lower = self._param("lower", lower, unit=coord.unit, scope="episode", bounds=free)
218
+ self.upper = self._param("upper", upper, unit=coord.unit, scope="episode", bounds=free)
219
+
220
+ def energy(self, ctx: Context, y: Any) -> Any:
221
+ """½ k (max(0, lower − y)² + max(0, y − upper)²)."""
222
+ k = ctx.param(self.stiffness)
223
+ below = ca.fmax(0, ctx.param(self.lower) - y)
224
+ above = ca.fmax(0, y - ctx.param(self.upper))
225
+ return ca.sum1(0.5 * k * (below**2 + above**2))
226
+
227
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
228
+ """k (max(0, lower − y) − max(0, y − upper))."""
229
+ k = ctx.param(self.stiffness)
230
+ below = ca.fmax(0, ctx.param(self.lower) - y)
231
+ above = ca.fmax(0, y - ctx.param(self.upper))
232
+ return k * (below - above)
233
+
234
+
235
+ @register("component", "gravity")
236
+ class Gravity(Component):
237
+ """Gravity on a robot's point masses: V = −Σ m_i gᵀ p_i, f_i = m_i g (a physical component).
238
+
239
+ Unlike a spring, V has no lower bound. ``gravity`` g [m/s², base frame] defaults to the
240
+ robot's ``gravity`` Param.
241
+ """
242
+
243
+ kind = "storage"
244
+
245
+ def __init__(self, robot: Any, gravity: Any = None) -> None:
246
+ from ..coordinates.ops import Stack
247
+ from .inertance import PointMass
248
+
249
+ masses = [c for c in robot.components.values() if isinstance(c, PointMass)]
250
+ if not masses:
251
+ raise ValueError(f"robot {robot.name!r} has no PointMass components")
252
+ super().__init__(Stack(*[m.coord for m in masses]))
253
+ self.masses = masses
254
+ if gravity is None:
255
+ if "gravity" not in robot.params:
256
+ raise ValueError(f"robot {robot.name!r} has no 'gravity' Param; pass gravity=...")
257
+ gravity = robot.params["gravity"]
258
+ self.gravity = self._param(
259
+ "gravity", gravity, unit="m/s^2", scope="design", bounds=(-np.inf, np.inf)
260
+ )
261
+
262
+ def _weights(self, ctx: Context) -> Any:
263
+ g = ca.reshape(ctx.param(self.gravity), 3, 1)
264
+ return ca.vertcat(*[ctx.param(m.mass) * g for m in self.masses])
265
+
266
+ def energy(self, ctx: Context, y: Any) -> Any:
267
+ """−Σ m_i gᵀ p_i."""
268
+ return -ca.dot(self._weights(ctx), y)
269
+
270
+ def force(self, ctx: Context, y: Any, yd: Any) -> Any:
271
+ """m_i g on each mass, stacked."""
272
+ return self._weights(ctx)
@@ -0,0 +1,24 @@
1
+ """Coordinates: what components act on, built from q, the virtual states z, Params and time."""
2
+
3
+ from .base import Context, Coordinate, as_coordinate, walk
4
+ from .frames import FramePoint
5
+ from .joints import Joint, State
6
+ from .ops import Custom, Difference, Norm, Projection, Slice, Stack
7
+ from .references import Ref
8
+
9
+ __all__ = [
10
+ "Context",
11
+ "Coordinate",
12
+ "Custom",
13
+ "Difference",
14
+ "FramePoint",
15
+ "Joint",
16
+ "Norm",
17
+ "Projection",
18
+ "Ref",
19
+ "Slice",
20
+ "Stack",
21
+ "State",
22
+ "as_coordinate",
23
+ "walk",
24
+ ]
@@ -0,0 +1,117 @@
1
+ """Coordinates: quantities y(q, z, p, t) that components act on, and where they are evaluated."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from typing import Any
7
+
8
+ from ...core.params import Binding, Param
9
+
10
+
11
+ class Context:
12
+ """Symbols a coordinate is evaluated on: robot configuration q, virtual positions z, time t.
13
+
14
+ Params resolve through ``binding`` (live symbols or folded constants). Values are memoized
15
+ per coordinate object, so a coordinate shared by several components is built once.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ q: Any,
21
+ binding: Binding,
22
+ z: Any = None,
23
+ t: Any = None,
24
+ states: Mapping[int, slice] | None = None,
25
+ ) -> None:
26
+ self.q = q
27
+ self.z = z
28
+ self.t = t
29
+ self.binding = binding
30
+ self._states = dict(states or {})
31
+ self._cache: dict[int, Any] = {}
32
+
33
+ def param(self, param: Param) -> Any:
34
+ """Expression of a Param."""
35
+ return self.binding(param)
36
+
37
+ def view(self, params: Mapping[str, Param]) -> dict[str, Any]:
38
+ """Expressions of a model's Params, keyed by the model's own names."""
39
+ return self.binding.view(params)
40
+
41
+ def state(self, state: Coordinate) -> Any:
42
+ """Slice of z holding a virtual state."""
43
+ try:
44
+ return self.z[self._states[id(state)]]
45
+ except KeyError:
46
+ raise KeyError(f"{state!r} is not a state of this system") from None
47
+
48
+ def value(self, coord: Coordinate) -> Any:
49
+ """Value of ``coord`` (dim × 1), built once per context."""
50
+ key = id(coord)
51
+ if key not in self._cache:
52
+ self._cache[key] = coord.value(self)
53
+ return self._cache[key]
54
+
55
+
56
+ class Coordinate:
57
+ """A quantity y with ``dim`` entries in ``unit``, computed from q, z, the Params and t.
58
+
59
+ Subtracting coordinates gives a ``Difference``; a plain array on either side becomes a
60
+ ``Ref`` (a live reference). Indexing gives a ``Slice``.
61
+ """
62
+
63
+ def __init__(self, dim: int, unit: str = "") -> None:
64
+ self.dim = int(dim)
65
+ self.unit = unit
66
+
67
+ def value(self, ctx: Context) -> Any:
68
+ """Symbolic value, shape (dim, 1)."""
69
+ raise NotImplementedError
70
+
71
+ def params(self) -> dict[str, Param]:
72
+ """Params owned by this coordinate, by local name."""
73
+ return {}
74
+
75
+ def children(self) -> tuple[Coordinate, ...]:
76
+ """Coordinates this one is built from."""
77
+ return ()
78
+
79
+ def __sub__(self, other: Any) -> Coordinate:
80
+ from .ops import Difference
81
+
82
+ return Difference(self, as_coordinate(other, self))
83
+
84
+ def __rsub__(self, other: Any) -> Coordinate:
85
+ from .ops import Difference
86
+
87
+ return Difference(as_coordinate(other, self), self)
88
+
89
+ def __getitem__(self, index: Any) -> Coordinate:
90
+ from .ops import Slice
91
+
92
+ return Slice(self, index)
93
+
94
+ def __repr__(self) -> str:
95
+ return f"{type(self).__name__}(dim={self.dim}, unit={self.unit!r})"
96
+
97
+
98
+ def as_coordinate(x: Any, like: Coordinate) -> Coordinate:
99
+ """``x`` itself if it is a coordinate, else a live reference shaped like ``like``."""
100
+ if isinstance(x, Coordinate):
101
+ return x
102
+ from .references import Ref
103
+
104
+ return Ref("ref", value=x, unit=like.unit)
105
+
106
+
107
+ def walk(coord: Coordinate) -> Iterator[Coordinate]:
108
+ """Every coordinate in the tree of ``coord``, each once, parents first."""
109
+ seen: set[int] = set()
110
+ stack = [coord]
111
+ while stack:
112
+ node = stack.pop()
113
+ if id(node) in seen:
114
+ continue
115
+ seen.add(id(node))
116
+ yield node
117
+ stack.extend(reversed(node.children()))
@@ -0,0 +1,62 @@
1
+ """Points fixed to a model's frames, at a named site or at an arc parameter s."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import casadi as ca
8
+
9
+ from ...core.params import Param, as_param
10
+ from .base import Context, Coordinate
11
+
12
+
13
+ class FramePoint(Coordinate):
14
+ """Position [m] of a point fixed to a model frame: a named site, or arc parameter ``s``.
15
+
16
+ ``s`` and ``offset`` (in the frame, [m]) become ``episode`` Params, folded in at compile time
17
+ unless made live. Give both ``at`` and ``s`` for a continuous part of an assembly.
18
+ """
19
+
20
+ def __init__(self, model: Any, at: str | None = None, *, s: Any = None, offset: Any = None):
21
+ if at is None and s is None:
22
+ raise ValueError("give a site name `at`, an arc parameter `s`, or both")
23
+ super().__init__(3, "m")
24
+ self.model = model
25
+ self.site = at
26
+ self.s = None if s is None else as_param(s, "s", bounds=(0.0, 1.0), scope="episode")
27
+ self.offset = (
28
+ None if offset is None else as_param(offset, "offset", unit="m", scope="episode")
29
+ )
30
+
31
+ def params(self) -> dict[str, Param]:
32
+ """The arc parameter and the offset, when given."""
33
+ out = {}
34
+ if self.s is not None:
35
+ out["s"] = self.s
36
+ if self.offset is not None:
37
+ out["offset"] = self.offset
38
+ return out
39
+
40
+ def value(self, ctx: Context) -> Any:
41
+ """Point position in the model's base frame."""
42
+ if self.s is None:
43
+ at: Any = self.site
44
+ elif self.site is None:
45
+ at = ctx.param(self.s)
46
+ else:
47
+ at = (self.site, ctx.param(self.s))
48
+ R, p = self.model.frame(ctx.q, at, ctx.view(self.model.params))
49
+ if self.offset is not None:
50
+ p = p + ca.mtimes(R, ca.reshape(ctx.param(self.offset), 3, 1))
51
+ return p
52
+
53
+ def __repr__(self) -> str:
54
+ where = ", ".join(
55
+ x
56
+ for x in (
57
+ repr(self.site) if self.site else "",
58
+ f"s={self.s.value.tolist()}" if self.s is not None else "",
59
+ )
60
+ if x
61
+ )
62
+ return f"FramePoint({where})"
@@ -0,0 +1,51 @@
1
+ """Joint coordinates of the robot, and states of virtual degrees of freedom."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any
7
+
8
+ import casadi as ca
9
+ import numpy as np
10
+ from numpy.typing import ArrayLike
11
+
12
+ from .base import Context, Coordinate
13
+
14
+
15
+ class Joint(Coordinate):
16
+ """Entries of the robot's generalized coordinates q: an index, a slice or a list of indices."""
17
+
18
+ def __init__(self, index: int | slice | Sequence[int], unit: str = "") -> None:
19
+ if isinstance(index, slice):
20
+ if index.stop is None:
21
+ raise ValueError("a Joint slice needs an explicit stop")
22
+ indices = list(range(index.stop))[index]
23
+ elif isinstance(index, int):
24
+ indices = [index]
25
+ else:
26
+ indices = [int(i) for i in index]
27
+ super().__init__(len(indices), unit)
28
+ self.indices = indices
29
+
30
+ def value(self, ctx: Context) -> Any:
31
+ """Selected entries of q."""
32
+ return ca.vertcat(*[ctx.q[i] for i in self.indices])
33
+
34
+ def __repr__(self) -> str:
35
+ return f"Joint({self.indices}, unit={self.unit!r})"
36
+
37
+
38
+ class State(Coordinate):
39
+ """Position of a virtual degree of freedom owned by a controller (part of its state z)."""
40
+
41
+ def __init__(self, name: str, dim: int = 1, unit: str = "", initial: ArrayLike = 0.0) -> None:
42
+ super().__init__(dim, unit)
43
+ self.name = name
44
+ self.initial = np.broadcast_to(np.asarray(initial, dtype=float), (dim,)).copy()
45
+
46
+ def value(self, ctx: Context) -> Any:
47
+ """Slice of z."""
48
+ return ctx.state(self)
49
+
50
+ def __repr__(self) -> str:
51
+ return f"State({self.name!r}, dim={self.dim}, unit={self.unit!r})"