lockwire 0.2.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.
- lockwire/__init__.py +9 -0
- lockwire/__main__.py +9 -0
- lockwire/advice.py +119 -0
- lockwire/api.py +1216 -0
- lockwire/api_payload.py +985 -0
- lockwire/bench.py +182 -0
- lockwire/blame.py +100 -0
- lockwire/catalog.py +100 -0
- lockwire/catalog_seed.yaml +545 -0
- lockwire/checks.py +3483 -0
- lockwire/cli.py +1799 -0
- lockwire/cli_line.py +73 -0
- lockwire/constraints.py +313 -0
- lockwire/copy.py +302 -0
- lockwire/describe.py +682 -0
- lockwire/edit.py +2098 -0
- lockwire/emit_ts.py +150 -0
- lockwire/export_mhs.py +115 -0
- lockwire/factdiff.py +254 -0
- lockwire/forces.py +334 -0
- lockwire/gitref.py +188 -0
- lockwire/hw.py +388 -0
- lockwire/mcp_server.py +276 -0
- lockwire/mechanism.py +379 -0
- lockwire/mesh_dae.py +555 -0
- lockwire/mesh_simple.py +198 -0
- lockwire/page.py +1485 -0
- lockwire/plan.py +849 -0
- lockwire/plan_payload.py +151 -0
- lockwire/plugins.py +70 -0
- lockwire/report.py +848 -0
- lockwire/report_html.py +707 -0
- lockwire/report_json.py +168 -0
- lockwire/review_draft.py +315 -0
- lockwire/scan.py +732 -0
- lockwire/scene.py +535 -0
- lockwire/scene_import.py +309 -0
- lockwire/scene_urdf.py +750 -0
- lockwire/scene_view.py +376 -0
- lockwire/schema.py +3616 -0
- lockwire/sdf.py +240 -0
- lockwire/serve.py +1256 -0
- lockwire/start_operation.py +92 -0
- lockwire/statics.py +77 -0
- lockwire/units.py +115 -0
- lockwire/urdf.py +348 -0
- lockwire/vcs.py +402 -0
- lockwire/whatif.py +161 -0
- lockwire-0.2.0.dist-info/METADATA +192 -0
- lockwire-0.2.0.dist-info/RECORD +52 -0
- lockwire-0.2.0.dist-info/WHEEL +4 -0
- lockwire-0.2.0.dist-info/entry_points.txt +3 -0
lockwire/sdf.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""SDF -> Tier A extraction (`lockwire init`/`start`, F3). Stdlib only for XML.
|
|
2
|
+
|
|
3
|
+
Reuses `urdf._principal` (inertia tensor -> principal moments/axes, same trap
|
|
4
|
+
as URDF: inertia is about the COM in the inertial frame) and `urdf._model_structure_hash`
|
|
5
|
+
via `schema.py`'s validator, plus `scene_urdf`'s pure-python pose algebra (already
|
|
6
|
+
used to place SDF/URDF visuals) so the joint-origin conversion below and the
|
|
7
|
+
scene renderer agree on what a pose means.
|
|
8
|
+
|
|
9
|
+
Pose conversion (the one trap this module exists to handle):
|
|
10
|
+
|
|
11
|
+
SDF gives every `<link>` a `<pose>` that is model-relative (this build does not
|
|
12
|
+
walk `relative_to` chains — same assumption `scene_urdf._scene_from_sdf` makes).
|
|
13
|
+
A `<joint>`'s own `<pose>` is relative to the CHILD link frame unless a
|
|
14
|
+
`relative_to` attribute says otherwise (SDF spec v1.9,
|
|
15
|
+
http://sdformat.org/spec?ver=1.9&elem=joint, `pose` element description).
|
|
16
|
+
URDF's `<joint><origin>`, by contrast, *is* the parent-frame pose of the child
|
|
17
|
+
link frame — it's how URDF places the child at all.
|
|
18
|
+
|
|
19
|
+
So to store a joint origin here that means what URDF's `<origin>` means, this
|
|
20
|
+
composes: joint_frame_in_model = child_link_pose ∘ joint.pose (child-relative
|
|
21
|
+
offset), then converts to the parent's frame:
|
|
22
|
+
|
|
23
|
+
origin_in_parent = inverse(parent_link_pose) ∘ joint_frame_in_model
|
|
24
|
+
|
|
25
|
+
When (as in nearly every real SDF, including PX4 Vector's) the joint carries no
|
|
26
|
+
`<pose>` of its own, this reduces to `inverse(parent) ∘ child` — the child's
|
|
27
|
+
pose expressed in the parent's frame, which is exactly the URDF-equivalence a
|
|
28
|
+
mixed-format drift check needs.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import datetime as dt
|
|
32
|
+
import hashlib
|
|
33
|
+
import xml.etree.ElementTree as ET
|
|
34
|
+
from math import asin, atan2, cos
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from lockwire.schema import (
|
|
38
|
+
Joint,
|
|
39
|
+
JointLimits,
|
|
40
|
+
JointOrigin,
|
|
41
|
+
Link,
|
|
42
|
+
ModelRecord,
|
|
43
|
+
PrincipalInertia,
|
|
44
|
+
Provenance,
|
|
45
|
+
Quantity,
|
|
46
|
+
_model_structure_hash,
|
|
47
|
+
)
|
|
48
|
+
from lockwire.scene_urdf import _apply_r, _compose, _first_model, _IDENTITY_POSE, _mat3_mul, _Pose
|
|
49
|
+
from lockwire.urdf import UrdfTopology, _principal, _refuse_doctype_entity
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _sdf_pose(
|
|
53
|
+
pose_el: ET.Element | None,
|
|
54
|
+
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
|
|
55
|
+
text = pose_el.text if pose_el is not None and pose_el.text else "0 0 0 0 0 0"
|
|
56
|
+
vals = [float(x) for x in text.split()]
|
|
57
|
+
vals += [0.0] * (6 - len(vals))
|
|
58
|
+
return (vals[0], vals[1], vals[2]), (vals[3], vals[4], vals[5])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _pose_mul(a: _Pose, b: _Pose) -> _Pose:
|
|
62
|
+
ra, ta = a
|
|
63
|
+
rb, tb = b
|
|
64
|
+
r_new = _mat3_mul(ra, rb)
|
|
65
|
+
dt_ = _apply_r(ra, tb)
|
|
66
|
+
t_new = (ta[0] + dt_[0], ta[1] + dt_[1], ta[2] + dt_[2])
|
|
67
|
+
return r_new, t_new
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _pose_inverse(pose: _Pose) -> _Pose:
|
|
71
|
+
r_mat, t = pose
|
|
72
|
+
r_t = tuple(tuple(r_mat[j][i] for j in range(3)) for i in range(3))
|
|
73
|
+
t_inv = tuple(-x for x in _apply_r(r_t, t))
|
|
74
|
+
return r_t, t_inv
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _matrix_to_rpy(r_mat: tuple[tuple[float, float, float], ...]) -> tuple[float, float, float]:
|
|
78
|
+
"""Inverse of `scene_urdf._rpy_matrix` (URDF/SDF's Rz(yaw)*Ry(pitch)*Rx(roll))."""
|
|
79
|
+
sp = max(-1.0, min(1.0, -r_mat[2][0]))
|
|
80
|
+
pitch = asin(sp)
|
|
81
|
+
cp = cos(pitch)
|
|
82
|
+
if abs(cp) > 1e-8:
|
|
83
|
+
roll = atan2(r_mat[2][1], r_mat[2][2])
|
|
84
|
+
yaw = atan2(r_mat[1][0], r_mat[0][0])
|
|
85
|
+
else:
|
|
86
|
+
# Gimbal lock: roll/yaw are not independently recoverable; pin roll=0.
|
|
87
|
+
roll = 0.0
|
|
88
|
+
yaw = atan2(-r_mat[0][1], r_mat[1][1])
|
|
89
|
+
# `-0.0 + 0.0 == 0.0`: normalize sign so an unrotated axis hashes identically
|
|
90
|
+
# to the URDF door's literal `0.0`, instead of `json.dumps` emitting "-0.0".
|
|
91
|
+
return roll + 0.0, pitch + 0.0, yaw + 0.0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _link_poses(model_el: ET.Element) -> dict[str, _Pose]:
|
|
95
|
+
poses: dict[str, _Pose] = {}
|
|
96
|
+
for link in model_el.findall("link"):
|
|
97
|
+
name = link.get("name", "?")
|
|
98
|
+
xyz, rpy = _sdf_pose(link.find("pose"))
|
|
99
|
+
poses[name] = _compose(_IDENTITY_POSE, xyz, rpy)
|
|
100
|
+
return poses
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parse_link(link_el: ET.Element) -> tuple[Link, bool]:
|
|
104
|
+
"""Returns (link, is_zero_mass). Mirrors `urdf._parse_link`."""
|
|
105
|
+
name = link_el.get("name", "?")
|
|
106
|
+
inertial = link_el.find("inertial")
|
|
107
|
+
if inertial is None:
|
|
108
|
+
return Link(name=name), True
|
|
109
|
+
|
|
110
|
+
mass_text = inertial.findtext("mass")
|
|
111
|
+
mass_val = float(mass_text) if mass_text is not None else 0.0
|
|
112
|
+
if mass_val == 0.0:
|
|
113
|
+
return Link(name=name), True
|
|
114
|
+
|
|
115
|
+
xyz, _rpy = _sdf_pose(inertial.find("pose"))
|
|
116
|
+
|
|
117
|
+
inertia_el = inertial.find("inertia")
|
|
118
|
+
inertia: PrincipalInertia | None = None
|
|
119
|
+
if inertia_el is not None:
|
|
120
|
+
inertia = _principal(
|
|
121
|
+
*(
|
|
122
|
+
float(inertia_el.findtext(k) or "0")
|
|
123
|
+
for k in ("ixx", "ixy", "ixz", "iyy", "iyz", "izz")
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
link = Link(
|
|
128
|
+
name=name, mass=Quantity(value=mass_val, unit="kg"), com=xyz, frame=name, inertia=inertia
|
|
129
|
+
)
|
|
130
|
+
return link, False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_joints(model_el: ET.Element, link_poses: dict[str, _Pose]) -> list[Joint]:
|
|
134
|
+
joints: list[Joint] = []
|
|
135
|
+
for joint_el in model_el.findall("joint"):
|
|
136
|
+
name = joint_el.get("name", "?")
|
|
137
|
+
parent = joint_el.findtext("parent") or "?"
|
|
138
|
+
child = joint_el.findtext("child") or "?"
|
|
139
|
+
|
|
140
|
+
j_xyz, j_rpy = _sdf_pose(joint_el.find("pose"))
|
|
141
|
+
child_pose = link_poses.get(child, _IDENTITY_POSE)
|
|
142
|
+
joint_frame_in_model = _compose(child_pose, j_xyz, j_rpy)
|
|
143
|
+
parent_pose = link_poses.get(parent, _IDENTITY_POSE)
|
|
144
|
+
origin_in_parent = _pose_mul(_pose_inverse(parent_pose), joint_frame_in_model)
|
|
145
|
+
r_origin, t_origin = origin_in_parent
|
|
146
|
+
origin = JointOrigin(xyz=t_origin, rpy=_matrix_to_rpy(r_origin)) # type: ignore[arg-type]
|
|
147
|
+
|
|
148
|
+
axis_el = joint_el.find("axis")
|
|
149
|
+
axis = None
|
|
150
|
+
limits = None
|
|
151
|
+
if axis_el is not None:
|
|
152
|
+
axis_text = axis_el.findtext("xyz")
|
|
153
|
+
if axis_text:
|
|
154
|
+
axis = tuple(float(x) for x in axis_text.split()) # type: ignore[assignment]
|
|
155
|
+
limit_el = axis_el.find("limit")
|
|
156
|
+
if limit_el is not None:
|
|
157
|
+
lower_raw = limit_el.findtext("lower")
|
|
158
|
+
upper_raw = limit_el.findtext("upper")
|
|
159
|
+
effort_raw = limit_el.findtext("effort")
|
|
160
|
+
velocity_raw = limit_el.findtext("velocity")
|
|
161
|
+
limits = JointLimits(
|
|
162
|
+
lower=float(lower_raw) if lower_raw is not None else None,
|
|
163
|
+
upper=float(upper_raw) if upper_raw is not None else None,
|
|
164
|
+
effort=float(effort_raw) if effort_raw is not None else None,
|
|
165
|
+
velocity=float(velocity_raw) if velocity_raw is not None else None,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
joints.append(
|
|
169
|
+
Joint(
|
|
170
|
+
name=name,
|
|
171
|
+
type=joint_el.get("type", "fixed"), # type: ignore[arg-type]
|
|
172
|
+
parent=parent,
|
|
173
|
+
child=child,
|
|
174
|
+
origin=origin,
|
|
175
|
+
axis=axis,
|
|
176
|
+
limits=limits,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
return joints
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _model_element(root: ET.Element, path: Path) -> ET.Element:
|
|
183
|
+
if root.tag != "sdf":
|
|
184
|
+
raise ValueError(f"{path}: root element is <{root.tag}>, expected <sdf>")
|
|
185
|
+
model_el = _first_model(root)
|
|
186
|
+
if model_el is None:
|
|
187
|
+
raise ValueError(f"{path}: no <model> element found")
|
|
188
|
+
return model_el
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse(path: Path) -> tuple[ET.Element, list[Link], list[str], list[Joint]]:
|
|
192
|
+
raw = path.read_bytes()
|
|
193
|
+
_refuse_doctype_entity(raw, path)
|
|
194
|
+
root = ET.fromstring(raw)
|
|
195
|
+
model_el = _model_element(root, path)
|
|
196
|
+
|
|
197
|
+
link_els = model_el.findall("link")
|
|
198
|
+
if not link_els:
|
|
199
|
+
raise ValueError(f"{path}: no <link> elements found")
|
|
200
|
+
|
|
201
|
+
links: list[Link] = []
|
|
202
|
+
zero_mass: list[str] = []
|
|
203
|
+
for link_el in link_els:
|
|
204
|
+
link, is_zero = _parse_link(link_el)
|
|
205
|
+
links.append(link)
|
|
206
|
+
if is_zero:
|
|
207
|
+
zero_mass.append(link.name)
|
|
208
|
+
|
|
209
|
+
link_poses = _link_poses(model_el)
|
|
210
|
+
joints = _parse_joints(model_el, link_poses)
|
|
211
|
+
return root, links, sorted(zero_mass), joints
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def parse_sdf(path: Path) -> ModelRecord:
|
|
215
|
+
"""First `<model>` in `path` (or the first inside a `<world>`) -> `ModelRecord`."""
|
|
216
|
+
raw = path.read_bytes()
|
|
217
|
+
root, links, zero_mass, joints = _parse(path)
|
|
218
|
+
return ModelRecord(
|
|
219
|
+
source=str(path),
|
|
220
|
+
source_sha256=hashlib.sha256(raw).hexdigest(),
|
|
221
|
+
structure_hash=_model_structure_hash(links, joints), # re-derived by the
|
|
222
|
+
# schema validator when joints is non-empty; kept here for the (rare)
|
|
223
|
+
# zero-joint single-link model where the validator leaves it as-is.
|
|
224
|
+
links=links,
|
|
225
|
+
joints=joints,
|
|
226
|
+
zero_mass_links=zero_mass,
|
|
227
|
+
provenance=Provenance(witness="w2", source=str(path), locked_at=dt.date.today()),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def read_sdf_topology(path: Path) -> UrdfTopology:
|
|
232
|
+
"""SDF twin of `urdf.read_urdf_topology`, for `checks.check_urdf_drift`."""
|
|
233
|
+
_root, links, _zero_mass, joints = _parse(path)
|
|
234
|
+
record_hash = _model_structure_hash(links, joints)
|
|
235
|
+
return UrdfTopology(
|
|
236
|
+
link_names=frozenset(link.name for link in links),
|
|
237
|
+
joints=tuple(joints),
|
|
238
|
+
topology_hash=record_hash,
|
|
239
|
+
record_hash=record_hash,
|
|
240
|
+
)
|