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.
- virtualmodelcontrol/__init__.py +75 -0
- virtualmodelcontrol/_version.py +24 -0
- virtualmodelcontrol/compiler.py +158 -0
- virtualmodelcontrol/control/__init__.py +5 -0
- virtualmodelcontrol/control/controller.py +76 -0
- virtualmodelcontrol/core/__init__.py +24 -0
- virtualmodelcontrol/core/params.py +244 -0
- virtualmodelcontrol/core/registry.py +54 -0
- virtualmodelcontrol/core/signals.py +46 -0
- virtualmodelcontrol/core/space.py +162 -0
- virtualmodelcontrol/core/symbolic.py +67 -0
- virtualmodelcontrol/core/units.py +34 -0
- virtualmodelcontrol/dynamics.py +149 -0
- virtualmodelcontrol/mechanisms/__init__.py +67 -0
- virtualmodelcontrol/mechanisms/components/__init__.py +33 -0
- virtualmodelcontrol/mechanisms/components/base.py +71 -0
- virtualmodelcontrol/mechanisms/components/dissipation.py +50 -0
- virtualmodelcontrol/mechanisms/components/inertance.py +56 -0
- virtualmodelcontrol/mechanisms/components/sources.py +63 -0
- virtualmodelcontrol/mechanisms/components/storage.py +272 -0
- virtualmodelcontrol/mechanisms/coordinates/__init__.py +24 -0
- virtualmodelcontrol/mechanisms/coordinates/base.py +117 -0
- virtualmodelcontrol/mechanisms/coordinates/frames.py +62 -0
- virtualmodelcontrol/mechanisms/coordinates/joints.py +51 -0
- virtualmodelcontrol/mechanisms/coordinates/ops.py +146 -0
- virtualmodelcontrol/mechanisms/coordinates/references.py +43 -0
- virtualmodelcontrol/mechanisms/mechanism.py +88 -0
- virtualmodelcontrol/models/__init__.py +22 -0
- virtualmodelcontrol/models/actuation.py +196 -0
- virtualmodelcontrol/models/assembly.py +154 -0
- virtualmodelcontrol/models/continuum/__init__.py +5 -0
- virtualmodelcontrol/models/continuum/pcc.py +120 -0
- virtualmodelcontrol/models/kinematic.py +41 -0
- virtualmodelcontrol/models/rigid/__init__.py +6 -0
- virtualmodelcontrol/models/rigid/couplings.py +64 -0
- virtualmodelcontrol/models/rigid/poe.py +95 -0
- virtualmodelcontrol/py.typed +0 -0
- virtualmodelcontrol/robots/__init__.py +5 -0
- virtualmodelcontrol/robots/adapt.py +98 -0
- virtualmodelcontrol/robots/helyx.py +79 -0
- virtualmodelcontrol/sim/__init__.py +7 -0
- virtualmodelcontrol/sim/model_plant.py +79 -0
- virtualmodelcontrol/sim/plant.py +40 -0
- virtualmodelcontrol/sim/run.py +73 -0
- virtualmodelcontrol/system.py +55 -0
- virtualmodelcontrol-0.1.0.dist-info/METADATA +82 -0
- virtualmodelcontrol-0.1.0.dist-info/RECORD +49 -0
- virtualmodelcontrol-0.1.0.dist-info/WHEEL +4 -0
- 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})"
|