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,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))
|