Repository navigation
Generate, build, run and inspect guarded motion specifications - #5
Merged
Merged
Conversation
A controller commanding a Cartesian force writes the magnitude the wrench operation reads. The feed-forward branch claimed the id first and named it `cmd_`, the forwarding form, so a feed-forward force command left the wrench reading a signal nothing wrote. Only a command that goes straight to a device is `cmd_`.
An error states which way the quantity has to move: positive to rise, as an equality's `setpoint - measured` does and as a band's lower edge does. The greater-than gate returned the negation, so a controller driving it to zero pushed the quantity further below the threshold it was meant to reach. The less-than gate beside it already read the right way round. Monitors compare the magnitude, so every gate authored today keeps its verdict.
The per-axis fan-out and its pose-difference errors are unchanged; only the destination differs. A moment plan's axes are f_ext directions, so they claim neither an acceleration row nor any of the solver's row budget.
The cartesian-force pass ran before the control laws were appended, so every commanded wrench was built from the previous tick's magnitude. Held back and emitted after the controllers instead; the walk still claims in place, so shared prerequisites keep their position.
…ries A pose of a scene object, or of one of the frames that object carries, is answered by the simulator's scene -- no forward kinematics of the arm reaches a drawer. Three things kept that from resolving, each keying behaviour on a spelling rather than on what the graph states: - the object lookup compared the scene's body name, which keeps its dashes, against the generated id, which spells them as underscores, so only an object whose body name held no dash ever matched; - the same call passed that generated id on as the body to look up at run time, where the name MuJoCo compiled is the scene's own spelling; - a frame stood for its body only when named `<body>_origin`, though the body states its root in the graph and `placement_frame` already reads it. A frame that is not its body's root is now read off the site the scene already marks it with, which `_scene_frames` mints and `get_site_frame` answers; nothing composes an offset onto the chain. `site` is None rather than "" when absent, because ST4 reads an empty string as true. Generated C++ for the three maintained models is byte-identical; their IR moves only where a root frame now reports its body (`world` -> `world_body`, `table_top` -> `table`), which reaches no template.
Two constraint handlers may author the same controller name. The IRI keeps them apart, `<handler>/<name>`, but the generated id is the local name alone, so both controllers and everything derived under them claimed one id and generation failed on the first derivation registered twice. Qualify a handler-owned name by its handler when a node under a second handler carries the same name, the way a context quantity is already qualified by its owner. It covers the nodes minted beside a controller -- saturation, angle normalization, integral state -- which would have merged silently rather than failing. A name no second handler uses keeps its id, so existing models generate unchanged. Raise ConstraintViolation, not RuntimeError, when two ids still collide, and say which entities claim the id and what to do about it.
Three fixtures had drifted from what they read: - A path driver owns a velocity profile since motion-spec-dsl 299d9f3, so the dual-arm model lowers two profiles per arm -- one shaping a measured input, one owning a tangent speed -- and pick_above schedules its profile between the projection that measures the parameter and the evaluator that samples the curve. - bdd_ros2_interfaces gave TrinaryStamped a `reason` field, so the message no longer has the single payload field the publish sugar needs. Name the field, and cover the sugar on a type that still leaves no choice. - The subscription cases lowered `detect_pick_single`, deleted as redundant in bdd_collab_bhv_cpp 99d09dd. Read `real_perception_test`, which is maintained and states the same chain.
A context declaration may now state `- <offset>`, which lowers to an op whose operands are named rather than collected: subtraction does not read the same either way round, so the minuend and the subtrahend each have their own predicate and the closure renders them in that order.
`config_poses` refuses to generate a real-world model whose `[config.<key>]` section the deployment config does not state, and the pre-launch check then refused to run because that same section matched no device. Between them no model reading a pose from its deployment could run at all: every `[poses.*]` section came back as "configures nothing this run binds". Count those sections as bound, and re-check their numbers here, since they are the ones a deployment may retune without regenerating. The message now names what the run does bind and what makes a section bound -- a device by the agent its solver realizes, a pose by a `[config.<key>]` declaration -- and points at the generation it was checked against, so a stale section says where to go and look.
`agent_home_positions` reads homes only for a simulated platform, so a `home` under an agent on hardware is a number the deployment tunes and nothing acts on -- the arm starts wherever it was left. The pre-launch check passed it silently, which is the failure the check exists to prevent: a wrong home is silent, and an unread one should not be. Every homed section is reported at once, and the message asks for the line to be commented out rather than deleted, so the numbers stay for the simulated platform that does reset to them. `home` and the pose fields are named once in resources.py and read from there, so the run checks what generation derives rather than a second spelling of it.
The message said a pose section is named by a `[config.<key>]` declaration without saying what one looks like, so a reader who had never written one had to go find out. State it with the offending key already in it.
A deployment keeps as many poses as it likes and a model reads the ones it names, so an unread `[poses.*]` section is a pose on offer rather than a mistake -- rejecting it forced the file to be edited per model. Sections the run does read are still checked for shape, and a section that is neither a pose nor a bound device still fails: that one reaches hardware this run never commands.
Tuning a deployment config or a scene the controller reads at startup means running the same build again, and restating how -- which arguments, which working directory -- every time. Nothing recorded that, so nothing could repeat it. Each run now writes `invocation.json` beside its REC record, and `rerun` launches the most recent one into a run of its own. It generates and builds nothing. The record is written before the executable starts, because a run worth repeating is often one that failed.
`rerun` still had to be told which generation to repeat, which is the one thing the terminal already knows: a generation's path carries a timestamp, and pasting it back is the friction the command exists to remove. `gen` and `run` now note the generation they made, and `rerun` with no argument takes it. A generation that has never been run has nothing to repeat, so it is launched the way `run GENERATION` would launch it rather than refused.
A generation's directory is named by a timestamp, so every command after `gen` had that path pasted into it -- including `rerun`, which was supposed to be the one that did not. `gen` and `run` now point `latest` at what they make: one beside the generation, one over all the models, the way a colcon workspace carries `log/latest` and a Bazel one `bazel-bin`. `rerun` follows the second when given no argument. A symlink rather than a remembered path, because it is the tree that knows: it survives across terminals, needs no shell hook -- a command cannot export into the shell that called it -- and every other command takes it as the ordinary path it is, so `run "$MOTION_SPEC_GEN/<model>/latest"` goes back to one model after another has been generated since.
It repeated the previous run's arguments, which meant recording them, a file in every run directory to record them in, and a line claiming to be repeating a run when it was starting a new one. None of that was asked for: the generation is what should not have to be named again. So it invokes `run` with the same options, on the generation `latest` points at, under a run id of its own. The invocation record goes with it.
path_project golden-sections the parameter to a bracket of kPathTangentStep, so it converges to 1 - 5e-5 rather than 1. remaining_path_length reported that sub-resolution tail, which floored the velocity profile's braking command at sqrt(2*a*tail) -- 4.5 mm/s on a 1.4 m arc at 0.2 m/s^2. Nothing opposes that: the position constraint of a path motion holds only the two path-normal directions, so the arm tracked the residual command off the end of the path indefinitely and the arrival monitor never latched. The tail is below what the projection can resolve, so report it as zero and let the profile brake to a stop.
`standing_shape` descended the message to the ROS type the quantity maps to, and the descent skips repeated fields -- so a Detection3DArray, whose detections are exactly that, reached no pose and was rejected. Nothing could publish one. When the payload is reachable only inside a repeated field, the message carries many of them: `_entry_shape` asks of one entry the questions `observation_shape` already asks of an arriving detection -- where the pose goes, which string names what it is about, which header states its frame -- so a topic can be published in the shape another model reads. The IR is one entry per reported quantity. What the model settles stays out of the loop: the array's size, each entry's id and frame are written once at setup, and the tick writes only the quantity and the stamps.
Where a body sits is composed along the poses that place it, searched over every `geom:Pose` the graph relates -- which includes the model's own context quantities. A world pose relates two frames the scene has already placed, so it is the shortest way between them, and the search takes it. Declaring a world pose of the table anchor was enough: the anchor is on the arm's own placement chain, so the arm resolved through a coordinate that holds no value until the first cycle and generation stopped. A spec pose is worse -- `look-wall-pose` relates the same two frames and does carry authored values, so a placement routed through it would have resolved to a motion target and said nothing. The search now runs over a graph without them, cached per model.
The rate is read off the monitor and turned into cycles between two messages by `annotate_publish_rates`, which runs where the control period is known -- a rate says how often in seconds, and only the loop knows how many cycles that is. A monitor stating none carries no divider, so its publish renders exactly as before. `ros_standing` had to learn to skip monitors. Both kinds of publish now state a rate with the same term and a monitor's node is typed a topic, so without the guard a rated monitor would be lowered as a run-wide publish and rejected for naming no quantity to report.
A pose read from the deployment config carries no authored components -- its whole frame arrives from the TOML file at runtime -- but the DSL stamps its orientation Euler-typed for lack of an authored coordinate to inspect. Motion-spec read that stamp as a literal pose and required every axis to be filled, so referencing a single axis (e.g. `.position.z`) left the rest rejected as missing. A config pose is now treated like a snapshot: never authored, never decomposed.
Owner-qualification matched a node whose namespace is exactly a handler, so `<handler>/<name>` split by handler but `<handler>/<controller>/<name>` did not. Once the DSL hangs a controller's profile op under the controller, two handlers with same-named controllers mint distinct IRIs whose ids still collapse -- and the id index keeps the first, so one motion's velocity profile silently disappeared from the IR. Match anywhere below the handler: a node the model hangs under a controller took its name from that controller, so it is as handler-scoped as the controller is.
mj_kdl_wrapper 0.3.6 fixes headless EGL recording picking EGL_DEFAULT_DISPLAY over a GPU device. health.py's MUJOCO_BUILD_PACKAGES was still pinned at 0.3.4, one version behind entry_build.stg's prior 0.3.5 -- health would pass an install the generated CMakeLists then rejected. Both now read 0.3.6.
A pose referenced only as geom-op:in1 of a ComposeOrientation was invisible to snapshots_for_motion's expand() (never captured -> read but never written) and to filter_shared_data (dropped from the blackboard -> C++ missing member). Both now follow the pose-components operands.
A run killed before the archiving step wrote manifest.json could never be replayed: resolve_archive hard-required verify_manifest even though the frame log carries its own decode contract, and run_dir_for's manifest-less fallback resolved to the wrong directory. Locate the frame log via the conventional logs/frame_log.pb layout when no manifest exists instead of rejecting outright.
…t dev from use A bare `cmake --build --parallel` emits `make -j`: unlimited jobs, overriding CMAKE_BUILD_PARALLEL_LEVEL and MAKEFLAGS too, so nothing could cap it and the machine swapped until the OOM killer took the terminal. build_jobs() now uses min(usable cores, RAM/2GiB), with -j, the cmake variable and [setup] jobs on top; the colcon path is capped through MAKEFLAGS. The console log opens with argv, workspace, job count and host memory, keeps the CLI's own lines, stamps each command and records the exit status or the signal that killed it. The file copy drops ANSI and progress-bar redraws. rdflib goes back to upstream: its multi-type scoped @context bug never fires on generated models, whose context is a prefix map with no type-scoped entries. The Python components are pip-installed from their pinned git refs; --dev checks them out into WORKSPACE/src and installs them editable.
The four CMake packages and STSTv4 have no pip or apt route, so a tree must exist to build them -- but src/ means "yours to edit", and a plain install had no business putting checkouts there. source_root(root, dev) now sends them to WORKSPACE/.ms-sources; --dev puts everything, Python and CMake alike, back in src/ as before. colcon follows through --base-paths, and `vcs import ws/src` stays right for the dev layout. Removal finds whichever tree exists, so --clean works across modes. Also fixes the log's file copy double-indenting every line that had not been redrawn, and the setup stubs in test_cli that enumerate every keyword argument and so rejected jobs and dev.
Nothing uses kdl_parser. Neither generation template mentions it, the built controller links mj_kdl_wrapper, orocos-kdl, tomlplusplus, coord2b, protobuf and mujoco but not it, and neither mj_kdl_wrapper nor coord2b references it. The chain reaches C++ through the generated .kdl.hpp, so nothing parses a URDF at runtime. Worse, the fork installs no CMake config, so the find_package probe could never succeed and health failed forever on a good install, offering a remedy that reinstalls the same two files. Gone from the manifest, COMPONENTS, health and the docs. health now closes with what you can actually do -- "sim ready | real NOT ready (robif2b)" -- counting base, dsl, codegen, build and runtime plus the target's own pair. ROS and introspection are left out: a model needs those, not a target. A target whose profiles were not all checked gets no verdict. Also removes templates/CMakeLists.txt, superseded by entry_build.stg's cmake_mj_kdl and cmake_robif2b, and documents the .ms-sources layout.
… a run fails A missing attachment was a runtime error: the controller generated, compiled, linked and then printed "MJCF attachment not found" on startup. unresolved_assets now resolves every .xml the IR names the way the generated find_asset_path will -- absolute, cwd, then the mj_kdl_wrapper cache prefixes -- and generate_code refuses the IR, naming each path and the roots it searched. The run summary said "done run <dir>" whatever the exit status, then echoed the directory again on its own line. A non-zero run now says "run failed (N)" in red, and the redundant echo is gone -- the path is already on the run directory line above.
…kers by component A scene names its assets relative to nothing in particular, and the generated program resolved them against its working directory, so a model ran only from the one place its paths happened to suit. resolve_model_assets now rewrites the files a model keeps beside itself to absolute paths while the model directory is still known; a vendored path stays as written, for the cache to answer. Both resolvers expand a leading ~, so a scene can name ~/.cache/mj_kdl_wrapper outright, and both now match the cache markers with relative_to and lexically_relative instead of searching the string. src/mj_kdl_wrapper/assets no longer matches inside mysrc/mj_kdl_wrapper/assets; no path in the models relies on a marker appearing mid-path. MissingAssets carries the IR, the paths and the roots it searched rather than a formatted sentence, and the CLI prints it through _Reported, which stamps every line like the rest of the log instead of dumping a traceback under an Error:.
…ore release `setup --dev` wrote a config saying `dev = false`, because the sample hardcoded the built-in defaults instead of the values in force. A later bare `setup` in that workspace would read it back, reinstall the Python components from git and undo the checkout. sample() and write_sample() now take dev and editable, and setup passes what it resolved; `config --init`, which has no run behind it, still writes the defaults. The config format goes back to 1 and the marker to 2, with the pip origin folded into v2's description. At 0.1.0 with no tags nothing has read either format but our own test workspaces, so a pre-release format grows in place -- the bumps only made every existing workspace warn "version 1, now at 2" on each read. No reader needed them: an older motion-spec meeting a pip marker reads "pip", finds it is not "cloned", and declines to remove a source, which is already correct.
A ROS setup in a plain venv dies building the mj_kdl_wrapper bindings: its cmake finds rclcpp, ament runs its own scripts under the venv interpreter, and ament_package is not there. pip strips PYTHONPATH for an isolated wheel build, so sourcing the distro does not reach it -- the interpreter has to carry it. health now probes it beside rosidl_runtime_py and ament_index_python, with `python3 -m venv --system-site-packages` as the remedy rather than the sourcing line the other two take. The verdict was one line stamped by the worst target, so "sim ready | real NOT ready (robif2b)" arrived as an error even though sim was fine. One line each, each stamped for itself.
find_package(rclcpp) makes ament run its own cmake scripts under the interpreter configuring the build, and those import ament_package. It reaches the venv over PYTHONPATH from the sourced distro, but pip replaces PYTHONPATH with its own for an isolated build, so `setup --ros` died on the mj_kdl_wrapper bindings with a bare "returned error code 1" and left the workspace without its environment file. A ROS install now passes --no-build-isolation, and since pip then installs no build backend either, the checkout's own build-system.requires go in first -- read from its pyproject, so nothing is hardcoded. ament_package joins the ROS import probes. Its remedy is the same sourcing line as its neighbours: with a distro sourced it imports, so health cannot see this failure, which lives entirely inside pip's isolated build.
Finding out that libeigen3-dev is absent after four checkouts and a build helps nobody. setup now runs the health checks its components need, keeps the ones apt must answer, and stops. Everything setup installs itself carries a `motion-spec setup <name>` remedy instead of an apt one, so it cannot refuse over the very things it is about to build. Scoped to what was asked for: `setup coord2b` wants the build profile, not the Ant and JDK only stst uses, and a Python component needs no profile at all. A ROS workspace adds colcon and the distribution's Python packages -- which absorbs the separate gate added for the latter. A missing yaml is what says the interpreter cannot reach apt's packages. The pyvenv.cfg flag does not: `uv venv --system-site-packages` sets it while building on uv's own CPython, whose system packages are not the distribution's.
…tro first Sourcing it left `motion-spec: command not found`, because the file said nothing about the environment setup ran in. It now activates that venv, and guards both ROS sourcings so a shell that already has them is unchanged -- sourcing twice leaves CMAKE_PREFIX_PATH the length it was. The file is written executable. The ROS distribution joins the preflight. _write_ros_environment refuses without one, and that is the last thing setup does: every checkout and every build would happen first, against whatever was sourced, before the refusal. --no-build-isolation goes back to the C++ bindings alone. It applies to a whole pip invocation, so a pure-Python component took its transitive source dependencies down with it: `--dev --ros` died installing motion-spec-dsl for want of hatchling, which belongs to a dependency and is in no pyproject setup can read.
…-dev omits motion-spec The quickstart described one path -- a checkout, a venv, pip -- and the install now has three independent choices: using it or working on it, colcon or not, pip or uv. Each has its own block, and the ROS one says why the environment must be built on the system interpreter, since a uv environment on uv's own CPython sets --system-site-packages and still reaches nothing from apt. --dev gives editable checkouts of every component except the one most likely to be edited: setup cannot check out the code it is running. It now says so, naming where motion-spec is installed from and where to clone it instead.
It reached into the venv with `uv pip install --python <path>`, which is what setup does internally and not what anyone should type. Create, activate, install, as in the three blocks above it.
Three ways a workspace can say what it builds, none of which it could before. A checkout already in the workspace is adopted at whatever ref it is on, rather than stopping the run for not being the pin. It is built as it stands, the drift is named, and the marker records the commit that was built -- so a rerun compares against the tree, not against a pin it never matched. Uncommitted changes are built too, and rebuilt every time, because nothing recorded can describe them. Only a directory that is not a git checkout is still refused. --external names a component you supply yourself: dropped before anything reads the list, so no clone, build, marker or prerequisite check, and health still reports whether it resolves. --repos replaces the manifest outright. What it omits must already be checked out, and setup says which components are left with neither a pin nor a source before it clones anything. The pins are passed down rather than rebound onto a module global, and health reads the manifest in force per call instead of the version it happened to import.
…xamples A fresh clone recorded the pin it was asked for -- a branch name -- while every later run resolved the checkout's HEAD, so a sha was compared against "master" and every component was rebuilt on every run. The clone now records the commit it landed on. Adoption follows the tree rather than the mode: source_tree takes this mode's directory when it holds a checkout and the other mode's when it does not, so a plain install builds src/<repository> where it finds one instead of cloning a second copy into .ms-sources. --clean never removes a source tree, whoever cloned it. It takes the installed files, the build directory and the marker, names the checkouts it left behind, and asks before each one: a prompt per component listing the paths and their sizes, default no. --all offers the whole build, install and log trees and the environment files, which is wider than any marker reaches; -y answers every prompt for a script, and end of input instead of an answer says to pass it. The colcon call names its build and install bases, so --prefix reaches the place the markers and the environment file describe rather than colcon's cwd default. The ROS prerequisite checks -- distro, colcon, the distribution's Python packages -- run even for a selection that needs no health profile, which is where a Python-only setup used to fail at the last step, after installing. is_installed reads the first marker line, so a half-finished install is no longer reported as done. motion-spec examples copies the models that now ship inside motion_spec_dsl into WORKSPACE/src/ms-examples, or --into anywhere else. It adds only: a file already at the destination is kept and reported, never overwritten. The tutorials and three fixtures follow the models to their new home. The README is a quickstart again, with the core requirements as a table and the RA-L paper as a citation at the end. setup.rst gains uv, the job count, --repos and --external, drops a flag that never existed, and answers what happens to a checkout that is already in the workspace.
The C++ components have adopted an existing checkout since 98f5dcf, while the Python ones kept taking the pip route before they looked: a src/motion-spec-dsl you were editing was ignored, and the pinned ref was installed over it on every run, including over an editable install made by hand. pip now fetches a Python component only when the workspace has no checkout of it, and one that is there is installed with pip install -e, so the DSL that runs is the DSL being edited -- and `motion-spec examples` copies the models out of that tree. component_installed compares the marker's origin against the route this run would take rather than against --dev, or the adopted checkout would count as not installed and be reinstalled every time. --editable defaults on, since it only ever applies where a checkout is installed, and it is no longer a usage error without --dev: that error's premise, no checkout to edit, is gone. mj_kdl_wrapper's compiled bindings stay editable only under --dev.
The MuJoCo target checked one thing, the mj_kdl_wrapper package at its pin. Its own CMake requires glfw3 and OpenGL, its video recorder is a stub without EGL, and both the recorder and motion-spec's ROS camera recording pipe frames to ffmpeg. None of the four was probed or in the apt line, so on a fresh machine health named only mj_kdl_wrapper and `setup` then failed in its configure step. Four rows now precede it under build[mujoco], each with its why and apt remedy. The two libraries are found as mj_kdl_wrapper finds them, with find_library; OpenGL goes that way too, since FindOpenGL exposes no config path. The README table and the Setup page's MuJoCo row list the same.
The page had no way to learn that a log-less run had begun: live_state returned before opening the control block, and the no-log verdict came from the manifest the runner writes only after the process exits. A simulator armed at --start-paused therefore waited for play behind a page waiting for frames. jobs.py keeps the log choice in the RUNNING entry and run_status reports it before the manifest exists. live.py builds the session for such a run from the generation's contract record, so the sampler and control block are reached and `started` means the loop answers. resolve_archive raises ArchiveError for a manifest without a frame log, and the page hands a finished no-log run to its console view once the runner exits.
…cument, the run as the Execution, no runtime graph
…out a catalogued run
`run` on a `gen code` output was refused with "generated/provenance/ motion-spec.ld.json: required generated artifact is missing". The validator still expected the old nested path while generation writes `generated/provenance.ld.json`. Both call sites now use GENERATION_DOCUMENT, so the two cannot drift again.
Follows the KDL fork onto vereshchagin-driver-weighting, which collapses the four solver variants into one. A motion with a Cartesian force no longer needs a second `_Fext` solve: the single solver runs with setDriverWeights at weight one, so the constraints let the modelled wrench through instead of compensating it. `beta` is now the true acceleration energy the model states. The solver offsets it by the root acceleration internally and reports true accelerations back, so `beta = 0` holds still and no caller shifts it by gravity any more. On robif2b the constraint torque is blind to a wrench it lets through, so the torque that exerts it -- `J^T f_ext` per forced segment, from ChainJntToJacSolver -- is added per segment.
The generated CMakeLists demanded mj_kdl_wrapper::camera_ros, and main.cpp included camera_ros.hpp, for any model that used ROS at all. A model with one publisher and no camera therefore failed to configure against a wrapper built without ROS -- MJ_KDL_WITH_ROS defaults to AUTO, so a wrapper built in an unsourced shell simply has no such target. The scene's cameras were already in the IR as composition.scene.cameras; the build template just never received them. It and app-chain-includes now do. The guards test `first(scene.cameras).id`, not the list and not `first(...)` alone: with stst's JSON binding both of those are true for an empty list, so only a property of a real element distinguishes. ir.py:189 records the same trap and solves it with `x or None` where a dict is being built.
The release that carries the vereshchagin-driver-weighting build and the Python examples' command fix. Both places that name the version move together, as the suite asserts: the manifest setup clones from, and the find_package the generated build emits.
rec dropped consolidation, so archive verification no longer calls _verify_consolidated; _require_rec_provenance already covers the run identity check and the SHACL check ran one line earlier. add_resource and add_artefact take used_by/used_at/generated_by/generated_at/label, dependencies() emits version, and record_software replaces the log_repositories + log_dependencies pairs in the runner and the archive.
… dashboard rec now records an OSLC state and verdict instead of a status: the lifecycle reader returns both, the runner ends a run with a verdict, the dashboard's runs and rows carry state and verdict and the badge shows the verdict of a complete run. rec's record is <run-id>.ld.json and the run's own execution document sits beside it; modelled robots are read from the generation document the caller names, which the run directory does not hold at start. The dashboard keeps one entry per run rather than per generation, lists a generation's live runs with a stop each, and stops or controls a run by its path. The runner names the frame and control blocks per run so two runs of one generation share nothing; the real robot still runs one thing at a time.
* Record occurrences in the runtime; project runtime.ttl while the run runs The runtime already detects every semantic edge (state change, event, constraint and monitor satisfied edges, watched members held or lost) and threw them away per tick. It now appends each one to logs/occurrences.pb, with a whole frame sampled at the declared interval, and one projector fed from that stream grows runtime/runtime.nt per triple and rewrites runtime/runtime.ttl atomically during the run. The archive, the dashboard and replay --recover-runtime-ttl read the same projector and file; the post-run recovery from the frame log, --no-runtime-ttl and the dashboard's own projector are gone. The runtime document's provenance names the projection activity. * World model layer: port table, one kinematics, backends with read and apply only Every measured and commanded physical variable is one port in a generated table, bound by address at startup and sealed; scene objects are world frames read from the integrated state through mj_kdl::SceneState, so nothing lags a step behind and the real backend accepts them. Backends have two hooks per tick, read and apply; the solver, forwarding and perturbation rules are backend-neutral and write command ports. WorldKinematics runs one outward pass per cycle carrying poses and twists; the chain-root inverse is taken once per runtime, the tip twist and the RNE wrench frame come off that pass, and no chain FK runs inside a motion. Requires mj_kdl_wrapper 0.3.17. * Tests: the proto parser skips enum bodies, gen takes a sample interval, a run archive carries its graph * Tests follow the world pass: twists and the RNE frame are world reads, the IR section takes its ports * Recovery reads the same design graphs the live projector did; units resolve order-independently; the observing agent is a sosa:Sensor A generation run's manifest imports its design graphs in place and lists no model_imports, so a reprojection found no signal map and dropped every sampled observation. One function now hands both paths the graphs beside the manifest. A subject declared with two units stays unmapped and each borrow pass reads only what the pass before settled, so the projection is a pure function of the stream. sens:ObservationShape requires the madeBySensor object to be a sosa:Sensor. * Tests: a root frame's segment is named after its body; a perturbation test places its body * Resolve a world joint's segment by the joint's scene IRI, never by its local name Two grippers on two arms declare the same local joint name; a suffix match put both measured ports on the first gripper's segment and the world model refused the second binding. * Test: the two-gripper port table places its free bodies * Parity doc: the wrench law as 51da846 runs it The page still described the pre-51da846 sim form, qdd = ACHD(f_ext = w) then a wrench-free RNEA bridge, and listed RNEA(qdd, 0) + tau_w as rejected. That rejection predates the fork fix that makes the _Fext solver return J^T f_ext alone; with it the composition is sound and both backends now run the same two passes, forking only in the completion. States the doctrine (a wrench is exerted, not absorbed), why the default solver cannot prioritise f_ext, the authoring consequence (redundancy pinned in joint space, impedance gains sized as joint torques), moves the old sim form into history, and names which models gate what. * Tell a free body's tree by its having no joint, not by a flag scene-dsl marked each tree free_root, but the trees already say it: a body no joint holds comes out as a tree of its own with no joint in it, the scene's tree articulates at least one. The port table reads that instead, and the scene-dsl flag goes. * Publish the world forest on /tf under the scene anchor: driven trees, scene-placed free roots, joints through the port table * Runtime graph: firings and goal satisfactions are typed, a maintenance is informed by the observation it was judged on, instants stand at dateTimeStamps, the tick rate is the controller's, the projection names its projector * Let a mobile platform be driven, placed and observed A force controller routed to a force distribution never reached the loop: _cartesian_force_nodes walked only serial chains, so the ops building its wrench reached the IR and no call reached the tick. A distribution's drivers hang off the solver, and are collected now. A PoseToDirection taken off a distance pointed at a carrier the DSL declares and never emits; it is retargeted to the distance's own relative pose, which is the line the distance measures. The platform itself was never placed in the world model: a tree only earned a free-root port when no segment carried a joint, and a platform articulates below its free root, so the world model held it at its spawn pose for the whole run while MuJoCo moved it. Trees are maximal joint-connected sets, so the test is whether the tree is the anchored one; an agent tree resolves its spawn body through the ktree mapping. A twist seen by a frame off this chain is answered the way a pose already was, by composing two world reads -- without it a hand held in a table frame has no correctly-framed derivative to damp against. The wrench a distribution sums is carried into the platform's own axes; the frames it needs are claimed as world reads, keyed by segment because two arms carry one bracelet_link. * Wire the platform through the loop's remaining seams The pieces the mobile-base work still had open in the tree: a force distribution carries the wrenches routed to it, the platform's twist and its commanded wrench are answered by the solvers that write them rather than by a chain, and the MuJoCo backend reads the pivots and hub rates and drains the hub torques through the same dictionaries the KELO drive uses. Committed as they stood: this is the tree that produced the verified eddie_align runs, not a separate change on top of it. * Carry an object's authored colour into the MuJoCo scene read_scene dropped what the scenex states about how an object is drawn, so the colour reached the graph and stopped there. It is read per asset now and expanded for a path-backed object too, which the backend emits as rgba beside the asset path. * Drive any arm in simulation, and place a commanded wrench from its own frame and point mj_kdl robot templates dispatch on the backend alone, so the simulator drives whatever arm the scene holds; only robif2b dispatches on to the robot model and rejects one it has no driver for. A commanded wrench is moved onto its segment from its as-seen-by frame and reference point, all three read off the world model, for ACHD and RNE alike. * Set up the workspace from the .repos manifests - `motion_spec.repos` is the one source list: Python repos under `thirdparty/`, then the CMake packages in build order; `motion_spec.real.repos` layers the real-robot drivers behind `--real` - `motion-spec setup` makes the venv, runs `vcs import`, pip-installs the Python repos (editable with `--dev`) and builds the CMake ones, with or without colcon - CMake flags come from `colcon.meta` (shipped defaults, the workspace's own overrides) - Sources always live in `WORKSPACE/src`; `thirdparty/` gets a COLCON_IGNORE under ROS - Drop the `[setup]` config section, `.ms-sources`, the git pins in pyproject and the `dsl` install feature - `setup --clean` prints each trashed item with where it landed in the trash, and each deleted marker - health, provenance and CI read their sources from the manifest * Follow mj_kdl_wrapper 0.4 and the numbered examples - Generate against the 0.4 API: `mj_env.scene`/`mj_env.viewer`, status returns with their error, `set_control_mode`, `open_viewer`, owned path strings, per-object name prefix - Name force-torque sensors by their local id and frame; the wrapper adds the runtime prefix - Drop the scene colour override, which 0.4 no longer takes - Leave out a chainless gripper's own assembly when a chain's assembly already mounts it - Docs, fixtures and tests use the renamed examples (`01_pick_and_place`, ...), found through the installed DSL's `models` * Apply the audit's fast fixes and bug fixes - remove the mutation package, the diff/journal/archive commands and the ir/codegen commands; gen ir|code is the one entry point - remove dead functions and module mains in introspection, generation and rdf_parser; drop nicegui - forward kinematics: a chain whose solver has no dynamics family is read only and held in position mode - fix dashboard -k/-r port detection: ask click whether --port was given - fix the JupyterLab frame policy to use the dashboard's own port - cmake_robif2b links hddc2b, LAPACK, BLAS, KELO and EtherCAT for a mobile base - remove can_start_* and monitor_<motion>, never called, with when_terms and their *_needs_* fields - key aggregate monitors' watched errors by constraint IRI: short ids collide across motions and made generation nondeterministic - tutorials follow the reworked examples - tests and fixtures follow the removed commands and the solver algorithm * Apply the audit's section 4 simplifications - drop the dashboard Reports sweep (analysis.py); the verdict table stays - sampling: seed from [sampling] seed in the robot config, draw recorded in the frame log; no --seed, no sampling.json - frame log: one shipped frame_log.proto compiled by protoc, format_version 2, repeated slot lists with sparse quantities, Controller/Monitor header slots and ControllerSample/MonitorSample frame samples - setup: one --clean, HEAD-only install markers, no --prefix/--repos/--cmake-arg/--clear-cache/--all/-y, no stale stst migration - config: one key table and resolver; workspace.shell removed - archive: drop the flat layout - dashboard: one raw_frames() walk, pidfile-based -k/-r, --lan on the click command, no argparse main, no unused RunStore - templates: runtime.stg keeps the loop core; helpers move to their domain templates and are emitted only when the spec uses them - templates: empty lists reach StringTemplate as null, replacing 11 presence flags; backend wrappers inlined at their call sites - introspection controller slots built from the IR rows - fix nondeterministic generation: sorted alignment and force walks, and a data id with several writers reads all their inputs * Apply the audit's section 4.3 and provenance rework - introspection splits into runs/ (runner, archive, replay, provenance, lifecycle events, ros video) and telemetry/ (frame log contract); .gitignore ignores only the top-level /runs/ so the package is tracked - scene and FSM graphs, the KDL headers and the FSM header come from scene-dsl and coord-dsl in memory; per-tree KDL namespaces and headers; no intermediate ld.json beyond what a reader needs - the scene and FSM graphs are kept as model/<source>.ld.json and listed in a run's files.graphs; the dashboard's model graph loads them - generation provenance written once: DSL, coord-dsl and motion-spec graphs, generation-scoped activities and file entities, versioned package agents, sources copied into generated/source - derivation document: every derived quantity the IR uses with its kind, unit and operands, typed derivation targets - run provenance: run-scoped controller process, mj_kdl_wrapper as the simulated runtime, the built executable as the used entity, rec logs only itself and the run checkout - the environment file is copied into the run as files/environment.<ext> - derived ids registered lazily by SolverIdFactory; acceleration driver ids follow one rule; per-axis moment control signals - QUDT kind and unit IRIs on quantities - cli: shared run options, replay --verify, view command for a .scenex - build with hatchling and hatch-vcs * Follow the source API in the tests - runs/ and telemetry/ replace introspection; the IR's telemetry key replaces introspection - new signatures: generate_code, the record_* provenance writers, build_derivation_document, MotionDrivers.handler, ChainBinding.namespace, MjcfSceneRobot.agent - archive tests use the generation layout; example and id renames followed - tests removed whose subject is gone: proto field numbering and proto/descriptor parity, flat-layout archive, can_start/when preconditions, per-package clean - tests/support.py load_model mirrors generate_model's in-memory load - still failing: generation determinism, ROS interface packages, frame-log .zst expectations, two hand-built scene fixtures * Expect one interface package per ROS shape - a message package exports the interface packages its fields reach into, so the build finds only the one the model names - subscription, standing publish and action server tests assert that package * Pack an archived frame log with zstd again - create_archive_manifest packs logs/frame_log.pb into frame_log.pb.zst once the run has stopped writing it (level 10; 01's log 20 MB -> 8.4 MB); manifest and rec name the packed file - zstandard moves from the test extra to the runtime dependencies - concepts.rst run layout: packed log, files/ environment copy, scene and FSM graphs in model/ * Cut the test suite to what example runs cannot show - Apply the audit's KEEP/MERGE/REMOVE lists, then keep only numerics, conventions, safety rejections and archive/dashboard security - Drop test_generation_determinism: regeneration need not be byte-identical - Take over the DSL's IR checks, incl. the Table II geometric operators with the table_edge_reach fixture - Hand-built graphs as Turtle text, no builder helpers or closure fixtures - 12,604 -> 3,909 lines; 126 passed * Remove dead code, simplify records and gate unused runtime code - Dead code and stale plan references gone; numpy declared, empy/lark/pillow dropped from [test]; PROV from rdflib only - health derives what setup provides from the shipped manifests - One base record for controllers and for monitors; one JointQuantity record and reader; caches via @reader - artifacts.py reads keys the IR always carries with [...] - One cmake_project template for both backends; always-true FSM guards gone; admittance, velocity-profile and KDL chain code emitted only when used - One ROS image subscription for the recorder and the dashboard camera - pipeline.load_model shared by generate_model and the tests - Unread manifest_version and provenance schema_version removed - Not verified: no tests or example runs since these changes * Split the parser by concern and model the algorithm in F/D/S blocks - Split resources.py into agents, runtime, scene and deployment, quantities into quantities and views, and coordination into coordination, fsm and ros_messages - Templates in folders by IR section, one root group per backend (backend/<name>/main.stg); shared groups call hooks by plain name, no backend dispatch, every runtime block gated on use - Controlled expressions: the controller divides its error by the expression's gradient norm, and quotient is guarded for vectors - Algorithm in Bruyninckx's terms (2026, sec. 2.3): computation.data holds the D-blocks, computation.functions the F-blocks (was closures), computation.data_access the data access constraints {write, read} with ports - Generated C++: struct algorithm_data, instance data, header algorithm_data.hpp; functions.stg replaces closures.stg - Data access is built from the records once every D-block exists; its analysis sets cadence and storage and removes D-blocks nothing writes; telemetry only taps it - Controller state and gains, joint-space mirrors and goal status are created by the concern that owns them - New provenance section (uris, derivations); a value read through a view derives from the value it views - Provenance documents under generated/provenance/ and runs/<id>/provenance/; every prov:atLocation is a path from $MOTION_SPEC_GEN, stated as each document's @base; the run's files are recorded in execution.ld.json - generations_root() is $MOTION_SPEC_GEN only, with no workspace fallback - Frame-log format 3: ConstantReader {id, kind, port} replaces ConstantConsumer - Remove the unread signal rows; replace getattr type sniffing with isinstance and tables; replace order-keeping dict dedupe with modelled order or membership checks; check_health takes a required progress callback - Architecture docs moved into Sphinx (architecture.md, diagrams in _static/); ruff check and format clean - Not verified: tests and examples not run since these changes * Fix the data-access analysis, archive verification and config check order - Skip a view whose subobject is its own superobject: a whole-component view of a pooled relation reads the superobject's sampling and would otherwise wait on itself - Check rec's own record for the run's type and agent, and the run's file use and SHACL shapes against rec plus the execution document, so a tampered rec record no longer passes through the merge - Check bound devices, config poses and unbound sections in IR and config-file order instead of set order, so the first error reported is stable - Build real Wrench, Pose and Frame records in the ROS publishing tests instead of SimpleNamespace stand-ins * Validate a run's provenance with the generation that describes what it used - The SHACL check of the run also reads the generation's provenance, where the executable the run used is described as an entity - Checksums and rec's own record are still checked against the run's documents alone * Follow mjkdl, model solver families as one table, merge band constraints - Follow the mjkdl rename: `find_package(mjkdl 0.5.0)`, `<mjkdl/mjkdl.hpp>`, `mjkdl::` in generated code; `motion_spec.repos` pins mjkdl `v0.5.0`; `colcon.meta`, health, setup, provenance, dashboard and docs use the new names - Drop the Menagerie lookup from generated programs, `codegen.asset_candidates` and `view.py`; `view.py` reuses `asset_candidates` for the asset cache - Solver families are one `SOLVER_FAMILIES` table of `SolverFamily` records keyed by algorithm IRI; FPK/FVK are read-only entries instead of `None`; the marker classes and `issubclass` dispatch are gone - `BandConstraint` replaces the identical `BilateralConstraint` and `OutsideConstraint` records; the IR `type` is unchanged - Raise `ConstraintViolation`, not `RuntimeError`, for an unsupported robif2b robot model, an output the simulator cannot report, a forwarded joint outside the handler's trees and a controller missing a required gain * Drop sorts that manufactured an order - mobile_base: platform wrench terms are summed, so their order is irrelevant - artifacts: devices and header slots already come in index order * One solver state per solver instead of one per motion - solver state lives in robot_io as <solver>_state, initialized once in main - sized by the new constraint_rows (widest motion); each motion zeroes the rows it leaves - ACHD f_ext and driver weights set once; a motion without a wrench passes f_ext_zero - per-motion solver members and init_<motion> removed; motion functions take robot_io non-const - hold_gravity_<motion> takes only robot * Target mj_kdl_wrapper 0.5.0 - `motion_spec.repos` pins `mj_kdl_wrapper` (vamsikalagaturu/mj_kdl_wrapper) at `v0.5.0`, checked out as `src/mj_kdl_wrapper`; `colcon.meta` passes `-DMJ_KDL_OROCOS_KDL_FROM_PACKAGE=ON` - Generated code uses `find_package(mj_kdl_wrapper 0.5.0)`, `mj_kdl_wrapper::mj_kdl_wrapper` / `::camera_ros`, `<mj_kdl_wrapper/...>` headers, namespace `mj_kdl` and `MJ_KDL_WITH_ROS` - Assets resolve from `src/mj_kdl_wrapper/assets` and the `~/.cache/mj_kdl_wrapper` cache, in generated code, `codegen` and `view.py` (`import mj_kdl_wrapper as mjk`) - health, setup, provenance, dashboard, README and docs use the same names * Read a joint's force instead of a gripper's motor current - A `joint-force` solver output reads the chain joint's effort port (`joint-effort` per backend; robif2b negates it); a joint off the solver's chain is refused - The gripper motor-current output, its world port and the simulator refusal for it are gone - Tests build solvers through `SOLVER_FAMILIES` * Pin motion-spec-dsl to v0.1.0 - `motion_spec.repos` checks out the DSL at its first release instead of dev * Run the test workflow without ROS and with the DSL beside the checkout - vcstool from apt: PyPI's imports pkg_resources, which current setuptools no longer ships - The manifest is imported outside the checkout and pytest runs tests/ only, so other repositories' suites are not collected - MOTION_SPEC_GEN is set, and motion-spec-dsl v0.1.0 is cloned next to the checkout, where the fixtures import its scenes from - REQUIRES_ROS moves to support.py; the tests that build example 06's IR, which publishes a ROS topic, skip without rosidl * Install protoc in the test workflow - The frame-log codec is generated with protoc, which the runner does not have; the README already lists it as an apt dependency * Show how to fetch the sources by hand before setup - Importing the manifests with vcs first leaves setup only to build what is in src/ * Import the sources by hand; setup only builds what src/ holds - The manifests move to the repository root for `vcs import src < src/motion-spec/motion_spec.repos`; the wheel ships copies through force-include, and an editable install reads them from the clone's root - setup no longer runs vcs: it builds the checkouts it finds, skips a missing one with a hint to import it, and stops before touching anything when none is imported - setup --dev accepts motion-spec cloned anywhere under the workspace's src/ - CI imports the root manifest; README, docs and the manifest header describe the import step and the colcon (--ros) variant * Lay out CI as the manifest does instead of cloning the DSL twice - motion-spec is checked out at thirdparty/motion-spec and the manifest is imported into the workspace, so thirdparty/motion-spec-dsl sits where the test fixtures import from - Every step runs from the workspace; the separate motion-spec-dsl clone and its second copy of the pin are gone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
motion-spec health