Skip to content

Release v0.1.0 - #6

Merged
vamsikalagaturu merged 377 commits into
mainfrom
dev
Oct 5, 2026
Merged

vamsikalagaturu merged 377 commits into
mainfrom
dev

Conversation

@vamsikalagaturu

Copy link
Copy Markdown
Contributor

First release of motion-spec-dsl: the textX language for guarded robot motion, emitting RDF (JSON-LD) with provenance.

- build.yml: ruff lint + `python -m build` + --no-deps import check across Py 3.10-3.12
- test.yml: pytest, installing the workspace siblings (motion_spec, coord_dsl, patched
  rdflib) from git since they are not on PyPI
- pyproject: add the missing [build-system] (setuptools) so `python -m build` is reliable
- README: build / test / docs status badges and a note on the package layout

Pages deploy (docs.yml) already exists. Docs build verified with sphinx -W.
Point the conformance suite at motion-spec-dsl/models and vendor the pick_place_single
.robmot/.fsm so the test no longer depends on the bdd_collab_bhv_cpp sibling checkout.
- test.yml: install the full sibling closure the workspace needs -- rdf_utils and
  jinja2 were missing (siblings under-declare deps); install siblings with deps,
  package with --no-deps, patched rdflib pinned last. Verified in a clean venv (88 pass).
- docs.yml: build on every branch/PR so the badge renders; deploy to Pages only from main.
The app manifest's iri-map now only declares where the model's own imported
graphs live; metamodel/ontology prefixes are resolved through the rdf-utils
IriToFileResolver instead of being baked in as machine-specific absolute
paths, so the generated manifest is portable into run archives.
…list caveat

Records that _canonicalize_jsonld is load-bearing for deterministic output (rdflib
serializes in hash-seeded set order), and the latent risk that _sort_lists would
reorder an ordered @list if the model ever emits one (harmless today — no @list).
…fest

The .robmot imports a .fsm, but the FSM structure (states/events/transitions/
reactions) was only framed into fsm_ir.json — it never reached the model graph.
Serialize the FSM rdflib graph (already built via coord_dsl get_fsm_graph) as a
separate named-graph JSON-LD (@id = FSM IRI) and add it to the app manifest's
imports, so ir_gen loads it as a named graph and derives the FSM wiring from the
combined graph instead of reading fsm_ir.json at codegen time.

