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,146 @@
1
+ """Coordinates built from other coordinates: differences, slices, stacks, projections, norms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from typing import Any
7
+
8
+ import casadi as ca
9
+
10
+ from ...core.params import Param, Scope, as_param
11
+ from ...core.symbolic import smooth_norm
12
+ from .base import Context, Coordinate
13
+
14
+
15
+ class Difference(Coordinate):
16
+ """a − b, e.g. a point minus its goal (the deflection of a spring)."""
17
+
18
+ def __init__(self, a: Coordinate, b: Coordinate) -> None:
19
+ if a.dim != b.dim:
20
+ raise ValueError(f"cannot subtract a {b.dim}-entry coordinate from a {a.dim}-entry one")
21
+ super().__init__(a.dim, a.unit)
22
+ self.a, self.b = a, b
23
+
24
+ def children(self) -> tuple[Coordinate, ...]:
25
+ """The two operands."""
26
+ return (self.a, self.b)
27
+
28
+ def value(self, ctx: Context) -> Any:
29
+ """a − b."""
30
+ return ctx.value(self.a) - ctx.value(self.b)
31
+
32
+
33
+ class Slice(Coordinate):
34
+ """Some entries of a coordinate."""
35
+
36
+ def __init__(self, coord: Coordinate, index: int | slice | Sequence[int]) -> None:
37
+ all_indices = list(range(coord.dim))
38
+ if isinstance(index, slice):
39
+ indices = all_indices[index]
40
+ elif isinstance(index, int):
41
+ indices = [all_indices[index]]
42
+ else:
43
+ indices = [all_indices[i] for i in index]
44
+ super().__init__(len(indices), coord.unit)
45
+ self.coord, self.indices = coord, indices
46
+
47
+ def children(self) -> tuple[Coordinate, ...]:
48
+ """The sliced coordinate."""
49
+ return (self.coord,)
50
+
51
+ def value(self, ctx: Context) -> Any:
52
+ """Selected entries."""
53
+ v = ctx.value(self.coord)
54
+ return ca.vertcat(*[v[i] for i in self.indices])
55
+
56
+
57
+ class Stack(Coordinate):
58
+ """Several coordinates stacked into one."""
59
+
60
+ def __init__(self, *coords: Coordinate) -> None:
61
+ units = {c.unit for c in coords}
62
+ super().__init__(sum(c.dim for c in coords), units.pop() if len(units) == 1 else "")
63
+ self.coords = coords
64
+
65
+ def children(self) -> tuple[Coordinate, ...]:
66
+ """The stacked coordinates."""
67
+ return self.coords
68
+
69
+ def value(self, ctx: Context) -> Any:
70
+ """Values stacked in order."""
71
+ return ca.vertcat(*[ctx.value(c) for c in self.coords])
72
+
73
+
74
+ class Projection(Coordinate):
75
+ """Signed length of a coordinate along a direction: y = n̂ᵀ c with n̂ = n / ‖n‖.
76
+
77
+ A spring on a projection acts along n only (a cart). The direction is an ``episode`` Param.
78
+ """
79
+
80
+ def __init__(self, coord: Coordinate, direction: Any, *, scope: Scope = "episode") -> None:
81
+ super().__init__(1, coord.unit)
82
+ self.coord = coord
83
+ self.direction = as_param(direction, "direction", scope=scope)
84
+ if self.direction.size != coord.dim:
85
+ raise ValueError(f"direction needs {coord.dim} entries, got {self.direction.size}")
86
+
87
+ def params(self) -> dict[str, Param]:
88
+ """The direction."""
89
+ return {"direction": self.direction}
90
+
91
+ def children(self) -> tuple[Coordinate, ...]:
92
+ """The projected coordinate."""
93
+ return (self.coord,)
94
+
95
+ def value(self, ctx: Context) -> Any:
96
+ """n̂ᵀ c."""
97
+ n = ca.reshape(ctx.param(self.direction), self.coord.dim, 1)
98
+ return ca.dot(n, ctx.value(self.coord)) / ca.norm_2(n)
99
+
100
+
101
+ class Norm(Coordinate):
102
+ """Smooth Euclidean length of a coordinate, √(cᵀc + ε)."""
103
+
104
+ def __init__(self, coord: Coordinate, eps: float = 1e-18) -> None:
105
+ super().__init__(1, coord.unit)
106
+ self.coord, self.eps = coord, eps
107
+
108
+ def children(self) -> tuple[Coordinate, ...]:
109
+ """The measured coordinate."""
110
+ return (self.coord,)
111
+
112
+ def value(self, ctx: Context) -> Any:
113
+ """√(cᵀc + ε)."""
114
+ return smooth_norm(ctx.value(self.coord), self.eps)
115
+
116
+
117
+ class Custom(Coordinate):
118
+ """y = fn(child values, Param expressions by name), with ``fn`` written in CasADi operations."""
119
+
120
+ def __init__(
121
+ self,
122
+ fn: Callable[..., Any],
123
+ children: Sequence[Coordinate] = (),
124
+ *,
125
+ dim: int,
126
+ unit: str = "",
127
+ params: dict[str, Any] | None = None,
128
+ ) -> None:
129
+ super().__init__(dim, unit)
130
+ self.fn = fn
131
+ self._children = tuple(children)
132
+ self._params = {name: as_param(v, name) for name, v in (params or {}).items()}
133
+
134
+ def params(self) -> dict[str, Param]:
135
+ """Params passed to ``fn`` by keyword."""
136
+ return dict(self._params)
137
+
138
+ def children(self) -> tuple[Coordinate, ...]:
139
+ """Coordinates passed to ``fn`` in order."""
140
+ return self._children
141
+
142
+ def value(self, ctx: Context) -> Any:
143
+ """``fn`` applied to the children's values and the Params."""
144
+ args = [ctx.value(c) for c in self._children]
145
+ kwargs = {name: ctx.param(p) for name, p in self._params.items()}
146
+ return ca.reshape(self.fn(*args, **kwargs), self.dim, 1)
@@ -0,0 +1,43 @@
1
+ """References: coordinates held in Params, such as goals and obstacle positions."""
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.params import Param, Scope
11
+ from .base import Context, Coordinate
12
+
13
+
14
+ class Ref(Coordinate):
15
+ """A reference value held in a Param; ``stage`` scope, so live at every step by default."""
16
+
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ dim: int | None = None,
21
+ value: Any = None,
22
+ *,
23
+ unit: str = "m",
24
+ scope: Scope = "stage",
25
+ ) -> None:
26
+ if isinstance(value, Param):
27
+ param = value
28
+ else:
29
+ if value is None:
30
+ value = np.zeros(1 if dim is None else dim)
31
+ param = Param(name, np.ravel(value), unit=unit, scope=scope)
32
+ if dim is not None and dim != param.size:
33
+ raise ValueError(f"Ref {name!r}: dim {dim} but the value has {param.size} entries")
34
+ super().__init__(param.size, param.unit)
35
+ self.param = param
36
+
37
+ def params(self) -> dict[str, Param]:
38
+ """The reference Param."""
39
+ return {self.param.name: self.param}
40
+
41
+ def value(self, ctx: Context) -> Any:
42
+ """The Param as a column."""
43
+ return ca.reshape(ctx.param(self.param), self.dim, 1)
@@ -0,0 +1,88 @@
1
+ """Mechanism: coordinates plus components, for a physical robot and a virtual one alike."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from numpy.typing import ArrayLike
8
+
9
+ from ..core.params import Param, ParamSet
10
+ from .components.base import Component
11
+ from .coordinates.base import walk
12
+ from .coordinates.frames import FramePoint
13
+ from .coordinates.joints import Joint, State
14
+
15
+
16
+ class Mechanism:
17
+ """Named components acting on coordinates.
18
+
19
+ A robot mechanism has a kinematic ``model`` (and optionally an ``actuation``); its components
20
+ are physical (masses, identified stiffness). A controller is a mechanism without a model:
21
+ its components act on the robot's coordinates and on its own virtual states.
22
+ """
23
+
24
+ def __init__(self, name: str, model: Any = None, actuation: Any = None) -> None:
25
+ self.name = name
26
+ self.model = model
27
+ self.actuation = actuation
28
+ self.components: dict[str, Component] = {}
29
+ self.states: dict[str, State] = {}
30
+ self._own_params: dict[str, Param] = {}
31
+
32
+ def add(self, name: str, component: Component) -> Component:
33
+ """Add ``component`` under ``name`` and return it."""
34
+ if name in self.components:
35
+ raise ValueError(f"{self.name!r} already has a component named {name!r}")
36
+ self.components[name] = component
37
+ return component
38
+
39
+ def add_state(self, name: str, dim: int = 1, unit: str = "", initial: ArrayLike = 0.0) -> State:
40
+ """Add a virtual degree of freedom (it needs an inertance) and return its coordinate."""
41
+ if name in self.states:
42
+ raise ValueError(f"{self.name!r} already has a state named {name!r}")
43
+ state = State(name, dim, unit, initial)
44
+ self.states[name] = state
45
+ return state
46
+
47
+ def add_param(self, param: Param) -> Param:
48
+ """Add a Param that belongs to the mechanism itself (gravity, say) and return it."""
49
+ self._own_params[param.name] = param
50
+ return param
51
+
52
+ def point(self, at: str | None = None, *, s: Any = None, offset: Any = None) -> FramePoint:
53
+ """A point of the model: a named site, or arc parameter ``s`` [0, 1]."""
54
+ if self.model is None:
55
+ raise ValueError(f"{self.name!r} has no kinematic model")
56
+ return FramePoint(self.model, at, s=s, offset=offset)
57
+
58
+ def joint(self, index: Any) -> Joint:
59
+ """Entries of the robot's generalized coordinates."""
60
+ return Joint(index, getattr(self.model, "q_unit", ""))
61
+
62
+ @property
63
+ def space(self) -> Any:
64
+ """Configuration space of the model."""
65
+ if self.model is None:
66
+ raise ValueError(f"{self.name!r} has no kinematic model")
67
+ return self.model.space
68
+
69
+ @property
70
+ def params(self) -> ParamSet:
71
+ """Every Param of the mechanism, by local name (component Params as ``name.param``)."""
72
+ ps = ParamSet()
73
+ if self.model is not None:
74
+ ps.merge(self.model.params)
75
+ if self.actuation is not None:
76
+ ps.merge(self.actuation.params)
77
+ for param in self._own_params.values():
78
+ ps.add(param)
79
+ for cname, component in self.components.items():
80
+ for local, param in component.params().items():
81
+ ps.add(param, f"{cname}.{local}", rename=True)
82
+ for coord in walk(component.coord):
83
+ for local, param in coord.params().items():
84
+ ps.add(param, f"{cname}.{local}", rename=True)
85
+ return ps
86
+
87
+ def __repr__(self) -> str:
88
+ return f"Mechanism({self.name!r}, components={list(self.components)})"
@@ -0,0 +1,22 @@
1
+ """Models: kinematics and actuation of robots."""
2
+
3
+ from .actuation import Actuation, Direct, TendonTransmission
4
+ from .assembly import Assembly, StackedActuation
5
+ from .continuum import PCC, segment_frame
6
+ from .kinematic import KinematicModel, evaluate_frame, from_dict
7
+ from .rigid import LinearCoupling, SerialChain
8
+
9
+ __all__ = [
10
+ "PCC",
11
+ "Actuation",
12
+ "Assembly",
13
+ "Direct",
14
+ "KinematicModel",
15
+ "LinearCoupling",
16
+ "SerialChain",
17
+ "StackedActuation",
18
+ "TendonTransmission",
19
+ "evaluate_frame",
20
+ "from_dict",
21
+ "segment_frame",
22
+ ]
@@ -0,0 +1,196 @@
1
+ """Actuation: how motor angles follow from q, and how motor torques u give τ = B(q) u."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any, Protocol
7
+
8
+ import casadi as ca
9
+ import numpy as np
10
+
11
+ from ..core.params import ParamSet, as_param
12
+ from ..core.registry import register
13
+ from ..core.units import RAD, M
14
+
15
+
16
+ class Actuation(Protocol):
17
+ """How motors drive a robot. Implement these to add a transmission (``register("actuation")``).
18
+
19
+ ``config_from_motors`` / ``velocity_from_motors`` invert the map when the controller reads
20
+ motors directly; ``allocate`` solves B u = τ.
21
+ """
22
+
23
+ params: ParamSet
24
+
25
+ def motor_sizes(self, space: Any) -> tuple[int, int]:
26
+ """Numbers of motor angles and motor rates."""
27
+ ...
28
+
29
+ def motor_angles(self, q: Any, p: dict[str, Any]) -> Any:
30
+ """Motor angles θ(q) [rad]."""
31
+ ...
32
+
33
+ def motor_rates(self, q: Any, v: Any, p: dict[str, Any]) -> Any:
34
+ """Motor rates θ̇ [rad/s]."""
35
+ ...
36
+
37
+ def generalized_force(self, u: Any, q: Any, p: dict[str, Any]) -> Any:
38
+ """τ = B(q) u."""
39
+ ...
40
+
41
+ def allocate(self, tau: Any, q: Any, p: dict[str, Any]) -> Any:
42
+ """Motor torques u with B(q) u = τ."""
43
+ ...
44
+
45
+ def config_from_motors(self, theta: Any, p: dict[str, Any]) -> Any:
46
+ """q from motor angles."""
47
+ ...
48
+
49
+ def velocity_from_motors(self, q: Any, theta_dot: Any, p: dict[str, Any]) -> Any:
50
+ """v from motor rates."""
51
+ ...
52
+
53
+
54
+ @register("actuation", "direct")
55
+ class Direct:
56
+ """One actuator per generalized coordinate: motor angles θ = q, B = I, u = τ."""
57
+
58
+ def __init__(self) -> None:
59
+ self.params = ParamSet()
60
+
61
+ def motor_sizes(self, space: Any) -> tuple[int, int]:
62
+ """Numbers of motor angles and motor rates: nq and nv."""
63
+ return space.nq, space.nv
64
+
65
+ def motor_angles(self, q: Any, p: dict[str, Any]) -> Any:
66
+ """θ = q."""
67
+ return q
68
+
69
+ def motor_rates(self, q: Any, v: Any, p: dict[str, Any]) -> Any:
70
+ """θ̇ = v."""
71
+ return v
72
+
73
+ def generalized_force(self, u: Any, q: Any, p: dict[str, Any]) -> Any:
74
+ """τ = u."""
75
+ return u
76
+
77
+ def allocate(self, tau: Any, q: Any, p: dict[str, Any]) -> Any:
78
+ """u = τ."""
79
+ return tau
80
+
81
+ def config_from_motors(self, theta: Any, p: dict[str, Any]) -> Any:
82
+ """q = θ."""
83
+ return theta
84
+
85
+ def velocity_from_motors(self, q: Any, theta_dot: Any, p: dict[str, Any]) -> Any:
86
+ """v = θ̇."""
87
+ return theta_dot
88
+
89
+ def to_dict(self) -> dict[str, Any]:
90
+ """No arguments."""
91
+ return {"type": "direct"}
92
+
93
+ @classmethod
94
+ def from_dict(cls, data: dict[str, Any]) -> Direct:
95
+ """Inverse of ``to_dict``."""
96
+ return cls()
97
+
98
+
99
+ @register("actuation", "tendons")
100
+ class TendonTransmission:
101
+ """Rigid tendons on PCC segments, with motor angle θ = −ΔL / r: θ > 0 pulls a tendon.
102
+
103
+ Tendon j of a segment changes length by ΔL_j = Dl − Dx cos δ_j − Dy sin δ_j. Per segment i:
104
+ ``seg{i}.delta`` tendon angles around the section at the base [rad] and ``seg{i}.r`` spool
105
+ radius [m], ``design`` Params. With three tendons per segment the map is invertible.
106
+ """
107
+
108
+ def __init__(self, delta: Sequence[Any], r: Any) -> None:
109
+ n = len(delta)
110
+ r_list = list(r) if isinstance(r, (list, tuple, np.ndarray)) else [r] * n
111
+ if len(r_list) != n:
112
+ raise ValueError(f"{n} segments of tendon angles but {len(r_list)} spool radii")
113
+ self.n_segments = n
114
+ self.params = ParamSet()
115
+ for i in range(n):
116
+ key = f"seg{i + 1}.delta"
117
+ free = (-np.inf, np.inf)
118
+ self.params.add(as_param(delta[i], key, unit=RAD, scope="design", bounds=free), key)
119
+ key = f"seg{i + 1}.r"
120
+ self.params.add(
121
+ as_param(r_list[i], key, unit=M, scope="design", bounds=(0, np.inf)), key
122
+ )
123
+ self.n_tendons = [self.params[f"seg{i + 1}.delta"].size for i in range(n)]
124
+ self.n_motors = sum(self.n_tendons)
125
+
126
+ def motor_sizes(self, space: Any) -> tuple[int, int]:
127
+ """Numbers of motor angles and motor rates: one per tendon."""
128
+ return self.n_motors, self.n_motors
129
+
130
+ def _blocks(self, p: dict[str, Any]) -> list[Any]:
131
+ """∂θ_i/∂q_i = −A_i / r_i per segment (the map is linear in q)."""
132
+ out = []
133
+ for i in range(self.n_segments):
134
+ delta, r = p[f"seg{i + 1}.delta"], p[f"seg{i + 1}.r"]
135
+ rows = [
136
+ ca.horzcat(-ca.cos(delta[j]), -ca.sin(delta[j]), 1) for j in range(delta.numel())
137
+ ]
138
+ out.append(-ca.vertcat(*rows) / r)
139
+ return out
140
+
141
+ def tendon_lengths(self, q: Any, p: dict[str, Any]) -> Any:
142
+ """Length change of every tendon, ΔL [m]."""
143
+ out = []
144
+ for i, block in enumerate(self._blocks(p)):
145
+ r = p[f"seg{i + 1}.r"]
146
+ out.append(-r * ca.mtimes(block, q[3 * i : 3 * i + 3]))
147
+ return ca.vertcat(*out)
148
+
149
+ def motor_angles(self, q: Any, p: dict[str, Any]) -> Any:
150
+ """θ = −ΔL / r [rad]."""
151
+ blocks = self._blocks(p)
152
+ return ca.vertcat(*[ca.mtimes(B, q[3 * i : 3 * i + 3]) for i, B in enumerate(blocks)])
153
+
154
+ def jacobian(self, p: dict[str, Any]) -> Any:
155
+ """∂θ/∂q, constant; the input matrix is B = (∂θ/∂q)ᵀ."""
156
+ return ca.diagcat(*self._blocks(p))
157
+
158
+ def motor_rates(self, q: Any, v: Any, p: dict[str, Any]) -> Any:
159
+ """θ̇ = (∂θ/∂q) v."""
160
+ return ca.mtimes(self.jacobian(p), v)
161
+
162
+ def generalized_force(self, u: Any, q: Any, p: dict[str, Any]) -> Any:
163
+ """τ = B u with B = (∂θ/∂q)ᵀ."""
164
+ return ca.mtimes(self.jacobian(p).T, u)
165
+
166
+ def allocate(self, tau: Any, q: Any, p: dict[str, Any]) -> Any:
167
+ """Motor torques u with B u = τ."""
168
+ blocks = self._blocks(p)
169
+ return ca.vertcat(*[ca.solve(B.T, tau[3 * i : 3 * i + 3]) for i, B in enumerate(blocks)])
170
+
171
+ def config_from_motors(self, theta: Any, p: dict[str, Any]) -> Any:
172
+ """q from motor angles (the exact inverse of ``motor_angles``)."""
173
+ out, offset = [], 0
174
+ for i, block in enumerate(self._blocks(p)):
175
+ m = self.n_tendons[i]
176
+ out.append(ca.solve(block, theta[offset : offset + m]))
177
+ offset += m
178
+ return ca.vertcat(*out)
179
+
180
+ def velocity_from_motors(self, q: Any, theta_dot: Any, p: dict[str, Any]) -> Any:
181
+ """v from motor velocities (the map is linear, so this is the same inverse)."""
182
+ return self.config_from_motors(theta_dot, p)
183
+
184
+ def to_dict(self) -> dict[str, Any]:
185
+ """Constructor arguments at the current Param values."""
186
+ n = range(1, self.n_segments + 1)
187
+ return {
188
+ "type": "tendons",
189
+ "delta": [self.params[f"seg{i}.delta"].value.tolist() for i in n],
190
+ "r": [float(self.params[f"seg{i}.r"].value) for i in n],
191
+ }
192
+
193
+ @classmethod
194
+ def from_dict(cls, data: dict[str, Any]) -> TendonTransmission:
195
+ """Inverse of ``to_dict``."""
196
+ return cls(data["delta"], data["r"])
@@ -0,0 +1,154 @@
1
+ """Assemblies: several models mounted on one base (fingers on a palm, two arms on a frame)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ import casadi as ca
9
+ import numpy as np
10
+
11
+ from ..core.params import Param, ParamSet, as_param
12
+ from ..core.registry import get, register
13
+ from ..core.space import Product
14
+ from ..core.symbolic import rotation_from_vector
15
+ from .actuation import Direct
16
+
17
+
18
+ @register("model", "assembly")
19
+ class Assembly:
20
+ """Several models mounted at fixed poses on one base; q stacks the parts' coordinates.
21
+
22
+ ``parts`` maps a name to ``(model, position, rotation)``: the mount position [m] and rotation
23
+ vector [rad] in the base frame, ``design`` Params named ``{name}.mount.position`` and
24
+ ``{name}.mount.rotation``. Sites are ``"name/site"``; for a continuous part, give
25
+ ``at="name"`` and an arc parameter s.
26
+ """
27
+
28
+ def __init__(self, parts: Mapping[str, tuple[Any, Any, Any]]) -> None:
29
+ self.parts = {name: spec[0] for name, spec in parts.items()}
30
+ self.space = Product(*[m.space for m in self.parts.values()])
31
+ self.params = ParamSet()
32
+ self._local: dict[str, dict[str, str]] = {}
33
+ self._mount: dict[str, tuple[Param, Param]] = {}
34
+ free = (-np.inf, np.inf)
35
+ for name, (model, position, rotation) in parts.items():
36
+ self.params.merge(model.params, name)
37
+ self._local[name] = {loc: self.params.name_of(par) for loc, par in model.params.items()}
38
+ pos = as_param(
39
+ position, f"{name}.mount.position", unit="m", scope="design", bounds=free
40
+ )
41
+ rot = as_param(
42
+ rotation, f"{name}.mount.rotation", unit="rad", scope="design", bounds=free
43
+ )
44
+ self.params.add(pos, f"{name}.mount.position")
45
+ self.params.add(rot, f"{name}.mount.rotation")
46
+ self._mount[name] = (pos, rot)
47
+ self._slices: dict[str, slice] = {}
48
+ offset = 0
49
+ for name, model in self.parts.items():
50
+ self._slices[name] = slice(offset, offset + model.space.nq)
51
+ offset += model.space.nq
52
+ self.sites = tuple(f"{n}/{s}" for n, m in self.parts.items() for s in m.sites)
53
+ units = {getattr(m, "q_unit", "") for m in self.parts.values()}
54
+ self.q_unit = units.pop() if len(units) == 1 else ""
55
+
56
+ def part_view(self, name: str, p: dict[str, Any]) -> dict[str, Any]:
57
+ """A part's Params under its own names."""
58
+ return {loc: p[full] for loc, full in self._local[name].items()}
59
+
60
+ def frame(self, q: Any, at: Any, p: dict[str, Any]) -> tuple[Any, Any]:
61
+ """(R, position) of ``"part/site"``, or of ``(part, s)`` for a continuous part."""
62
+ if isinstance(at, tuple):
63
+ name, where = at
64
+ else:
65
+ name, where = at.split("/", 1)
66
+ if name not in self.parts:
67
+ raise KeyError(f"unknown part {name!r}; parts: {list(self.parts)}")
68
+ R, pos = self.parts[name].frame(q[self._slices[name]], where, self.part_view(name, p))
69
+ R_m = rotation_from_vector(ca.reshape(p[f"{name}.mount.rotation"], 3, 1))
70
+ p_m = ca.reshape(p[f"{name}.mount.position"], 3, 1)
71
+ return ca.mtimes(R_m, R), p_m + ca.mtimes(R_m, pos)
72
+
73
+ def stacked_actuation(self, actuations: Mapping[str, Any] | None = None) -> StackedActuation:
74
+ """One actuation for the whole assembly: each part's own (default ``Direct``)."""
75
+ actuations = actuations or {}
76
+ return StackedActuation(
77
+ [(self.parts[n].space, actuations.get(n, Direct()), n) for n in self.parts]
78
+ )
79
+
80
+ def to_dict(self) -> dict[str, Any]:
81
+ """Each part's model and mount pose."""
82
+ return {
83
+ "type": "assembly",
84
+ "parts": {
85
+ n: [m.to_dict(), self._mount[n][0].value.tolist(), self._mount[n][1].value.tolist()]
86
+ for n, m in self.parts.items()
87
+ },
88
+ }
89
+
90
+ @classmethod
91
+ def from_dict(cls, data: dict[str, Any]) -> Assembly:
92
+ """Inverse of ``to_dict``."""
93
+ parts = {
94
+ n: (get("model", spec[0]["type"]).from_dict(spec[0]), spec[1], spec[2])
95
+ for n, spec in data["parts"].items()
96
+ }
97
+ return cls(parts)
98
+
99
+
100
+ class StackedActuation:
101
+ """Each part of an assembly driven by its own actuation; motors are stacked in part order."""
102
+
103
+ def __init__(self, parts: Sequence[tuple[Any, Any, str]]) -> None:
104
+ self.parts = list(parts)
105
+ self.params = ParamSet()
106
+ for _space, actuation, name in self.parts:
107
+ self.params.merge(actuation.params, name)
108
+ self._sizes = [(s.nq, s.nv, *a.motor_sizes(s)) for s, a, _ in self.parts]
109
+
110
+ def _view(self, i: int, p: dict[str, Any]) -> dict[str, Any]:
111
+ name, actuation = self.parts[i][2], self.parts[i][1]
112
+ return {loc: p[f"{name}.{loc}"] for loc in actuation.params}
113
+
114
+ def _split(self, x: Any, which: int) -> list[Any]:
115
+ out, offset = [], 0
116
+ for sizes in self._sizes:
117
+ out.append(x[offset : offset + sizes[which]])
118
+ offset += sizes[which]
119
+ return out
120
+
121
+ def motor_sizes(self, space: Any) -> tuple[int, int]:
122
+ """Total motor angles and rates."""
123
+ return sum(s[2] for s in self._sizes), sum(s[3] for s in self._sizes)
124
+
125
+ def _map(self, method: str, xs: list[Any], p: dict[str, Any], *extra: list[Any]) -> Any:
126
+ out = []
127
+ for i, (_space, actuation, _name) in enumerate(self.parts):
128
+ args = [x[i] for x in (xs, *extra)]
129
+ out.append(getattr(actuation, method)(*args, self._view(i, p)))
130
+ return ca.vertcat(*out)
131
+
132
+ def motor_angles(self, q: Any, p: dict[str, Any]) -> Any:
133
+ """Each part's motor angles, stacked."""
134
+ return self._map("motor_angles", self._split(q, 0), p)
135
+
136
+ def motor_rates(self, q: Any, v: Any, p: dict[str, Any]) -> Any:
137
+ """Each part's motor rates, stacked."""
138
+ return self._map("motor_rates", self._split(q, 0), p, self._split(v, 1))
139
+
140
+ def generalized_force(self, u: Any, q: Any, p: dict[str, Any]) -> Any:
141
+ """Each part's generalized force, stacked."""
142
+ return self._map("generalized_force", self._split(u, 3), p, self._split(q, 0))
143
+
144
+ def allocate(self, tau: Any, q: Any, p: dict[str, Any]) -> Any:
145
+ """Each part's motor torques, stacked."""
146
+ return self._map("allocate", self._split(tau, 1), p, self._split(q, 0))
147
+
148
+ def config_from_motors(self, theta: Any, p: dict[str, Any]) -> Any:
149
+ """Each part's configuration, stacked."""
150
+ return self._map("config_from_motors", self._split(theta, 2), p)
151
+
152
+ def velocity_from_motors(self, q: Any, theta_dot: Any, p: dict[str, Any]) -> Any:
153
+ """Each part's velocity, stacked."""
154
+ return self._map("velocity_from_motors", self._split(q, 0), p, self._split(theta_dot, 3))
@@ -0,0 +1,5 @@
1
+ """Continuum models."""
2
+
3
+ from .pcc import PCC, segment_frame
4
+
5
+ __all__ = ["PCC", "segment_frame"]