Skip to main content

dalaran-urdf

Drive the Dalaran URDF API (dalaran.urdf.UrdfTree) to ingest a URDF as a Transform3D layer on a robot recording. Read when logging a robot model, running forward kinematics from joint states, composing a fixed chain for sensor extrinsics, or when the transform tree will not connect from the data alone. Builds on dalaran-chunk-processing (stream/lens mechanics) and dalaran-data-model (entity paths, timeline, base-vs-layer).

Informações da origem

Repositório
Flaminis/Dalaran
Última atividade na origem
7 de agosto de 2026 às 09:48
Idioma detectado do SKILL.md
inglês
Estrelas
736
Forks
31

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
dalaran-urdf
description
Drive the Dalaran URDF API (dalaran.urdf.UrdfTree) to ingest a URDF as a Transform3D layer on a robot recording. Read when logging a robot model, running forward kinematics from joint states, composing a fixed chain for sensor extrinsics, or when the transform tree will not connect from the data alone. Builds on dalaran-chunk-processing (stream/lens mechanics) and dalaran-data-model (entity paths, timeline, base-vs-layer).
user_invocable
true
allowed-tools
Read, Grep, Bash, WebFetch
# Dalaran URDF ingestion A URDF gives you a robot's geometry and its kinematic tree. Ingesting it means two API calls on one `dalaran.urdf.UrdfTree`: stream the static model, then drive it with forward kinematics from joint states. The transforms it produces are derived, so they are a **layer**, never base (the `URDF + joints (computed)` row of the `dalaran-data-model` table). This skill is the `UrdfTree` API and the two judgment calls it cannot make for you: how your joint values map to URDF joints, and how the disconnected frames in the scene connect to one root. The stream/lens plumbing (`LazyChunkStream`, `DeriveLens`, `Selector`, writing and optimizing RRDs) is in `dalaran-chunk-processing`; reach for it, do not re-derive it. Nothing below is tied to a data format: where your joint names, joint values, and calibration come from is yours to wire in. ## The API ```python from dalaran.urdf import UrdfTree urdf = UrdfTree.from_file_path( urdf_path, entity_path_prefix="robot", # links log under /robot/<link> frame_prefix="", # prepended to every frame name static_transform_entity_path="robot/tf_static", ) ``` - `entity_path_prefix` namespaces the entity tree. One per robot instance. - `frame_prefix` namespaces the **frame names** (`base_link` -> `arm_base_link`). Two robots in one recording need different prefixes or their roots collide and transforms cross-wire. Leave it empty for a single robot. - `static_transform_entity_path` is where the URDF's fixed-joint transforms log (defaults to `/tf_static`). The tree is also introspectable (full surface in `help(UrdfTree)`) For one-off, non-stream use there are `joint.compute_transform(value)`, `joint.compute_transform_columns(values)` (feeds `rr.send_columns`), and `urdf.log_urdf_to_recording()` to log the whole model through the classic logging API (the `animated_urdf` example, `https://github.com/Flaminis/Dalaran/tree/main/examples/python/animated_urdf`, is that style). For pipelines, a `UrdfTree` does two things. **1. Stream the static model.** Emits the visual meshes (`Asset3D`) and the fixed-joint transforms as chunks. This is the whole "log the URDF" step: ```python model = ( urdf.stream(include_joint_transforms=True).drop( # rest-pose joint transforms too content="/robot/**/collision_geometries/**" ) # unless you need collision meshes ) ``` Recolor a robot's meshes with a `MutateLens` on `Asset3D:albedo_factor` (see `dalaran-chunk-processing`). **2. Solve forward kinematics.** Given joint names and the matching joint values, it returns one `dalaran.urdf.JointTransformBatch` per input row. Each batch is a list of per-joint entries with `parent_frame`, `child_frame`, `translation`, and `quaternion`: ```python batches = urdf.compute_joint_transform_batches(names, values, clamp=False) # names, values: pyarrow arrays, one list per timestamp (names aligned to values) # clamp=True clamps out-of-limit values and warns, useful while debugging units ``` You will almost always run this inside a stream so it stays columnar and lazy, via the two-lens pattern: derive the batch, then `scatter=True` it into `Transform3D.descriptor_translation/quaternion/parent_frame/child_frame`. The full shape is the "Minimal shape" section below; the `robot_data_preprocessing` example (References) is a complete working instance. The only URDF-specific part is the `compute_joint_transform_batches` call inside the first lens; everything else is generic stream mechanics (`dalaran-chunk-processing`). Merge the model stream and the FK stream, `collect(optimize=OBJECT_STORE)`, and `write_dlr(..., recording_id=<segment_id>)`. The `recording_id` must equal the base segment id or the layer never attaches; `application_id` is discarded on registration. ## Mapping joint state to URDF joints (you supply this; the data will not) `compute_joint_transform_batches` is only as right as the `names`/`values` you hand it, and the mapping is not in the URDF. Three things go wrong silently: - **Order.** Build an explicit `names` array aligned to the `values` you read. Never assume your message's field order matches the URDF's `<joint>` order. - **Count.** The URDF's non-`fixed` joint count rarely equals your reported value count. A gripper sent as one value is often two prismatic joints in the URDF; a mimic joint may be omitted from the message. Reconcile explicitly, and use the API to do it: iterate `urdf.joints()`, partition by `joint_type` and `mimic`. A joint with `mimic` set derives its value from the driver joint as `driver * multiplier + offset`; feed it that, not a message field. Confirm the count against `urdf.joints()`, not the message length. - **Units.** URDF joints are radians (revolute) and meters (prismatic). Convert if your source differs. Get any of these wrong and FK runs and writes a confident, wrong pose. ## Make the joint states readable first Where the joint values come from is not this skill's problem, but a dead joint source produces an empty FK layer **with no error**: a source path that matched nothing, a decoder that yielded zero rows, or a reader that dropped the message silently. Whatever the source, confirm the joint-state stream yields rows before debugging FK. The importer skill for your source format covers its own empty-stream failure modes. ## How transforms compose (reason about this before logging anything) Dalaran resolves a pose by chaining transforms from a frame up to a root. There are two ways an edge in that chain gets defined, and a URDF ingest mixes both: - **By entity-path hierarchy.** A `Transform3D` on `/a/b` with no frame names is the transform of `/a/b` relative to its parent path `/a`. Composition follows the entity tree. - **By explicit frame graph.** A `Transform3D` that carries `parent_frame` and `child_frame` defines an edge between two **named frames**, independent of where in the entity tree it is logged. URDF FK uses this: every joint transform names its parent and child link frames. So a URDF ingest is a graph of named frames. An edge exists only if some `Transform3D` names that exact `parent_frame -> child_frame` pair. A frame with no incoming edge is a root. The viewer renders every root at the world origin, which is why two unconnected robots silently overlap instead of erroring. ## Resolving the transform forest (the part the data cannot always give you) A URDF is **one tree rooted at its base link**. FK and fixed joints supply every edge inside that tree. A real scene is a **forest** of roots the URDF never connects: a world or scene frame, each robot's base, every camera or sensor frame. The edges that join those roots come from calibration (extrinsics in a sidecar, a TF static publisher, a hand-eye result), not from the URDF, and some are simply absent. A single connected tree is not always solvable. Resolve it deliberately: 1. **Enumerate every frame.** Iterate `urdf.joints()` and collect the `parent_link`/`child_link` pairs (the in-tree edges, frame-prefixed); `urdf.root_link()` is that URDF's root. Add every sensor/world frame the scene needs (from the `dalaran-data-model` table). Decide the one intended root. 2. **Classify each edge by source.** In-URDF edges (FK joints, `fixed` joints) come from the URDF plus joint values. Inter-root edges (root to each robot base, root to each fixed sensor, an arm link to a wrist-mounted camera) come from calibration and you must log them yourself. 3. **Compose fixed chains from the URDF** when you need the transform between two links joined only by `fixed` joints (a camera bracket, a tool mount): walk parent links across `fixed` joints via `urdf.joints()`, turning each joint's `origin_xyz`/`origin_rpy` into a homogeneous matrix and multiplying along the chain. If the walk cannot reach the target link, **stop and say so** ("no fixed chain from A to B; stuck at C"). A broken chain is a wrong pose, not a missing one. 4. **Build the edge set and find the roots.** Collect every `parent_frame -> child_frame` pair you will log (URDF + calibration). Walk parents from each frame; any frame that does not reach the intended root is an unconnected root and names a **missing edge**. This is a pure graph check you can run before writing the DLR. 5. **Resolve every missing edge, or fail loudly.** For each one: - If calibration supplies the transform, log it (next section). - If the data does not, the tree is unsolvable. Do **not** leave the frame disconnected (it collapses onto the origin and reads as one merged scene). Either abort and name the missing edge, or log identity and emit a loud warning naming the assumed edge. State which you did. 6. **Match frame names across sources.** FK-derived `parent_frame`/`child_frame` must equal the names `urdf.stream()` emits for the static geometry, and your calibration edges must use those same names, or links float off the mesh. The `frame_prefix` is what keeps them identical; reuse it everywhere. A correct ingest has exactly one root, and a path from every frame to it. ## Logging a connection correctly A connecting edge is a **static** `Transform3D` carrying the bridging frame names. Static (no time index) so it holds for the whole recording; the frame names, not the entity path, are what create the graph edge. This `Chunk.from_columns` is the rare **sidecar exception** — a calibration transform no reader or FK lens can produce; do not generalize it to transforms a reader emits (a `frame_transforms` topic → `Transform3D`) or that FK derives: ```python import dalaran as rr from dalaran.experimental import Chunk, LazyChunkStream # world -> this robot's base, from your calibration (translation + xyzw quaternion) edge = Chunk.from_columns( "/world/robot_base", # any sensible bridging entity path indexes=[], # no index == static columns=rr.Transform3D.columns( translation=[translation], quaternion=[quaternion_xyzw], parent_frame=["world"], # must match the root frame name child_frame=["arm_base_link"], # must match the URDF root frame (prefixed) ), ) edges = LazyChunkStream.from_iter([edge]) # merge alongside model + FK streams ``` Log a fixed-chain result (step 3) the same way, with `parent_frame` and `child_frame` set to the two link frames the chain spans. Merge all edge chunks into the same recording as the model and FK streams so they share the graph. ## Minimal shape (one robot, generic joint source) ```python import dalaran as rr from dalaran.experimental import DeriveLens, LazyChunkStream, OptimizationProfile, Selector from dalaran.urdf import UrdfTree urdf = UrdfTree.from_file_path(urdf_path, entity_path_prefix="robot", static_transform_entity_path="robot/tf_static") model = urdf.stream(include_joint_transforms=True).drop(content="/robot/**/collision_geometries/**") joints = source_joint_state_stream() # your reader; one message column of names+values fk = ( joints .lenses( DeriveLens(JOINT_MSG_COMPONENT, output_entity="/tmp/batches").to_component( "dalaran.urdf.JointTransformBatch", Selector(".").pipe(lambda msgs: urdf.compute_joint_transform_batches(read_names(msgs), read_values(msgs))), ), content=JOINT_SOURCE_PATH, output_mode="forward_all", ) .lenses( DeriveLens("dalaran.urdf.JointTransformBatch", output_entity="/robot/transforms", scatter=True) .to_component(rr.Transform3D.descriptor_translation(), Selector("[].translation")) .to_component(rr.Transform3D.descriptor_quaternion(), Selector("[].quaternion")) .to_component(rr.Transform3D.descriptor_parent_frame(), Selector("[].parent_frame")) .to_component(rr.Transform3D.descriptor_child_frame(), Selector("[].child_frame")), content="/tmp/batches", output_mode="drop_unmatched", ) .filter(content="/robot/transforms") ) LazyChunkStream.merge(model, fk).collect(optimize=OptimizationProfile.OBJECT_STORE).write_dlr( out_path, application_id="urdf", recording_id=segment_id, ) ``` `read_names`, `read_values`, `JOINT_MSG_COMPONENT`, and `JOINT_SOURCE_PATH` are the only data-specific pieces, and the mapping section above is what makes them correct. ## Gotchas that cause real failures 1. Empty layer, no error: dead joint-state source (decoded to zero rows; see the importer skill for your format) or wrong `JOINT_SOURCE_PATH`/component name. 2. Confident wrong pose: joint count, order, or units wrong. 3. Layer writes but never attaches: `recording_id != segment_id`. 4. Frames collide: two robots sharing a `frame_prefix`. 5. Scene looks merged at the origin: unconnected roots logged as identity without a calibration edge. 6. Catalog ingest rejects or misorders chunks: `OBJECT_STORE` optimization skipped. ## References - `https://github.com/Flaminis/Dalaran/tree/main/examples/python/robot_data_preprocessing` (FK two-lens pattern, two robots + scene URDFs, prefixes, recoloring, calibration offsets) - `https://github.com/Flaminis/Dalaran/tree/main/examples/python/animated_urdf` (classic logging API: `log_urdf_to_recording`, per-joint `compute_transform`) - `dalaran-data-model` (the mapping table this skill consumes) - the importer skill for your joint-state source format (making the source readable) - `dalaran-chunk-processing` (lens/stream, write/optimize mechanics)
Ver no GitHub