The .hpp and fsm_ir.json are still generated (standalone header + archive input).
- split the monolithic grammar into grammars/*.tx (base, model, motion_spec,
  quantities, trajectory, constraint_handler); move textX classes into a
  classes/ package (namespaces.py → classes/common.py); drop domain.py
- add scoping.py + validation/names.py for name resolution; drop the standalone
  environment validation
- integrate scene-dsl: models import .scenex/.scene/.ktree instead of an inline
  ENVIRONMENT block
- rdf builder: emit raw cross-frame pose references; transform derivation now
  lives downstream in motion-spec
- reorg tests: drop the fixtures/ tree and test_validation; add test_scoping

Generated artefacts (*.jsonld, *_fsm.hpp, fsm_ir.json, provenance/) intentionally
left uncommitted.
A snapshot may now name an FSM event to re-sample on:

    pose start-pose = snapshot of <shared.world.pose-ee-base> on event aas.E_ARC_ENTERED

Emits snap:sampling as initial-sampling or event-triggered-sampling plus
snap:trigger, matching the xone pairing in snapshot.shacl.ttl. This replaces
the on task/on entry clause dropped in the metamodel refactor.

EventName moves from constraint_handler.tx to base.tx: quantities.tx needs it
and cannot import constraint_handler without a cycle. base.tx already declares
the coord_dsl_fsm reference, and the EventName.event scope provider is keyed by
rule name so it keeps resolving.

The unqualified form is rejected: it mints a monitor-owned event rather than
referencing a declared el:Event, and has no URI to emit under a snapshot.

State entry needs no new concept -- an FSM reaction's existing FIRES: fires an
entry event from every reaction transitioning into that state.
The arc traced no z motion because plane-normal was sign-flipped. The sagitta
direction is arc_normal x chord_hat with the apex at mid + sagitta*perp, so
with chord (-0.05, 0.30, 0) the negated normal bulged 0.18 m down into the
table instead of up.

Touchdown had been ported from a velocity constraint to a position constraint
targeting contact-z, putting Kp=200 on a ~0.4 m error, and its until became a
bare position threshold that fired while still moving at full speed. Restored
the descend-vel velocity constraint and the velocity-stall until, which is
actual contact detection.

The arc's start-pose re-samples on E_ARC_ENTERED, fired by both reactions that
enter S_ARC. Its end-* snapshots stay initial-sampled, reproducing the old
start-pose-on-entry + task-pose-on-task split.

Verified headless and GUI: S_HOME -> S_TOUCHDOWN -> S_FORWARD -> S_ARC ->
S_DONE, descent at 0.053 m/s, arc peaking 0.15 m above the table.
comply-ori held an orientation captured once, but the arc rotates roll by
-0.75 rad along its path, so re-entering admittance mid-arc left Kp=240/Kd=160
driving the wrist back toward a stale target. The pre-refactor model sampled
this `on entry`; the port dropped the qualifier when the clock was removed.

S_ADMITTANCE has one entering reaction, so R_FORCE_DETECTED fires
E_ADMITTANCE_ENTERED and the snapshot triggers on it.
until applied one all/any over the whole section, so a motion could carry only
one transition condition. S_ADMITTANCE needs two independent ones -- released on
every force axis goes back to S_ARC, bracelet-on-table ends the task -- which
forced the release test into while, monitored as a single constraint. That test
could then only cover one axis: pushing along Y let X fall under the release
threshold, so the FSM left admittance while the push was still going and
force-y immediately sent it back. A ~0.6 s limit cycle, felt as instability.

until now takes named groups, each with its own logic and its own monitor:

    until {
        released all { released-x: ..., released-y: ..., released-z: ... },
        table-contact all { near-table: ..., contact-table: ... }
    }

    mon-released:      monitor <compliance.released> ...
    mon-table-contact: monitor <compliance.table-contact> ...

A group becomes a cstr-ext:ConstraintConjunction (or Disjunction for 'any')
carrying its members, which the metamodel already defined and nothing emitted.
Monitors targeting one aggregate exactly as a whole-section monitor does.

`until all { ... }` is unchanged, so the other motions are untouched.
The comply axes ran Kp=150 with Ki=0, so a sustained push settled where the
proportional term balanced it: measured tracking error against the admittance
reference was 0.069 m/s at 16 N and 0.115 m/s at 20 N, on a 0.30 m/s reference.
A proportional-only velocity loop cannot reject a constant disturbance, and the
external force is exactly that, so the end-effector never actually followed the
velocity the admittance filter generated.

Ki=800 (integral time Kp/Ki ~ 0.19 s) closes that, with the integral clamped to
+/-0.02 m so its authority stays near the proportional term's (~16 m/s2) and a
long push cannot wind it up into a lunge on release.

damping and max-velocity are deliberately unchanged so the effect of the
integral term is attributable on its own.
output-saturation was validated against the command type, which infer_command_type
reports as LinearVelocity for a position or velocity constraint. But such a
controller does not emit a velocity: its signal becomes the solver's
acceleration-energy row, and that is what the generated clamp applies to. Authors
were required to bound an acceleration with a velocity quantity, and the one
existing use did exactly that.

Velocity command types now expect their acceleration counterpart; force and
torque commands are emitted in their own dimension and are unchanged.

Adds tests/test_interaction_contract.py covering the interaction path: velocity
constraints reaching the solver, the scenex force-torque sensor reaching the
runtime, admittance references being produced before they are consumed, and no
motion capturing or scheduling another motion's state. Each was verified to fail
when its defect is reintroduced -- none of these were caught by a headless run,
which completed start-to-end throughout.
Matches admittance_arc_single: each model owns a directory holding its .robmot,
.fsm, .scene and .scenex, with the shared .ktree files staying at the models
root and reached by a relative import.

The scoping test builds a synthetic probe.robmot at the models root, so its
import needed the folder-qualified path rather than the bare file name.
coord-dsl's `.fsm` grammar now uses lowercase keywords, `{ }` scopes with
comma-separated entries and `<...>` references instead of SHOUTY sections and
the `@` sigil. Converted by round-tripping through the parser, so the models
are semantically unchanged -- same states, events, transitions, reactions and
ordering, and identical generated output.
tests/fixtures/base.robmot is a minimal valid model; parse_mutated (conftest.py)
replaces one clause of it and parses, so a negative test is a one-line diff
against known-good source rather than a hand-built invalid fixture.

tests/fixtures/extra_constructs.robmot exercises constructs the two real
example models don't use (joint-position, impedance, output-saturation) so
those validator/emitter branches run at all.
ProfileSpec (the `= profile { max-velocity: ..., max-acceleration: ... }`
syntax) was drafted in trajectory.tx but never listed as an alternative in
ContextQuantityValue, quantities.tx's only rule for a ContextQuantity's value.
No .robmot source could ever produce one, so `profile_qty.value` could never
be a ProfileSpec instance -- any controller that set `profile: <ref>`
unconditionally failed validation. Nothing caught it: no example model uses
the feature.

ProfileSpec's fields are ContextRef, and ContextRef itself references
ContextQuantity -- the rule ContextQuantityValue extends. That's a same-file
rule cycle, which textX resolves fine, but the moment ContextRef and
ContextQuantity live in files that import each other, the file-level import
graph becomes cyclic and textX's grammar loader breaks outright (reproduced
in isolation). So ProfileSpec has to live wherever ContextQuantity does; renamed
quantities.tx to context.tx since "quantities" no longer covers ContextQuantity,
its value forms, and reference-generator specs (admittance, velocity profile).

Two more bugs surfaced once the path became reachable and testable:
- The standalone profile quantity's RDF placeholder over-emitted as a full
  traj:VelocityProfile op (max-velocity/max-acceleration/...), which made
  SHACL require a traj:reference output it never provided -- unconditional
  conformance failure. Now emits a plain LinearVelocity placeholder, mirroring
  how the admittance placeholder already relates to its own filter op.
- The per-controller profile reference output was typed Distance instead of
  LinearVelocity -- wrong regardless of what the profile drives, since a
  velocity profile's output is always a velocity command.

Verified behavior-preserving: both example models' RDF dataset, app manifest
and FSM .hpp are byte-identical before/after (neither uses `profile:`), and a
hand-authored profiled-controller model now parses, validates, generates and
SHACL-conforms end to end.
coord-dsl added graphviz targets and a provenance.jsonld beside its generated
artifacts, but _gen_fsm only ever called the low-level graph/gen_json/
gen_cpp_header functions directly, bypassing the wrapper layer that draws the
graph and records generation. So motion-spec-dsl got neither.

_gen_fsm now also writes a .dot drawing (always; no dependency) and a .svg
render (only when `dot` is on PATH -- generation stays behavior-preserving on
a machine without graphviz), and records every FSM artifact into coord-dsl's
own provenance.jsonld. Deliberately not reusing coord-dsl's own gen_cpp():
it also runs clang_format_file, which would reformat the .hpp and add a new
external-tool dependency to the default generate path. The existing
gen_cpp_header() call is untouched, so the .hpp stays byte-identical.

motion-spec-dsl's own provenance/dsl.jsonld previously attributed every
artifact to a single motion_spec_dsl agent, including the FSM and scenex
outputs it doesn't actually produce itself. It now attributes FSM artifacts
to a coord_dsl agent and scenex artifacts to a scene_dsl agent, each with
their own activity, and links to coord-dsl's provenance.jsonld via a pointer
entity. The two documents can't literally share activity nodes: dsl.jsonld is
SHACL-validated standalone (prov.shacl.ttl's sh:class constraints need the
referenced node's type asserted in the same graph), so the agents are
motion-spec-dsl's own self-contained nodes rather than reused coord-dsl IRIs.

Also replaced _fsm_output_names, a static predictor of what _gen_fsm would
write, with _gen_fsm's actual returned filenames -- it couldn't know whether
.svg was produced (that depends on `dot` being on PATH) and so silently
under-listed it in the provenance document.

Verified: both example models' RDF dataset, app manifest and .hpp stay
byte-identical; the new provenance/dsl.jsonld SHACL-conforms (checked
explicitly, not just via non-error); both models' app manifests still
SHACL-conform end to end.
A name is appended directly to its namespace, so an authored `ns` URI that
lacks a trailing separator silently ran the two together, and one with an
empty path segment or a fragment name minted a malformed IRI. Reject all of
these where the URI enters, so nothing downstream has to defend.

The DSL docs now live in the motion-spec Sphinx tree; drop the standalone
build here.
vamsikalagaturu and others added 29 commits September 2, 2026 20:53
`pose from results { of: <frame>, wrt: <frame> }` says the publisher reports
a frame other than the observed quantity -- a marker on a body seen by a
camera -- so the composition into the quantity is the model's, stated with the
geometric vocabulary rather than implied by the channel.
joint-current reads a joint's motor current: qudt:Quantity +
kc-stat:JointReference + act:JointCurrent with of-joint, quantity kind
ElectricCurrent, unit A. The spec grammar gains the current scalar type
and the ampere unit, and the dimension vectors carry a fifth, electric
current, component. A current is measured only: a controller on one is
refused.

norm of <vector view> [across <direction>] is a scalar view backed by
geom-op-ext:VectorNorm: the Euclidean norm of a 3-vector view, or of its
component across a direction seen in the same frame. Its kind and unit
follow the vector's.
A `joint-velocity` is a joint-referenced angular velocity: typed
`kc-stat:JointVelocityCoordinate` + `kc-stat:JointReference`, kind
AngularVelocity, unit rad/s, with `kc-stat:of-joint`, mirroring
joint-position. Only monitors and until terms may read it; a
controller on it is refused, as for joint-current.
A wrench world quantity can name the agent whose serial chain a KDL
momentum observer runs on, with its gain and filter constant, as the
alternative to ft-sensor. The graph types the observer est:MomentumObserver
with agn:of-agent and qudt quantities for gain (Hz) and filter; the terms
live in vocab.py only. An estimated wrench cannot be commanded, cannot also
name an ft-sensor, and needs re-tare-on like a measured one.
…t table contact

The world tree now places the arm on the table correctly, so pose-ee-base.z at contact reads
0.040 m; the old 0.03 m threshold was met only because every reading was off by the table's
placement, and the arc could never end.
Since motion-spec 51da846 an authored wrench is exerted as J^T f_ext on
both backends instead of being absorbed by the constrained solve. The
forearm-align impedance (400 Nm/rad on a 0.56 rad error at HOME) was a
224 Nm demand that only ever worked because it was absorbed; exerted, it
fought the six tip rows and MuJoCo diverged at t~138 s.

Six task rows plus one joint-space elbow pin determine the 7-DOF arm.
pick_place_single already had the pin, so its align-forearm goes; the
undriven align-forearm-lean row stays as the Plan 10 gradient-row case.
pick_place_dual had only the impedance, so each arm gets the same elbow
pin as drawer_dual. Both reach S_DONE on dev motion-spec (13.4 s, 16.5 s).
…goal gates

The pinch frame reads with the gripper's -90 deg mount yaw since scene-dsl
70ecf10, so every orientation authored before it was a quarter turn off
about the tool axis, and a wrench applied at a mid-link is really exerted
since motion-spec 51da846. Each example is brought to that world.

look_cartesian_test, look_angular_velocity_test: look pose recomputed
from the measured home (tool axis along base x, yawed -120 deg); the
tool-frame twist rows remapped to the real pinch axes (x down, y tangent);
elbow pinned in joint space instead of a forearm-align or elbow-height
impedance; the angular-velocity test ends when the reach settles.

handover_dual: all authored orientations composed with the mount yaw,
elbow pins on both arms, arm1's retreat stops before the wrist fold
(0.58 m), transport gates on arm2's orientation too.

geometric_expressions, geometric_relations (was tableii_probe): forearm
impedances dropped, alignment rows kept undriven, elbow pinned, a heading
row so the tilt is fully determined, and every state exits on its goal
instead of a timer. tableii_probe renamed for what it exercises.
The parked pose was authored before the gripper's mount yaw entered the
world reading, so S_HOME began with a quarter-turn roll error and the
wrist rolled 90 deg before the pick. The measured home (tool axis along
base x, pads down) replaces it; the hold now starts satisfied.
The wrapper ships kinova_gen3 with the base_link/shoulder_link contact exclusion that look_joint1_test found necessary; every scene and that test now name it instead of the Menagerie cache.
The JSON-LD keeps every distrib:SampledQuantity symbolic: the controller draws it once per run.
The jsonld generator loses its seed parameter and the draw module.
rdf-utils, coord_dsl and scene-dsl are published on no index, so resolving
this package failed on the first of them wherever it was installed from.
A model's own iri-map claims the whole secorolab host, which outranked the cache and made every generation download the metamodels into its own directory. The cache map now lists the metamodel prefixes too, which are longer still. The generator's ten prints become log records, so a caller can format them.
The generator's prints become log records, and a caller that sets MOTION_SPEC_LOG_FORMAT gets its own stamp and level words on them: textx runs the generator in its own process, so the line is formatted there or not at all.
Every scene carrying an FT mount spelled it 'src/motion-spec-dsl/models/
ft_mount.xml', which only resolved from the root of a workspace that had this
repository checked out at src/motion-spec-dsl. A motion-spec install no longer
creates that path, so the runs failed with "MJCF attachment not found". The
path is now 'models/ft_mount.xml', relative to this repository, so a run from
the checkout root finds it.
… cache

'src/motion-spec-dsl/models/ft_mount.xml' only resolved from the root of a
workspace holding this repository at that path, which a motion-spec install no
longer creates. It is now '../ft_mount.xml', relative to the scene that names
it, the way the ktree imports beside it already work; motion-spec resolves it
against the model directory at generation time.

drawer_open_real reached the wrapper's assets with '../../../mj_kdl_wrapper',
walking out of this repository into a sibling checkout. Those files are not in
any repository -- they are unpacked into ~/.cache/mj_kdl_wrapper -- so the scene
now says so.
A wheel can only carry data that lives inside the package, so models/ moves to
src/motion_spec_dsl/models/ and package-data names the model extensions rather
than the tree -- which keeps the two 1.9 MB recordings out of it: 272 KB rather
than 4.5 MB. `motion-spec examples` copies them into a workspace from wherever
pip put the package, so it needs neither a checkout nor a clone of this
repository.
The example tree moved into the package, to src/motion_spec_dsl/models, but
the suite still resolved `parents[1] / "models"` and the fixtures still
imported `../../models/...`. Neither path exists, so nothing that parses a
model could run: 190 of 225 tests failed, every one of them on the import.

With the paths corrected the suite is 3 failed, 222 passed, and all three
failures are environmental -- two need ROS on the path, one an installed
PyKDL carrying setDriverWeights.
The grasp normal was the tool's x, but the 2f85 fingers sit at +/-y of the
pinch frame, so every model approached the handle with the jaws parallel to
the bar and could not hold it. It is the tool's y in all of them now.

door_open: the swing drove a chord to a precomputed point on the hinge
circle, which stuttered. It is six tool-frame twist rows turning about the
hinge instead. The retreat left one of the seven joints free -- two angle
rows pin only two of three turns -- and the wrist sank to 0.028 m off the
table; all three orientation components are held, and it clears at 0.226 m.
S_DONE had no motion, so the coordinator's default left the arm limp; it now
holds a rest pose.

look_joint1_test: the motion ran unsafely fast, and its exit compared against
a configured pose captured with the wrist at roll 0 while the model holds
q7 = pi/2, so the state never ended. The gate is banded and robot.toml holds
the measured pose. look_cartesian_test was simply too slow to watch.
The only publisher in the example set named a message from a package outside
this workspace, so the model was rejected unless that package happened to be
built and sourced. diagnostic_msgs ships with ROS and carries the same shape
the row needs: a byte field with named constants, OK where the motion
finished and ERROR where its constraint was violated.
geometric_relations and geometric_expressions existed only to exercise the
Table II operators; nothing else referenced them. Borghesan's own worked
example (sec. VII) is the drawer, so the operators now live in the task that
motivates them.

Four operators were unique to the probes and are added to drawer_open as
watched rows -- point-to-line and line-to-line distance, line-on-line
projection, direction-to-plane angle. Watched, because a row costs a solver
axis and every state already spends all six. `pull-open` became a distance
from the drawer face, which is the exact negation of the projection it
replaces, so the driven path exercises the plane operator too. The two
tool-riding lines are stated in the base frame: a line pair needs both
directions in one frame, and parallel lines share no unique common normal,
which the first attempt showed as a zero gradient.

The hand-geometry checks move with them, to test_table_ii_values.py. Porting
them turned up a wrong expectation: the direction-pair angle gradient is
cross(b, a) where the direction-to-plane one is cross(a, b), and correctly
so -- turning the tool about cross(a, b) carries a towards b, which raises
the elevation over a plane but closes the angle between two directions. The
old test asserted cross(a, b) for both and failed against its own model; it
skips without a recorded run, so nobody saw it.

drawer_open also carries the grasp-axis fix its siblings got, and the
contract fixture follows the model.
* Square and centre eddie against the edge it holds

eddie_align squares the platform to the contact line and slides it to the
middle of the two hands, holding both hands where they started.

square: the shoulder-to-shoulder line parallel to the contact line -- stated as
two literal directions, one in a frame bolted to the platform, one in the
table. It commands a moment, so it turns the base without moving it.

centred: each shoulder projected onto the line through its own hand, the two
taken in opposite senses along that line. They agree only when the platform
stands between the hands, so the target is the other projection and no distance
is authored. A projection's gradient is the line itself, so it commands a force
along the edge and no yaw -- a reach distance mixes the two, which is why a
matched pair of them only ever rotated the base.

until any: the motion ends when a hand leaves its contact band. A section is a
conjunction unless it says otherwise, so the guard needs `any` to fire on one.

The DSL gains a scalar defined by a binary view, so a projection can be named
and used as another constraint's reference; a moment routed to a platform
solver is built like a force already was, and a cone alignment's command comes
off its gradient, which is the one thing its controller writes.

eddie_arms_only: the right arm pins its redundant DOF on joint 1, not joint 3.
Joint 3 carries 0.60 of the left arm's self-motion but 0.02 of the right's, so
the pin held one arm and let the other run away through its own null space.

* Follow the MJCF for the platform, and carry the drive models

eddie_base.ktree is transcribed from eddie_description/mujoco/mobile_platform.xml
rather than the xacro: the URDF's wheel hinges are still the eccentric ones the
MJCF re-centred, and body and joint names taken from the MJCF resolve in a run
directly.

Brings in the drive and dual-arm models the platform work was exercised with,
and the scenex viewer used to check a graph by eye.

* Stop linking composition back-edges for imported scenes

scene-dsl no longer keeps them: a chain is declared by the tree that declares
its joints, so an importer has nothing to link.

* Show an object in the colour its scene states

The viewer passes a path-backed object's colour through, and pick_place_dual's
two cubes -- one asset, so one colour until now -- are authored red and green.

* Watch the eddie_align contact from the front left as well

The scene had only the overhead camera, which flattens the approach: the
gripper and the table top project onto each other and the last centimetres of
the insertion are hidden behind the hand. side_cam is a three-quarter view from
the robot's front left, posed on the table top so it follows the table rather
than the world, and renders at the same 1280x720 and 30 Hz as the existing one.

* Place a commanded wrench where the runtime can find it; add the crf-unscrewing and drawer baselines

A force now acts at its constrained quantity's `of` frame, and a couple at its as-seen-by
frame's origin, instead of at a declared point nothing places: motion-spec resolves both
off the world model.

drawer_dual targets drawer 2, the only one stiff enough to slide the cabinet unpressed, with
its own cabinet and low-friction table assets. Adds drawer_dual_unheld, drawer_dual_baseline,
crf-unscrewing (UR10e) and crf-unscrewing-kinova, and the UR10e tree.

* Number and rename the examples by what they show

- Keep 14 examples, ordered 01-14, each named for its task: pick_and_place, base_rotation_*, drawer_opening, door_opening, dual_arm_*, arc_tracing_with_admittance, mobile_base_*, real_*.
- Rename files, identifiers and IRIs inside each example to match.
- Import the robot trees scene-dsl ships (scene_dsl:robots/...) instead of copies beside the examples.
- Drop ft_probe, eddie_push, eddie_arms_only, eddie_dual, drawer_open_real, drawer_dual_baseline and drawer_dual_unheld; move the crf models out of the package.
- Drop object colours from dual_arm_pick_and_place until colour is supported.
- Point tests, fixtures and the README at the new names.

* Scope a named until group's rows to the motion that owns it

- `_owning_section` / `_owning_motion` find the motion a named until group sits in
- Evaluator ids, difference plans, distance carriers and geometric distance plans key on that motion, so two motions' groups of the same name no longer share rows or snapshots

* Merge the base rotations, open two cabinets in one drawer example, drop the door

- One `03_base_rotation` example replaces the joint-space, Cartesian-arc and angular-velocity variants
- `04_drawer_opening`: arm1 pulls cabinet1's stiff drawer and the cabinet slides; arm2 presses cabinet2 down and arm1 opens its drawer; overtravel and opened exits as named until groups, speed-limited pull, release and retreat states
- Drop the door example, which modelled no proper door
- Renumber the rest (handover 07 ... real gripper 12) and point fixtures and tests at the new paths
- Handover caps turn and move accelerations on its orientation and unprofiled reach/rest rows
- Pick-and-place models drop unused quantities
- `test_mobile_platform_solver_contract` finds the metamodels relative to the checkout

* Require a solver algorithm and add forward kinematics

- serial-chain algorithm is required: achd, rne, fpk or fvk
- fpk/fvk declare a read-only chain; validate_kinematics_solvers rejects controllers routed to one
- emit kc:ForwardPositionKinematics and kc-op-ext:ForwardVelocityKinematics
- drop the check.py entry point and the unused PyLD dependency
- test fixtures name their algorithm

* Rework the example models for consistency

- renumber to 01-10 and move shared scenes into models/common
- every example sequences through the until idiom: the current motion's until monitor fires the next event
- ns fsm for every FSM; scene prefixes <abbr>_{env,agn,scene,mjc}
- each gripper is its own agent with its own command-forwarding solver
- common/dual_arm_table.scenex shared by 02 and 05
- 39 Nm arm torque limit on every arm solver
- wrist_ft_site yawed a half turn to match the MJCF sensor site
- 01/02: place gate on the place height, grasp gate held 0.2 s in the on-cube band, one shared grasp orientation
- 06: touchdown ends on contact height and zero descent speed
- remove natural_faults.json, dead content and narrating comments

* Merge the emitter's angle, plan-cache and view paths

- one plan function and one emit loop for the three angle-between forms
- one plan cache for every constraint kind
- one map:View emitter; wrench and twist vector views are world-component views

* Move checks into validation and record provenance in memory

- generate() returns the model dataset and its provenance graph; provenance is validated in memory and written once as provenance/dsl.ld.json
- scene and FSM generation move to motion-spec; the manifest's iri-map path is "." and build_url_map loses the models/ heuristic
- source authors are OS-account agents; the package agent carries its version and commit
- world quantities are sosa:ObservableProperty; an FT wrench is a sosa:Observation made by its sensor
- per-axis moment controllers always carry a control signal
- about 70 checks move from the RDF emitter to classes/validation, with new validators for device bindings, config poses, controller commands, world quantities, context geometry, view operands and perturbation directions
- units: one token-to-QUDT-kind table; Position is qk:PositionVector
- namespace classes flatten to IHasNamespaceDeclare and NamedNamespaceObject
- view_scenex.py removed; motion-spec view replaces it
- build with hatchling and hatch-vcs

* Follow the source API in the tests

- rejections now raised at parse time expect TextXSemanticError
- fixtures follow the moved validators (measured wrench re-tare, monitor triggers) and the reworked examples
- conftest load_authored loads a model in memory
- still failing: geometric expression dimensions, Table II operators

* Cover the geometric expression operators with a table-edge reach

- fixtures/table_edge_reach: hover above the table, level with its edge, fingers across it; drives the point-plane distance, point-on-line projection and planar angle, and watches point-line, line-line, line-on-line and direction-plane in its done monitor
- covers Table II of Borghesan et al., "Introducing Geometric Constraint Expressions Into Robot Constrained Motion Specification and Control", IEEE RA-L 1(2), 2016; no shipped example uses all of them
- fixture renamed geometric_operators_ir, test renamed test_every_geometric_operator_is_complete_and_called

* Cut the test suite to one test per DSL concept

- Every shipped example generates and conforms to the SHACL shapes
- Concepts no example uses each keep a test; rejections in one table
- IR checks and the table_edge_reach fixture move to motion-spec
- Tests call the metamodel directly; support.py holds shared source text
- 5,251 -> 1,625 lines; 104 passed

* Move user-input checks into validation and merge the unit tables

- Validation no longer imports from the emitter; shared helpers in classes/
- Per-motion world scope collected in classes/ and checked by validation
  (undeclared world poses and twists, frames, whole-pose bands)
- One DSL_UNITS table: token -> QUDT unit, kinds and dimension
- WORLD_SPECS as named records; geometric properties read one way
- Geometry-kind rejection tested; schema_version dropped from dsl.ld.json
- Not verified: no tests or example runs since these changes

* Split the RDF emitter by concern and emit controlled-expression gradients

- Split rdf/motion_spec.py into emission, geometry, expressions, plans, quantities, constraints, views, handlers, solvers, execution and dataset; build_dataset(model) replaces the dataset builder class
- classes/views.py holds the view and constraint classification the emitter and validation share
- A controlled expression emits its gradient as the same arithmetic over the terms' gradients, normalized; the top operation carries gradient, gradient moment and norm
- Validation of controlled expressions: moved leaves, divisors, frames, halves, and the force/moment half check
- SHACL shapes keyed by vocabulary namespace and chosen from the namespaces the emitted graph uses
- Pending references resolve through scene-dsl's InstancedRefScopeProvider; build_instance_trees lands them
- Enums replaced by string tables, marker classes by kind attributes; user-input checks moved into validation; public names for anything imported across modules
- Not verified: tests and examples not run since these changes

* Resolve scene references through scene-dsl and fix their readers

- Trust scene-dsl's instanced-tree resolution; fall back to the unique suffix match only when it finds nothing, walking the scenes in document order
- Read a geometric prop's IRI from its reference field, so scene frames, joints, sensors and agents give their own IRI
- Read a solver reference's name through the solver, not a copy taken before textX sets it
- Return a derived scalar's declaring context as its owner instead of walking past the model root
- Reword the path-setpoint message so it reads as a sentence
- Give the PID test sources the Ki and Kd the grammar requires and match the current validation messages

* Resolve frame references through scene-dsl's FrameLike

- Geometric props, rotated-in frames, direction endpoints, observed poses and detect targets reference scene.FrameLike, so a body or tree names its default frame in every reference form
- Register scene-dsl's lower_frame_refs after the instanced trees are built, so validation and emission only see frames
- Drop GeoPropPair.value, a copy that would keep the body; geo_prop_value reads the reference fields
- Drop the suffix fallback's own body-to-frame coercion; it now only checks the reference's class
- Test a body, a bare instanced tree, a body in an instanced tree and a frame, and a body with no frame

* Bound arm 1's transfer speed in the drawer-opening example

- Add `transfer-profile`, a trapezoidal profile of 0.1 m/s and 0.3 m/s^2 per axis
- Profile arm 1's x/y/z controllers in `retreat1` and `stage2`: under the acceleration cap alone a 0.6 m transfer reached 0.43 m/s; it now peaks at 0.15 m/s

* Follow the mjkdl rename, register constant-expression classes, use textX tree helpers

- Scene asset paths `src/mj_kdl_wrapper/` -> `src/mjkdl/`
- Register `ConstFactor` and `ConstAtom`, so `const_value` dispatches with `isinstance` instead of `hasattr`
- The suffix scope search walks the scenes with textX `get_children` and skips template internals with `get_parent_of_type`
- Test the parse-time rejection of a whole geometric quantity in an expression

* Negate thresholds inline instead of keeping negated spec constants

- 06: drop neg-force-threshold, neg-release-threshold, neg-comply-int-max; bounds use (-<threshold>), integral saturation is symmetric
- 10: drop neg-force-threshold; force-z bound uses (-<force-threshold>)

* Read a joint's force instead of a gripper's motor current

- `joint-force` replaces `joint-current` as a world quantity; it is a torque (QuantityType.Torque), and the `current` scalar type, the `A` unit and the ElectricCurrent dimension are gone
- Joint scalar quantities (position, velocity, force) may be commanded only through their joint torque; a feedback law without a force or torque command outputs a linear or angular acceleration along its constraint
- Emission, vocabulary and validation follow; the quantity and validation tests cover the new form

* Target mj_kdl_wrapper 0.5.0 and scene-dsl main: frames named explicitly

- Scene asset paths point at the `src/mj_kdl_wrapper/` checkout of mj_kdl_wrapper v0.5.0
- Geometric props, rotated-in frames, direction endpoints, observed poses and detect targets reference scene.Frame again, and lower_frame_refs is no longer registered: scene-dsl main has no FrameLike
- The pick-and-place and handover examples name the cube's origin frame instead of the body
- test_scoping resolves a frame in the graph and one in an instanced tree; the body and tree cases go with FrameLike

* Name frames in the validation tests' scene references

- The detect target and the twist's `of:` frame name `table.table_top` and `cube.cube_origin`; a body no longer stands for its frame

* Clear the ruff findings in the sources

- DefinedNamespace `_extras` are tuples: rdflib only tests membership, and a mutable class default is shared by every subclass
- SSN comes from rdflib.namespace in the two modules that use it, so vocab no longer re-exports an import it never uses
- The SHACL loader catches what loading can raise (OSError, SyntaxError) instead of every exception; its implicit string concatenations are parenthesized
- The division-by-zero check is one condition; imports are sorted

* Run the test workflow against the current repos and published metamodels

- Install the package with its own git dependencies (rdf-utils, scene-dsl, coord-dsl) instead of a stale sibling list, then pin the patched rdflib
- The tests resolve metamodels by their IRIs: conftest no longer forces METAMODELS_PATH to a sibling checkout, and the shape test loads shapes through the metamodel resolver
- The SHACL gate installs motion-spec from the branch of the same name when it exists, else dev, and checks the 01, 02 and 06 examples at their current paths

* Drop the motion-spec SHACL gate from the test workflow

- The gate installed motion-spec to build application graphs, which needs rec and the rest of motion-spec's setup; this repo's CI tests this repo only
- Install from a clone with `pip install -e .`; the git-only dependencies come from pyproject.toml
- Link the published language reference instead of a path in motion-spec's dev branch
- Development checks are the ones CI runs: pytest with the patched rdflib, `ruff check src`
- CI installs the package and its declared dependencies only; the secorolab rdflib fork is no longer forced over PyPI's rdflib
- The deprecation-warning filters stay; their comment no longer names the fork
- A Generate section: `textx generate <model> --target jsonld -o out` and the three files it writes
- The Development section is gone
- Pin ruff 0.16.9 in CI and in the pre-commit hook; drop the ruff-format hook the code was never formatted with
- Remove pyrightconfig.json: it pointed at a workspace venv and motion-spec's sources, which this package does not import
- Drop the stale comp-rob2b/ ignore entry
- The package description names the .robmot extension and RDF as the output
@vamsikalagaturu
vamsikalagaturu merged commit 3092c71 into main Oct 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants