Gallery › robotparts › Actuator model page

Actuator model page

One actuator model's own page, the one every model name in the models case links to: what the model is (its robotparts contract 2.3 kind and equation, what the twin actually runs), its error on each bench, its fitted terms, the commit the rack's twin code was fitted with and that code's README at the commit, rendered as markdown. Configuration only: a record view and a table over the models case's data file, the model picked by the rail.

robotpartsrecordtable2 views of one models.json

The .base file and its data

Every tab above is one entry of this file; the views read these rows. ⋯ in the toolbar copies the current view as YAML.

# One actuator model's page: what it is (contract kind and equation), what the twin runs, its error, its fitted terms,
# the commit it was fitted with and the twin code's README at that commit. The rail picks the model; a link from the
# models case opens it as ?variant=<model>. Configuration only over the models case's data file (CONTRACT §16).
# Check it: node bin/data-ui.mjs check <this file>
data: ../robotparts-models/models.json
facets:
  - { property: variant, as: select, label: Model }
  - actuator
properties:
  variant: { displayName: Model }
  actuator: { displayName: Actuator }
  rung: { displayName: Rung }
  family: { displayName: Family }
  description: { displayName: What it is }
  readme: { displayName: README (at the fit commit) }
  code_commit: { displayName: Fitted with commit }
  code_path: { displayName: Code (local) }
  code_hosted: { displayName: Hosted }
  code_url: { displayName: Code on GitHub }
  headline_x: { displayName: Held-out error }
  dataset_x: { displayName: Dataset }
  worst_class_x: { displayName: Worst class }
  floor_tape_x: { displayName: Floor tape }
  twin_grade: { displayName: Twin-grade }
  benches: { displayName: Benches }
  n_runs: { displayName: Runs }
  coulomb: { displayName: Coulomb }
  viscous: { displayName: Viscous }
  weakening: { displayName: Weakening }
  ripple: { displayName: Ripple RMS }
  backlash: { displayName: Backlash }
  inertia: { displayName: Inertia }
  kt: { displayName: Kt (output) }
  current_loop: { displayName: Current loop }
views:
  - type: record
    name: Model
    filters: 'kind == "model"'
    record:
      sections:
        - { name: Model, properties: [variant, actuator, rung, family, description] }
        - { name: Error, properties: [headline_x, dataset_x, worst_class_x, floor_tape_x, twin_grade, benches, n_runs] }
        - { name: Fitted terms, properties: [coulomb, viscous, weakening, ripple, backlash, inertia, kt, current_loop] }
        - { name: Code, properties: [code_commit, code_hosted, code_url, code_path, readme] }
  - type: table
    name: Actuators it ran on
    intro:
      text: |
        Every actuator this model is defined on, with its error on each bench (× that actuator's floor). "—" is a
        bench it did not run.
    filters: 'kind == "model"'
    order: [actuator, headline_x, dataset_x, worst_class_x, floor_tape_x, twin_grade, n_runs, code_commit]
    sort:
      - { property: headline_x, direction: ASC }
// ../robotparts-models/models.json: 623 rows, 915 KB; the first 3
{
 "id": "robotparts-models",
 "title": "Actuator models × benches (BAM rack)",
 "titleProperty": "variant",
 "source": "BAM rack analysis, /archive/datasets/bam_rig (budget Oct 23, dataset Oct 24/25, floors, model cards); naming: robotparts lane/datasheet @ ec9a13c",
 "properties": {
  "kind": {
   "type": "text",
   "width": 70
  },
  "actuator": {
   "type": "text",
   "width": 220
  },
  "can": {
   "type": "number",
   "hidden": true
  },
  "part": {
   "type": "text",
   "width": 170
  },
  "board": {
   "type": "text",
   "width": 110
  },
  "rung": {
   "type": "text",
   "width": 70
  },
  "family": {
   "type": "text",
   "width": 210
  },
  "model": {
   "type": "text",
   "width": 160
  },
  "variant": {
   "type": "text",
   "width": 190
  },
  "about": {
   "type": "text",
   "width": 300
  },
  "backend": {
   "type": "text",
   "width": 260
  },
  "ladder_step": {
   "type": "text",
   "width": 170
  },
  "ladder": {
   "type": "text",
   "hidden": true
  },
  "headline_mrad": {
   "type": "number",
   "unitText": "mrad",
   "decimals": 2,
   "width": 130,
   "values": "headline_runs",
   "better": "lower"
  },
  "headline_runs": {
   "type": "list",
   "hidden": true
  },
  "headline_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 130,
   "values": "headline_x_runs",
   "better": "lower"
  },
  "headline_x_runs": {
   "type": "list",
   "hidden": true
  },
  "dataset_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 120,
   "better": "lower",
   "derived": "median over the Oct 24 dataset / floor"
  },
  "worst_class_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 120,
   "better": "lower",
   "derived": "the worst motion class's median / floor"
  },
  "worst_class": {
   "type": "text",
   "width": 100
  },
  "floor_tape_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 120,
   "better": "lower",
   "derived": "median over the 5 floor repeats / floor"
  },
  "oct25_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 120,
   "better": "lower",
   "derived": "median over the Oct 25 held-out seeds / floor"
  },
  "floor_mrad": {
   "type": "number",
   "unitText": "mrad",
   "decimals": 3,
   "width": 100
  },
  "n_runs": {
   "type": "number",
   "unitText": "",
   "decimals": 0,
   "width": 70
  },
  "benches": {
   "type": "list",
   "width": 260
  },
  "rank": {
   "type": "number",
   "unitText": "",
   "decimals": 0,
   "width": 60
  },
  "rank_desc": {
   "type": "number",
   "hidden": true,
   "derived": "models ranked on this actuator, worst first"
  },
  "best": {
   "type": "boolean",
   "width": 60
  },
  "best_model": {
   "type": "text",
   "width": 170
  },
  "leader": {
   "type": "text",
   "width": 300
  },
  "runner_up_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 110,
   "derived": "the second-best model's headline on this actuator"
  },
  "twin_grade": {
   "type": "text",
   "width": 100,
   "display": "badge",
   "colors": {
    "yes": 3,
    "no": 8
   }
  },
  "bench": {
   "type": "text",
   "width": 190
  },
  "bench_order": {
   "type": "number",
   "hidden": true
  },
  "test": {
   "type": "text",
   "width": 110
  },
  "test_order": {
   "type": "number",
   "hidden": true
  },
  "run": {
   "type": "text",
   "width": 300
  },
  "q_rms_mrad": {
   "type": "number",
   "unitText": "mrad",
   "decimals": 2,
   "width": 110,
   "better": "lower"
  },
  "x_floor": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 100,
   "better": "lower",
   "derived": "q RMS / this actuator's floor"
  },
  "term": {
   "type": "text",
   "width": 150
  },
  "term_note": {
   "type": "text",
   "width": 360
  },
  "delta_mrad": {
   "type": "number",
   "unitText": "mrad",
   "decimals": 3,
   "width": 130,
   "values": "delta_runs"
  },
  "delta_runs": {
   "type": "list",
   "hidden": true
  },
  "delta_x": {
   "type": "number",
   "unitText": "× floor",
   "decimals": 2,
   "width": 110,
   "derived": "delta / this actuator's floor"
  },
  "description": {
   "type": "text",
   "display": "markdown",
   "width": 320
  },
  "readme": {
   "type": "text",
   "display": "markdown",
   "width": 320
  },
  "code_commit": {
   "type": "text",
   "width": 260
  },
  "code_path": {
   "type": "text",
   "width": 320
  },
  "code_hosted": {
   "type": "text",
   "width": 220
  },
  "code_url": {
   "type": "text",
   "display": "link",
   "width": 220
  },
  "page": {
   "type": "text",
   "hidden": true
  },
  "coulomb": {
   "type": "number",
   "unitText": "N·m",
   "decimals": 3,
   "width": 185
  },
  "viscous": {
   "type": "number",
   "unitText": "N·m·s/rad",
   "decimals": 4,
   "width": 185
  },
  "breakaway": {
   "type": "number",
   "unitText": "N·m",
   "decimals": 3,
   "width": 185
  },
  "stribeck_velocity": {
   "type": "number",
   "unitText": "rad/s",
   "decimals": 3,
   "width": 185
  },
  "weakening": {
   "type": "number",
   "unitText": "N·m·s/rad",
   "decimals": 4,
   "width": 185
  },
  "capture_fraction": {
   "type": "number",
   "unitText": "",
   "decimals": 2,
   "width": 185
  },
  "capture_speed": {
   "type": "number",
   "unitText": "rad/s",
   "decimals": 3,
   "width": 185
  },
  "capture_dwell": {
   "type": "number",
   "unitText": "s",
   "decimals": 3,
   "width": 185
  },
  "ripple": {
   "type": "number",
   "unitText": "N·m",
   "decimals": 4,
   "width": 185
  },
  "backlash": {
   "type": "number",
   "unitText": "arcmin",
   "decimals": 1,
   "width": 185
  },
  "gear_stiffness": {
   "type": "number",
   "unitText": "N·m/rad",
   "decimals": 0,
   "width": 185
  },
  "inertia": {
   "type": "number",
   "unitText": "kg·m²",
   "decimals": 6,
   "width": 185
  },
  "torque_scale": {
   "type": "number",
   "unitText": "",
   "decimals": 2,
   "width": 185
  },
  "kt": {
   "type": "number",
   "unitText": "N·m/A",
   "decimals": 3,
   "width": 185
  },
  "current_loop": {
   "type": "number",
   "unitText": "Hz",
   "decimals": 0,
   "width": 185
  },
  "latency": {
   "type": "number",
   "unitText": "ms",
   "decimals": 1,
   "width": 185
  },
  "resistance": {
   "type": "number",
   "unitText": "Ω",
   "decimals": 3,
   "width": 185
  },
  "inductance": {
   "type": "number",
   "unitText": "mH",
   "decimals": 3,
   "width": 185
  },
  "kv": {
   "type": "number",
   "unitText": "rpm/V",
   "decimals": 1,
   "width": 185
  },
  "winding_rth": {
   "type": "number",
   "unitText": "K/W",
   "decimals": 2,
   "width": 185
  }
 },
 "records": {
  "columns": [
   {
    "id": "bench",
    "label": "Bench"
   },
   {
    "id": "test",
    "label": "Test"
   },
   {
    "id": "run",
    "label": "Run"
   },
   {
    "id": "q_rms_mrad",
    "label": "q RMS (mrad)"
   },
   {
    "id": "x_floor",
    "label": "× floor"
   }
  ]
 },
 "rows": [
  {
   "kind": "model",
   "actuator": "CAN 14 · GIM4305-10",
   "can": 14,
   "part": "GIM4305-10",
   "board": "moteus-c1",
   "rung": "M2",
   "family": "M2 · fitted constant friction",
   "model": "M2.coulomb_viscous",
   "variant": "M2.coulomb_viscous",
   "about": "Coulomb + viscous, as identified",
   "backend": "MuJoCo native, soft friction, no driver twin",
   "ladder": null,
   "ladder_step": "1 · M2 identified",
   "headline_mrad": {
    "value": 2.101711,
    "unit": "mrad",
    "n": 19,
    "basis": "measured",
    "source": "median q RMS over the 19 Oct 23 held-out runs (budget_can14.json)"
   },
   "headline_runs": [
    2.904902,
    2.517507,
    1.356701,
    3.423471,
    3.08045,
    2.681032,
    3.142865,
    1.770424,
    2.006569,
    2.148341,
    1.629252,
    1.976596,
    2.138154,
    1.817268,
    1.980765,
    2.164142,
    1.593516,
    1.873887,
    2.101711
   ],
   "headline_x": {
    "value": 1.942787,
    "n": 19,
    "basis": "derived",
    "source": "headline / this actuator's floor, 1.082 mrad (floors.json, 5 repeats)"
   },
   "headline_x_runs": [
    2.685243,
    2.327142,
    1.254112,
    3.1646,
    2.847517,
    2.478302,
    2.905213,
    1.63655,
    1.854839,
    1.985891,
    1.506054,
    1.827132,
    1.976474,
    1.679853,
    1.830986,
    2.000497,
    1.47302,
    1.73219,
    1.942787
   ],
   "dataset_x": null,
   "worst_class_x": null,
   "worst_class": null,
   "floor_tape_x": null,
   "oct25_x": null,
   "floor_mrad": 1.081802,
   "n_runs": 19,
   "benches": [
    "Held-out tapes · Oct 23"
   ],
   "rank": 3,
   "rank_desc": 2,
   "best": false,
   "best_model": "M4.ripple.ideal",
   "leader": "CAN 14 · M4.ripple.ideal",
   "runner_up_x": 1.032416,
   "twin_grade": "no",
   "coulomb": {
    "value": 0.04133438,
    "unit": "N·m",
    "source": "model card: Coulomb friction (output) (tau = Fc*sgn(v_cmd) + b*v_cmd + tau0 on plateau rows >= 0.5 s after each speed change, block bootstrap)",
    "basis": "measured",
    "low": 0.03967717,
    "high": 0.04354373
   },
   "viscous": {
    "value": -0.00655994,
    "unit": "N·m·s/rad",
    "source": "model card: viscous friction (tau = Fc*sgn(v_cmd) + b*v_cmd + tau0 on plateau rows >= 0.5 s after each speed change, block bootstrap)",
    "basis": "estimate",
    "note": "fit refused by the model card (its 95 % CI fails the card's test)",
    "low": -0.00994945,
    "high": -0.00393923
   },
   "breakaway": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "stribeck_velocity": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "weakening": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "capture_fraction": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "capture_speed": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "capture_dwell": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "ripple": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "backlash": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "gear_stiffness": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "inertia": {
    "value": 0.00010966,
    "unit": "kg·m²",
    "source": "model card: axis inertia at the output (all moving parts + reflected rotor) (argmin over a geometric armature grid of the q RMS of the identified+ twin replaying the fixed-frequency chirps (parabola in log I); 95% CI: small-sample t over each run's own minimum)",
    "basis": "estimate",
    "note": "fit refused by the model card (its 95 % CI fails the card's test)",
    "low": -0.0000199,
    "high": 0.00023554
   },
   "torque_scale": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "kt": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "current_loop": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "latency": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "resistance": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "inductance": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "kv": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "winding_rth": {
    "value": null,
    "why": "not in M2.coulomb_viscous"
   },
   "description": "**M2.coulomb_viscous** on CAN 14 · GIM4305-10: Coulomb + viscous, as identified.\n\nContract kind `coulomb_viscous` (robotparts contract 2.3, not merged): Constant (Coulomb) plus viscous friction at the output, fitted from measurements: MuJoCo's frictionloss and damping, so its simulator is optional.\n\n```\ntau_f = coulomb_friction*sign(v) + viscous_friction*v\n```\n\nThe budget's *identified* reference twin: the identified Coulomb friction and inertia on MuJoCo's native joint friction, no driver twin.",
   "readme": "# bam_rack_twin\n\nHow far the One Dof Test Rack's digital twin is from the physical rack, station by station, with\nuncertainty. Offline only: it reads recorded evidence and the cad2robot/1 payload, and never opens a\nCAN adapter or a camera.\n\nFor every station (a moteus board bound to a CAD joint in\n`src/dev/bam_rack_twin/records/one-dof-test-rack/deployment.json`) it:\n\n1. scans the evidence directory (`<name>.npz` + `<name>.json`, any number of runs, any depth), judges\n   each run (aborted, watchdog-latched, setpoint not followed, hard fault) and converts the usable\n   ones to runtrace-v1 in the joint output frame;\n2. declares the split **before fitting**: identification runs (`gravity`, `friction`, `chirp`) train,\n   every other replayable run (today the `wiggle`s) is held out;\n3. reads the CAD's numbers from the payload with MuJoCo 3.11 (gravity amplitude and bottom, axis\n   inertia with armature, frictionloss, damping, range, ratio);\n4. identifies gravity, Coulomb + viscous friction and inertia from the training runs, each with a\n   block-bootstrap 95% CI, and **refuses** what the evidence cannot pin (ill-conditioned, one speed,\n   a CI that spans zero or ±50%, a filter-dependent inertia);\n5. replays every replayable run's logged command tape (same times, cap, gains, watchdog, NaN position\n   for velocity mode) through two single-joint MuJoCo twins behind bam_sim's moteus PID twin:\n   `cad` (the payload as exported) and `identified` (armature, frictionloss, damping, a gravity\n   scale, and two round-3 knobs: for a pendulum with an identified G and inertia, ONE equivalent\n   rigid body replaces the moving subtree's inertials (CAD mass kept unless m·r² will not fit, CAD\n   COM direction kept); for a station with a measured stop, the joint limit's solref is calibrated\n   by a static press to the measured stiffness and the unchanged homing is re-run on it);\n6. scores real − twin for q and τ (RMS, max, CI) and writes the scorecard and a traj_compare report.\n\n`--evidence` takes one or more roots; with several, every run name starts with its root's\ndirectory name. A station whose `cad_joint` is null (its joint is not in the CAD export; CAN 14's\nGIM43 plate since the 2026-10-20 camera map) has no CAD twin: its identified twin is a bare hinge\n(identified Coulomb, the identified inertia or its CI upper bound as armature, no gravity), and any\n`superseded_mappings` it carries are still replayed and listed, never in a headline.\n\n**Camera ground truth** (`bam_rack_twin.camera`, the `camera` extra, OpenCV pinned as bam_aruco\npins it): every RealSense recording in the evidence (`frames` + `wall` npz) is matched to the runs it\noverlaps. It produces a mapping matrix of every tag × every run (in-plane angle vs encoder: r, slope),\nand, for a tag that moves with its run, the camera/encoder ratio about the tag's own axis\n(solvePnP IPPE_SQUARE) with a block-bootstrap CI, the residual, and the camera latency by\ncross-correlation. It refuses too few frames, a small tag or a short sweep.\n\nA new station is a new component in the deployment record\n(`records/one-dof-test-rack/deployment.json`); new runs are picked up by scanning. A replayable run\nrecorded on firmware the board no longer runs is **superseded**: replayed and listed, never in the\nheld-out score.\n\nAlso measured, never replayed:\n\n- **Hardstops** (`kind: hardstop`, stop.py): datum repeatability (σ with a chi-square CI, spread, n)\n  and stop stiffness (press-step slope, t-interval) in the board frame and the joint output frame,\n  against bam_homing's 400 N·m/rev floor and 1 mrad repeat. The CAD twin's side runs the UNCHANGED\n  `HomingController` on the payload's stop (`homing_sim.py`, in bam_homing's own uv environment,\n  because the two packages pin bam-robot-descriptions differently). The unloaded stop is the\n  station's frame witness.\n- **Delays**: command → measurement per run (normalised cross-correlation of commanded and measured\n  velocity), and the torque/acceleration alignment the inertia fit estimates per chirp (|d| ≤ 25 ms,\n  re-fitted in every bootstrap resample). Evidence is never shifted.\n\n## Run it\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra camera python -m bam_rack_twin scorecard --evidence /archive/datasets/bam_rig/rack_2026-10-18 /archive/datasets/bam_rig/rack_2026-10-20 --bundle ~/.cache/cad2robot-build/bundles/a7b1095/fixtures/cad2robot/one_dof_test_rack --out ~/.cache/rack-twin-build --report ~/bam_ws/reports/media/rack-twin-2026-10-18/report.html\n```\n\n`--bundle` may come from `$BAM_RACK_PAYLOAD` instead (the payload's 39 MB of STL never enters git).\nOutputs under `--out`:\n\n| path | what |\n|---|---|\n| `split.json` | the declared split, with bam_deployments' split digest |\n| `identified/<station>.json` | every estimate (value, CI, method, refusal reason), the identified twin, the frame map, provenance (runs + sha256, code version) |\n| `scorecard.json` | parameter and trajectory tables per station, the evidence ledger |\n| `runs/one-dof-test-rack__<evidence>/` | real runs, runtrace-v1 (+ mode/fault/voltage) |\n| `runs/one-dof-test-rack__twin-{cad,identified}__<evidence>/` | twin replays, `meta.replay_of` / `twin_version` |\n| `twins/<station>__{cad,identified}.xml` | the exact scenes replayed |\n| `report.html` | traj_compare: one group per station, Summary, Insights |\n\n## Acquisition (moves real motors)\n\n`bam_rack_twin.hardware` is the guarded acquisition the evidence comes from (ported from\n`~/.cache/rack-commissioning`): bus flock, port-holder refusal, preflight, countdown, 0.25 s\nwatchdog, mode-11 abort, abort only on mode 1 or fault codes 32–50 (limiter 96–105 is not a fault),\n10-sample-mean overspeed, GC paused, STOP in `finally`. It needs the `hardware` extra:\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra hardware python -m bam_rack_twin.hardware sine --can-id 15 --cap-nm 1.0 --amplitude-rad 0.05 --freq-hz 0.5 --out /archive/datasets/bam_rig/rack_2026-10-18/round2 --name can15_sine\n```\n\nKinds: `gravity`, `swing`, `friction`, `chirp`, `sine`, `hardstop`, `tape`, `inventory` (read-only),\n`record` (a RealSense colour recording, `--extra record`; it refuses, and never takes the camera,\nwhile another process holds a RealSense node or a realsense-viewer runs).\n\n`tape` plays payload_compare's frozen out-and-back raised cosines (slow/medium/fast legs 4/2/1 s,\n0.5 s hold), one evidence run per speed, from the editable `TAPE_STATIONS` block in\n`bam_rack_twin/hardware/tape.py` (amplitude ceiling, side, cap per board; CAN 1 goes to its free\nside only and aborts if pushed toward its stop). The tapes and their sha256 are written before the\nfirst command. The scorecard grades tape runs as held-out tracking.\n\n## Frames\n\n- Evidence is in the board's frame. `frame.output_per_board` in the record turns it into joint\n  output radians (torque by the inverse, gains by the inverse square). CAN 1 is unconfigured, so\n  its evidence is rotor-frame and × 0.1 is an **assumption**, flagged everywhere.\n- The payload frame is the CAD joint coordinate: `q_payload = q_output + offset`. A gravity station\n  takes the offset from its gravity bottom (the frame witness); a ranged joint without a witness is\n  placed mid-range; a continuous joint without gravity needs none.\n\n## Tests\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra test pytest -q -p no:cacheprovider\n```\n\nSynthetic evidence is recorded off a twin with known physics (the cad2robot_payload synthetic\nfixture's pendulum at 0.6 × its CAD gravity, 0.03 N·m Coulomb); identification must recover it inside\nits CI and the identified twin must beat the CAD twin on the held-out run. `tests/fixtures/` holds\nthree real CAN 15 runs (184 KB) for the run-judgement checks.\n",
   "code_commit": "b0ad72b5cd + uncommitted changes (source sha256 e0c3b227ec95e1a1)",
   "code_path": "~/vt-lanes/rig-bam_ws/src/dev/bam_rack_twin",
   "code_hosted": "local only: the commit is not on GitHub's origin",
   "code_url": null,
   "page": "../robotparts-model/index.html?variant=M2.coulomb_viscous",
   "id": "m0000"
  },
  {
   "kind": "model",
   "actuator": "CAN 14 · GIM4305-10",
   "can": 14,
   "part": "GIM4305-10",
   "board": "moteus-c1",
   "rung": "M4",
   "family": "M4 · grey-box drivetrain",
   "model": "M4.ripple",
   "variant": "M4.ripple.c1",
   "about": "Coulomb + weakening + cogging map + backlash, with the moteus-c1 driver twin",
   "backend": "MuJoCo + plugin + moteus-c1 driver twin",
   "ladder": null,
   "ladder_step": "4 · full + driver twin",
   "headline_mrad": {
    "value": 1.11687,
    "unit": "mrad",
    "n": 19,
    "basis": "measured",
    "source": "median q RMS over the 19 Oct 23 held-out runs (budget_can14.json)"
   },
   "headline_runs": [
    1.268209,
    1.269012,
    2.044472,
    1.240039,
    1.064797,
    1.178394,
    1.612492,
    1.130757,
    1.103063,
    1.11687,
    1.096979,
    1.083231,
    1.125344,
    1.176362,
    0.999837,
    1.09128,
    1.061484,
    1.051552,
    1.107015
   ],
   "headline_x": {
    "value": 1.032416,
    "n": 19,
    "basis": "derived",
    "source": "headline / this actuator's floor, 1.082 mrad (floors.json, 5 repeats)"
   },
   "headline_x_runs": [
    1.172311,
    1.173054,
    1.889876,
    1.146272,
    0.984281,
    1.089288,
    1.490561,
    1.045253,
    1.019653,
    1.032416,
    1.014029,
    1.001321,
    1.040249,
    1.08741,
    0.924233,
    1.008761,
    0.981218,
    0.972037,
    1.023307
   ],
   "dataset_x": 4.239043,
   "worst_class_x": 8.239367,
   "worst_class": "cycle",
   "floor_tape_x": 3.602484,
   "oct25_x": null,
   "floor_mrad": 1.081802,
   "n_runs": 36,
   "benches": [
    "Held-out tapes · Oct 23",
    "Dataset · Oct 24",
    "Floor repeats · Oct 24"
   ],
   "rank": 2,
   "rank_desc": 3,
   "best": false,
   "best_model": "M4.ripple.ideal",
   "leader": "CAN 14 · M4.ripple.ideal",
   "runner_up_x": 1.032416,
   "twin_grade": "no",
   "coulomb": {
    "value": 0.04133438,
    "unit": "N·m",
    "source": "model card: Coulomb friction (output) (tau = Fc*sgn(v_cmd) + b*v_cmd + tau0 on plateau rows >= 0.5 s after each speed change, block bootstrap)",
    "basis": "measured",
    "low": 0.03967717,
    "high": 0.04354373
   },
   "viscous": {
    "value": -0.00655994,
    "unit": "N·m·s/rad",
    "source": "model card: viscous friction (tau = Fc*sgn(v_cmd) + b*v_cmd + tau0 on plateau rows >= 0.5 s after each speed change, block bootstrap)",
    "basis": "estimate",
    "note": "fit refused by the model card (its 95 % CI fails the card's test)",
    "low": -0.00994945,
    "high": -0.00393923
   },
   "breakaway": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "stribeck_velocity": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "weakening": {
    "value": 0.00655994,
    "unit": "N·m·s/rad",
    "source": "model card: velocity weakening (Stribeck-like) (minus the plateaus' viscous slope b (tau = Fc*sgn(v) + b*v + tau0), applied up to the fastest plateau speed)",
    "basis": "measured",
    "low": 0.00393923,
    "high": 0.00994945
   },
   "capture_fraction": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "capture_speed": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "capture_dwell": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "ripple": {
    "value": 0.01862969,
    "unit": "N·m",
    "source": "model card: torque ripple + cogging (rotor-angle map, RMS) (tau = c_plateau + sum_k a_k cos(k*10*q) + b_k sin, k = 1..32 per rotor revolution, on settled plateaus with k_max*r*|v|/2pi <= 0.7 x the reply Nyquist rate; block bootstrap of the map RMS; tiers [64, 128] only if every leave-one-run-out fold gains >= 0.02 R2)",
    "basis": "measured",
    "low": 0.01796942,
    "high": 0.01926719
   },
   "backlash": {
    "value": 12.21336486,
    "unit": "arcmin",
    "source": "model card: gear backlash (dead band) (camera tag (output) vs encoder (rotor / N) at settled staircase holds, differenced per level (up vs down))",
    "basis": "measured",
    "low": 8.13125715,
    "high": 15.91993247
   },
   "gear_stiffness": {
    "value": null,
    "why": "not measured yet"
   },
   "inertia": {
    "value": 0.00010966,
    "unit": "kg·m²",
    "source": "model card: axis inertia at the output (all moving parts + reflected rotor) (argmin over a geometric armature grid of the q RMS of the identified+ twin replaying the fixed-frequency chirps (parabola in log I); 95% CI: small-sample t over each run's own minimum)",
    "basis": "estimate",
    "note": "fit refused by the model card (its 95 % CI fails the card's test)",
    "low": -0.0000199,
    "high": 0.00023554
   },
   "torque_scale": {
    "value": 1.36,
    "basis": "measured",
    "source": "budget: full_twin.torque_scale (independent Kt check)"
   },
   "kt": {
    "value": 0.50872801,
    "unit": "N·m/A",
    "source": "model card: Kt the board uses (rotor / output), the board's calibration (moteus: 8.27 / Kv x i_q; output x the gear ratio)",
    "basis": "measured"
   },
   "current_loop": {
    "value": 100,
    "unit": "Hz",
    "basis": "chosen",
    "source": "the full twin's current loop: the board's servo.pid_dq_hz (budget)"
   },
   "latency": {
    "value": 14.01323696,
    "unit": "ms",
    "source": "model card: command -> motion delay (closed loop) (median over runs of the cross-correlation lag qd_target -> qd)",
    "basis": "measured",
    "low": 7.53553958,
    "high": 27.38150994
   },
   "resistance": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "inductance": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "kv": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "winding_rth": {
    "value": null,
    "why": "not in M4.ripple.c1"
   },
   "description": "**M4.ripple.c1** on CAN 14 · GIM4305-10: Coulomb + weakening + cogging map + backlash, with the moteus-c1 driver twin.\n\nContract kind `ripple_map` (robotparts contract 2.3, not merged): Cogging and commutation ripple as rotor-periodic torque harmonics at the output (the torque_ripple records in one map), optionally with Coulomb and viscous friction.\n\n```\ntau_ripple = sum_k amplitude_k*sin(rotor_harmonic_k*theta_rotor + phase_k), theta_rotor = gear_ratio*theta_output in rad from the rotor zero the simulator uses\n```\n\nThe full twin with the moteus-c1 driver twin (its current loop), as the budget ran it.\n\nWhat this twin runs (`budget_can14.json`, full_twin.sources):\n\n- **model**: bare single hinge: the station's joint is not in the CAD export\n- **armature**: UPPER BOUND of the refused fit's 95% CI, 0.0003008 kg.m^2 (ill-posed: 95% CI [-6.39e-05, 0.000301] includes zero; filter windows (13, 21, 31) move it by 9.24e-05, more than 25% of it); replay-error inertia refused (95% CI [-1.99e-05, 0.000236] includes zero)\n- **frictionloss**: identified Coulomb\n- **damping**: none: viscous refused (negative viscous -0.00656 (CI excludes zero): Stribeck-like, not a damping a twin can run)\n- **gravity**: none (no CAD mass, none identified)\n- **friction_constraint**: solreffriction 0.4 ms / 1, solimpfriction d 0.99: the frictionloss sticks (MuJoCo's soft default creeps at ~0.4 rad/s on a 1e-4 kg.m^2 hinge)\n- **torque_ripple**: plateau ripple map, k 1..96 per rotor rev (r 10), RMS 18.6 mN.m [18.0, 19.3], cross-validated R2 0.54, 0.26, 0.33, 0.43, 0.51\n- **friction_weakening**: 0.00656 N.m.s/rad [0.00394, 0.00995] up to 1 rad/s (the plateaus' negative viscous slope)\n- **budget**: full twin, board torque units",
   "readme": "# bam_rack_twin\n\nHow far the One Dof Test Rack's digital twin is from the physical rack, station by station, with\nuncertainty. Offline only: it reads recorded evidence and the cad2robot/1 payload, and never opens a\nCAN adapter or a camera.\n\nFor every station (a moteus board bound to a CAD joint in\n`src/dev/bam_rack_twin/records/one-dof-test-rack/deployment.json`) it:\n\n1. scans the evidence directory (`<name>.npz` + `<name>.json`, any number of runs, any depth), judges\n   each run (aborted, watchdog-latched, setpoint not followed, hard fault) and converts the usable\n   ones to runtrace-v1 in the joint output frame;\n2. declares the split **before fitting**: identification runs (`gravity`, `friction`, `chirp`) train,\n   every other replayable run (today the `wiggle`s) is held out;\n3. reads the CAD's numbers from the payload with MuJoCo 3.11 (gravity amplitude and bottom, axis\n   inertia with armature, frictionloss, damping, range, ratio);\n4. identifies gravity, Coulomb + viscous friction and inertia from the training runs, each with a\n   block-bootstrap 95% CI, and **refuses** what the evidence cannot pin (ill-conditioned, one speed,\n   a CI that spans zero or ±50%, a filter-dependent inertia);\n5. replays every replayable run's logged command tape (same times, cap, gains, watchdog, NaN position\n   for velocity mode) through two single-joint MuJoCo twins behind bam_sim's moteus PID twin:\n   `cad` (the payload as exported) and `identified` (armature, frictionloss, damping, a gravity\n   scale, and two round-3 knobs: for a pendulum with an identified G and inertia, ONE equivalent\n   rigid body replaces the moving subtree's inertials (CAD mass kept unless m·r² will not fit, CAD\n   COM direction kept); for a station with a measured stop, the joint limit's solref is calibrated\n   by a static press to the measured stiffness and the unchanged homing is re-run on it);\n6. scores real − twin for q and τ (RMS, max, CI) and writes the scorecard and a traj_compare report.\n\n`--evidence` takes one or more roots; with several, every run name starts with its root's\ndirectory name. A station whose `cad_joint` is null (its joint is not in the CAD export; CAN 14's\nGIM43 plate since the 2026-10-20 camera map) has no CAD twin: its identified twin is a bare hinge\n(identified Coulomb, the identified inertia or its CI upper bound as armature, no gravity), and any\n`superseded_mappings` it carries are still replayed and listed, never in a headline.\n\n**Camera ground truth** (`bam_rack_twin.camera`, the `camera` extra, OpenCV pinned as bam_aruco\npins it): every RealSense recording in the evidence (`frames` + `wall` npz) is matched to the runs it\noverlaps. It produces a mapping matrix of every tag × every run (in-plane angle vs encoder: r, slope),\nand, for a tag that moves with its run, the camera/encoder ratio about the tag's own axis\n(solvePnP IPPE_SQUARE) with a block-bootstrap CI, the residual, and the camera latency by\ncross-correlation. It refuses too few frames, a small tag or a short sweep.\n\nA new station is a new component in the deployment record\n(`records/one-dof-test-rack/deployment.json`); new runs are picked up by scanning. A replayable run\nrecorded on firmware the board no longer runs is **superseded**: replayed and listed, never in the\nheld-out score.\n\nAlso measured, never replayed:\n\n- **Hardstops** (`kind: hardstop`, stop.py): datum repeatability (σ with a chi-square CI, spread, n)\n  and stop stiffness (press-step slope, t-interval) in the board frame and the joint output frame,\n  against bam_homing's 400 N·m/rev floor and 1 mrad repeat. The CAD twin's side runs the UNCHANGED\n  `HomingController` on the payload's stop (`homing_sim.py`, in bam_homing's own uv environment,\n  because the two packages pin bam-robot-descriptions differently). The unloaded stop is the\n  station's frame witness.\n- **Delays**: command → measurement per run (normalised cross-correlation of commanded and measured\n  velocity), and the torque/acceleration alignment the inertia fit estimates per chirp (|d| ≤ 25 ms,\n  re-fitted in every bootstrap resample). Evidence is never shifted.\n\n## Run it\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra camera python -m bam_rack_twin scorecard --evidence /archive/datasets/bam_rig/rack_2026-10-18 /archive/datasets/bam_rig/rack_2026-10-20 --bundle ~/.cache/cad2robot-build/bundles/a7b1095/fixtures/cad2robot/one_dof_test_rack --out ~/.cache/rack-twin-build --report ~/bam_ws/reports/media/rack-twin-2026-10-18/report.html\n```\n\n`--bundle` may come from `$BAM_RACK_PAYLOAD` instead (the payload's 39 MB of STL never enters git).\nOutputs under `--out`:\n\n| path | what |\n|---|---|\n| `split.json` | the declared split, with bam_deployments' split digest |\n| `identified/<station>.json` | every estimate (value, CI, method, refusal reason), the identified twin, the frame map, provenance (runs + sha256, code version) |\n| `scorecard.json` | parameter and trajectory tables per station, the evidence ledger |\n| `runs/one-dof-test-rack__<evidence>/` | real runs, runtrace-v1 (+ mode/fault/voltage) |\n| `runs/one-dof-test-rack__twin-{cad,identified}__<evidence>/` | twin replays, `meta.replay_of` / `twin_version` |\n| `twins/<station>__{cad,identified}.xml` | the exact scenes replayed |\n| `report.html` | traj_compare: one group per station, Summary, Insights |\n\n## Acquisition (moves real motors)\n\n`bam_rack_twin.hardware` is the guarded acquisition the evidence comes from (ported from\n`~/.cache/rack-commissioning`): bus flock, port-holder refusal, preflight, countdown, 0.25 s\nwatchdog, mode-11 abort, abort only on mode 1 or fault codes 32–50 (limiter 96–105 is not a fault),\n10-sample-mean overspeed, GC paused, STOP in `finally`. It needs the `hardware` extra:\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra hardware python -m bam_rack_twin.hardware sine --can-id 15 --cap-nm 1.0 --amplitude-rad 0.05 --freq-hz 0.5 --out /archive/datasets/bam_rig/rack_2026-10-18/round2 --name can15_sine\n```\n\nKinds: `gravity`, `swing`, `friction`, `chirp`, `sine`, `hardstop`, `tape`, `inventory` (read-only),\n`record` (a RealSense colour recording, `--extra record`; it refuses, and never takes the camera,\nwhile another process holds a RealSense node or a realsense-viewer runs).\n\n`tape` plays payload_compare's frozen out-and-back raised cosines (slow/medium/fast legs 4/2/1 s,\n0.5 s hold), one evidence run per speed, from the editable `TAPE_STATIONS` block in\n`bam_rack_twin/hardware/tape.py` (amplitude ceiling, side, cap per board; CAN 1 goes to its free\nside only and aborts if pushed toward its stop). The tapes and their sha256 are written before the\nfirst command. The scorecard grades tape runs as held-out tracking.\n\n## Frames\n\n- Evidence is in the board's frame. `frame.output_per_board` in the record turns it into joint\n  output radians (torque by the inverse, gains by the inverse square). CAN 1 is unconfigured, so\n  its evidence is rotor-frame and × 0.1 is an **assumption**, flagged everywhere.\n- The payload frame is the CAD joint coordinate: `q_payload = q_output + offset`. A gravity station\n  takes the offset from its gravity bottom (the frame witness); a ranged joint without a witness is\n  placed mid-range; a continuous joint without gravity needs none.\n\n## Tests\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra test pytest -q -p no:cacheprovider\n```\n\nSynthetic evidence is recorded off a twin with known physics (the cad2robot_payload synthetic\nfixture's pendulum at 0.6 × its CAD gravity, 0.03 N·m Coulomb); identification must recover it inside\nits CI and the identified twin must beat the CAD twin on the held-out run. `tests/fixtures/` holds\nthree real CAN 15 runs (184 KB) for the run-judgement checks.\n",
   "code_commit": "b0ad72b5cd + uncommitted changes (source sha256 e0c3b227ec95e1a1)",
   "code_path": "~/vt-lanes/rig-bam_ws/src/dev/bam_rack_twin",
   "code_hosted": "local only: the commit is not on GitHub's origin",
   "code_url": null,
   "page": "../robotparts-model/index.html?variant=M4.ripple.c1",
   "id": "m0001"
  },
  {
   "kind": "model",
   "actuator": "CAN 14 · GIM4305-10",
   "can": 14,
   "part": "GIM4305-10",
   "board": "moteus-c1",
   "rung": "M4",
   "family": "M4 · grey-box drivetrain",
   "model": "M4.ripple",
   "variant": "M4.ripple.ideal",
   "about": "Coulomb + weakening + cogging map + backlash, ideal driver (current loop off)",
   "backend": "MuJoCo + plugin, ideal driver",
   "ladder": null,
   "ladder_step": "3 · full, ideal driver",
   "headline_mrad": {
    "value": 1.101199,
    "unit": "mrad",
    "n": 19,
    "basis": "measured",
    "source": "median q RMS over the 19 Oct 23 held-out runs (budget_can14.json)"
   },
   "headline_runs": [
    1.282905,
    1.284585,
    1.809055,
    1.115638,
    1.021629,
    1.182252,
    1.620853,
    1.03897,
    1.065223,
    1.129556,
    1.002165,
    1.041609,
    1.137089,
    1.066692,
    0.970864,
    1.101199,
    0.957311,
    1.021648,
    1.113429
   ],
   "headline_x": {
    "value": 1.01793,
    "n": 19,
    "basis": "derived",
    "source": "headline / this actuator's floor, 1.082 mrad (floors.json, 5 repeats)"
   },
   "headline_x_runs": [
    1.185896,
    1.187449,
    1.672261,
    1.031277,
    0.944377,
    1.092854,
    1.49829,
    0.960407,
    0.984674,
    1.044143,
    0.926385,
    0.962846,
    1.051106,
    0.986033,
    0.897451,
    1.01793,
    0.884923,
    0.944395,
    1.029236
   ],
   "dataset_x": null,
   "worst_class_x": null,
   "worst_class": null,
   "floor_tape_x": null,
   "oct25_x": null,
   "floor_mrad": 1.081802,
   "n_runs": 19,
   "benches": [
    "Held-out tapes · Oct 23"
   ],
   "rank": 1,
   "rank_desc": 4,
   "best": true,
   "best_model": "M4.ripple.ideal",
   "leader": "CAN 14 · M4.ripple.ideal",
   "runner_up_x": 1.032416,
   "twin_grade": {
    "value": null,
    "why": "not run on the motion classes (Oct 24)"
   },
   "coulomb": {
    "value": 0.04133438,
    "unit": "N·m",
    "source": "model card: Coulomb friction (output) (tau = Fc*sgn(v_cmd) + b*v_cmd + tau0 on plateau rows >= 0.5 s after each speed change, block bootstrap)",
    "basis": "measured",
    "low": 0.03967717,
    "high": 0.04354373
   },
   "viscous": {
    "value": -0.00655994,
    "unit": "N·m·s/rad",
    "source": "model card: viscous friction (tau = Fc*sgn(v_cmd) + b*v_cmd + tau0 on plateau rows >= 0.5 s after each speed change, block bootstrap)",
    "basis": "estimate",
    "note": "fit refused by the model card (its 95 % CI fails the card's test)",
    "low": -0.00994945,
    "high": -0.00393923
   },
   "breakaway": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "stribeck_velocity": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "weakening": {
    "value": 0.00655994,
    "unit": "N·m·s/rad",
    "source": "model card: velocity weakening (Stribeck-like) (minus the plateaus' viscous slope b (tau = Fc*sgn(v) + b*v + tau0), applied up to the fastest plateau speed)",
    "basis": "measured",
    "low": 0.00393923,
    "high": 0.00994945
   },
   "capture_fraction": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "capture_speed": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "capture_dwell": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "ripple": {
    "value": 0.01862969,
    "unit": "N·m",
    "source": "model card: torque ripple + cogging (rotor-angle map, RMS) (tau = c_plateau + sum_k a_k cos(k*10*q) + b_k sin, k = 1..32 per rotor revolution, on settled plateaus with k_max*r*|v|/2pi <= 0.7 x the reply Nyquist rate; block bootstrap of the map RMS; tiers [64, 128] only if every leave-one-run-out fold gains >= 0.02 R2)",
    "basis": "measured",
    "low": 0.01796942,
    "high": 0.01926719
   },
   "backlash": {
    "value": 12.21336486,
    "unit": "arcmin",
    "source": "model card: gear backlash (dead band) (camera tag (output) vs encoder (rotor / N) at settled staircase holds, differenced per level (up vs down))",
    "basis": "measured",
    "low": 8.13125715,
    "high": 15.91993247
   },
   "gear_stiffness": {
    "value": null,
    "why": "not measured yet"
   },
   "inertia": {
    "value": 0.00010966,
    "unit": "kg·m²",
    "source": "model card: axis inertia at the output (all moving parts + reflected rotor) (argmin over a geometric armature grid of the q RMS of the identified+ twin replaying the fixed-frequency chirps (parabola in log I); 95% CI: small-sample t over each run's own minimum)",
    "basis": "estimate",
    "note": "fit refused by the model card (its 95 % CI fails the card's test)",
    "low": -0.0000199,
    "high": 0.00023554
   },
   "torque_scale": {
    "value": 1.36,
    "basis": "measured",
    "source": "budget: full_twin.torque_scale (independent Kt check)"
   },
   "kt": {
    "value": 0.50872801,
    "unit": "N·m/A",
    "source": "model card: Kt the board uses (rotor / output), the board's calibration (moteus: 8.27 / Kv x i_q; output x the gear ratio)",
    "basis": "measured"
   },
   "current_loop": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "latency": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "resistance": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "inductance": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "kv": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "winding_rth": {
    "value": null,
    "why": "not in M4.ripple.ideal"
   },
   "description": "**M4.ripple.ideal** on CAN 14 · GIM4305-10: Coulomb + weakening + cogging map + backlash, ideal driver (current loop off).\n\nContract kind `ripple_map` (robotparts contract 2.3, not merged): Cogging and commutation ripple as rotor-periodic torque harmonics at the output (the torque_ripple records in one map), optionally with Coulomb and viscous friction.\n\n```\ntau_ripple = sum_k amplitude_k*sin(rotor_harmonic_k*theta_rotor + phase_k), theta_rotor = gear_ratio*theta_output in rad from the rotor zero the simulator uses\n```\n\nThe same twin with the driver's current loop switched off (an ideal driver): the budget's `current loop` ablation, run by run.\n\nWhat this twin runs (`budget_can14.json`, full_twin.sources):\n\n- **model**: bare single hinge: the station's joint is not in the CAD export\n- **armature**: UPPER BOUND of the refused fit's 95% CI, 0.0003008 kg.m^2 (ill-posed: 95% CI [-6.39e-05, 0.000301] includes zero; filter windows (13, 21, 31) move it by 9.24e-05, more than 25% of it); replay-error inertia refused (95% CI [-1.99e-05, 0.000236] includes zero)\n- **frictionloss**: identified Coulomb\n- **damping**: none: viscous refused (negative viscous -0.00656 (CI excludes zero): Stribeck-like, not a damping a twin can run)\n- **gravity**: none (no CAD mass, none identified)\n- **friction_constraint**: solreffriction 0.4 ms / 1, solimpfriction d 0.99: the frictionloss sticks (MuJoCo's soft default creeps at ~0.4 rad/s on a 1e-4 kg.m^2 hinge)\n- **torque_ripple**: plateau ripple map, k 1..96 per rotor rev (r 10), RMS 18.6 mN.m [18.0, 19.3], cross-validated R2 0.54, 0.26, 0.33, 0.43, 0.51\n- **friction_weakening**: 0.00656 N.m.s/rad [0.00394, 0.00995] up to 1 rad/s (the plateaus' negative viscous slope)\n- **budget**: full twin, board torque units",
   "readme": "# bam_rack_twin\n\nHow far the One Dof Test Rack's digital twin is from the physical rack, station by station, with\nuncertainty. Offline only: it reads recorded evidence and the cad2robot/1 payload, and never opens a\nCAN adapter or a camera.\n\nFor every station (a moteus board bound to a CAD joint in\n`src/dev/bam_rack_twin/records/one-dof-test-rack/deployment.json`) it:\n\n1. scans the evidence directory (`<name>.npz` + `<name>.json`, any number of runs, any depth), judges\n   each run (aborted, watchdog-latched, setpoint not followed, hard fault) and converts the usable\n   ones to runtrace-v1 in the joint output frame;\n2. declares the split **before fitting**: identification runs (`gravity`, `friction`, `chirp`) train,\n   every other replayable run (today the `wiggle`s) is held out;\n3. reads the CAD's numbers from the payload with MuJoCo 3.11 (gravity amplitude and bottom, axis\n   inertia with armature, frictionloss, damping, range, ratio);\n4. identifies gravity, Coulomb + viscous friction and inertia from the training runs, each with a\n   block-bootstrap 95% CI, and **refuses** what the evidence cannot pin (ill-conditioned, one speed,\n   a CI that spans zero or ±50%, a filter-dependent inertia);\n5. replays every replayable run's logged command tape (same times, cap, gains, watchdog, NaN position\n   for velocity mode) through two single-joint MuJoCo twins behind bam_sim's moteus PID twin:\n   `cad` (the payload as exported) and `identified` (armature, frictionloss, damping, a gravity\n   scale, and two round-3 knobs: for a pendulum with an identified G and inertia, ONE equivalent\n   rigid body replaces the moving subtree's inertials (CAD mass kept unless m·r² will not fit, CAD\n   COM direction kept); for a station with a measured stop, the joint limit's solref is calibrated\n   by a static press to the measured stiffness and the unchanged homing is re-run on it);\n6. scores real − twin for q and τ (RMS, max, CI) and writes the scorecard and a traj_compare report.\n\n`--evidence` takes one or more roots; with several, every run name starts with its root's\ndirectory name. A station whose `cad_joint` is null (its joint is not in the CAD export; CAN 14's\nGIM43 plate since the 2026-10-20 camera map) has no CAD twin: its identified twin is a bare hinge\n(identified Coulomb, the identified inertia or its CI upper bound as armature, no gravity), and any\n`superseded_mappings` it carries are still replayed and listed, never in a headline.\n\n**Camera ground truth** (`bam_rack_twin.camera`, the `camera` extra, OpenCV pinned as bam_aruco\npins it): every RealSense recording in the evidence (`frames` + `wall` npz) is matched to the runs it\noverlaps. It produces a mapping matrix of every tag × every run (in-plane angle vs encoder: r, slope),\nand, for a tag that moves with its run, the camera/encoder ratio about the tag's own axis\n(solvePnP IPPE_SQUARE) with a block-bootstrap CI, the residual, and the camera latency by\ncross-correlation. It refuses too few frames, a small tag or a short sweep.\n\nA new station is a new component in the deployment record\n(`records/one-dof-test-rack/deployment.json`); new runs are picked up by scanning. A replayable run\nrecorded on firmware the board no longer runs is **superseded**: replayed and listed, never in the\nheld-out score.\n\nAlso measured, never replayed:\n\n- **Hardstops** (`kind: hardstop`, stop.py): datum repeatability (σ with a chi-square CI, spread, n)\n  and stop stiffness (press-step slope, t-interval) in the board frame and the joint output frame,\n  against bam_homing's 400 N·m/rev floor and 1 mrad repeat. The CAD twin's side runs the UNCHANGED\n  `HomingController` on the payload's stop (`homing_sim.py`, in bam_homing's own uv environment,\n  because the two packages pin bam-robot-descriptions differently). The unloaded stop is the\n  station's frame witness.\n- **Delays**: command → measurement per run (normalised cross-correlation of commanded and measured\n  velocity), and the torque/acceleration alignment the inertia fit estimates per chirp (|d| ≤ 25 ms,\n  re-fitted in every bootstrap resample). Evidence is never shifted.\n\n## Run it\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra camera python -m bam_rack_twin scorecard --evidence /archive/datasets/bam_rig/rack_2026-10-18 /archive/datasets/bam_rig/rack_2026-10-20 --bundle ~/.cache/cad2robot-build/bundles/a7b1095/fixtures/cad2robot/one_dof_test_rack --out ~/.cache/rack-twin-build --report ~/bam_ws/reports/media/rack-twin-2026-10-18/report.html\n```\n\n`--bundle` may come from `$BAM_RACK_PAYLOAD` instead (the payload's 39 MB of STL never enters git).\nOutputs under `--out`:\n\n| path | what |\n|---|---|\n| `split.json` | the declared split, with bam_deployments' split digest |\n| `identified/<station>.json` | every estimate (value, CI, method, refusal reason), the identified twin, the frame map, provenance (runs + sha256, code version) |\n| `scorecard.json` | parameter and trajectory tables per station, the evidence ledger |\n| `runs/one-dof-test-rack__<evidence>/` | real runs, runtrace-v1 (+ mode/fault/voltage) |\n| `runs/one-dof-test-rack__twin-{cad,identified}__<evidence>/` | twin replays, `meta.replay_of` / `twin_version` |\n| `twins/<station>__{cad,identified}.xml` | the exact scenes replayed |\n| `report.html` | traj_compare: one group per station, Summary, Insights |\n\n## Acquisition (moves real motors)\n\n`bam_rack_twin.hardware` is the guarded acquisition the evidence comes from (ported from\n`~/.cache/rack-commissioning`): bus flock, port-holder refusal, preflight, countdown, 0.25 s\nwatchdog, mode-11 abort, abort only on mode 1 or fault codes 32–50 (limiter 96–105 is not a fault),\n10-sample-mean overspeed, GC paused, STOP in `finally`. It needs the `hardware` extra:\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra hardware python -m bam_rack_twin.hardware sine --can-id 15 --cap-nm 1.0 --amplitude-rad 0.05 --freq-hz 0.5 --out /archive/datasets/bam_rig/rack_2026-10-18/round2 --name can15_sine\n```\n\nKinds: `gravity`, `swing`, `friction`, `chirp`, `sine`, `hardstop`, `tape`, `inventory` (read-only),\n`record` (a RealSense colour recording, `--extra record`; it refuses, and never takes the camera,\nwhile another process holds a RealSense node or a realsense-viewer runs).\n\n`tape` plays payload_compare's frozen out-and-back raised cosines (slow/medium/fast legs 4/2/1 s,\n0.5 s hold), one evidence run per speed, from the editable `TAPE_STATIONS` block in\n`bam_rack_twin/hardware/tape.py` (amplitude ceiling, side, cap per board; CAN 1 goes to its free\nside only and aborts if pushed toward its stop). The tapes and their sha256 are written before the\nfirst command. The scorecard grades tape runs as held-out tracking.\n\n## Frames\n\n- Evidence is in the board's frame. `frame.output_per_board` in the record turns it into joint\n  output radians (torque by the inverse, gains by the inverse square). CAN 1 is unconfigured, so\n  its evidence is rotor-frame and × 0.1 is an **assumption**, flagged everywhere.\n- The payload frame is the CAD joint coordinate: `q_payload = q_output + offset`. A gravity station\n  takes the offset from its gravity bottom (the frame witness); a ranged joint without a witness is\n  placed mid-range; a continuous joint without gravity needs none.\n\n## Tests\n\n```bash\ncd ~/bam_ws/src/dev/bam_rack_twin\nenv -u VIRTUAL_ENV uv run --extra test pytest -q -p no:cacheprovider\n```\n\nSynthetic evidence is recorded off a twin with known physics (the cad2robot_payload synthetic\nfixture's pendulum at 0.6 × its CAD gravity, 0.03 N·m Coulomb); identification must recover it inside\nits CI and the identified twin must beat the CAD twin on the held-out run. `tests/fixtures/` holds\nthree real CAN 15 runs (184 KB) for the run-judgement checks.\n",
   "code_commit": "b0ad72b5cd + uncommitted changes (source sha256 e0c3b227ec95e1a1)",
   "code_path": "~/vt-lanes/rig-bam_ws/src/dev/bam_rack_twin",
   "code_hosted": "local only: the commit is not on GitHub's origin",
   "code_url": null,
   "page": "../robotparts-model/index.html?variant=M4.ripple.ideal",
   "id": "m0002"
  }
 ]
}
skin.css, README.md
/* robotparts' next skin: "Catalog" in light and "Signal" in dark (robotparts lane/colors @ 2b5bf9f,
   site/static/colors/1-catalog.css and 4-signal.css, read 2026-10-26; a prototype, not released). Their own tokens,
   then the one block that maps them onto data-ui's --data-* (docs/SPEC.md §10). The gallery scopes a skin to
   .data-ui-skin; on robotparts.dev the same block sits at :root. */
.data-ui-skin {
  --bg: #ffffff; --bg2: #eeeeee; --hover: #edf4f9; --ink: #222222; --ink2: #555555; --rule: #c4c4c4; --rule2: #dddddd;
  --accent: #006699; --on-accent: #ffffff; --warn: #a8320c; --good: #0b8c6d; --bad: #a8320c;
  --s1: #006699; --s2: #7b3c62; --s3: #0b8c6d; --s4: #a08718; --s5: #904714; --s6: #4f8952; --s7: #8d73b8; --s8: #c14f4b;
  --b-measured: #006699; --b-other: #555555;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Liberation Sans", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .data-ui-skin {
    --bg: #08090a; --bg2: #111214; --hover: #16171b; --ink: #f7f8f8; --ink2: #a6a8b0; --rule: #26282d; --rule2: #1a1b1f;
    --accent: #8e95ff; --on-accent: #08090a; --warn: #ff9e66; --good: #26aa87; --bad: #ff9e66;
    --s1: #727afc; --s2: #26aa87; --s3: #9a3d67; --s4: #b78a00; --s5: #2293d3; --s6: #3fa423; --s7: #bd67e0; --s8: #b61b2e;
    --b-measured: #8e95ff; --b-other: #a6a8b0;
  }
}
:root[data-theme="dark"] .data-ui-skin {
  --bg: #08090a; --bg2: #111214; --hover: #16171b; --ink: #f7f8f8; --ink2: #a6a8b0; --rule: #26282d; --rule2: #1a1b1f;
  --accent: #8e95ff; --on-accent: #08090a; --warn: #ff9e66; --good: #26aa87; --bad: #ff9e66;
  --s1: #727afc; --s2: #26aa87; --s3: #9a3d67; --s4: #b78a00; --s5: #2293d3; --s6: #3fa423; --s7: #bd67e0; --s8: #b61b2e;
  --b-measured: #8e95ff; --b-other: #a6a8b0;
}
/* the mapping: this is all a vanilla app writes */
.data-ui-skin {
  --data-bg: var(--bg); --data-fg: var(--ink); --data-muted: var(--ink2); --data-subtle: var(--bg2);
  --data-border: var(--rule); --data-rule: var(--rule2); --data-hover: var(--hover); --data-accent: var(--accent);
  --data-on-accent: var(--on-accent); --data-warn: var(--warn); --data-good: var(--good); --data-bad: var(--bad);
  --data-font: var(--sans); --data-mono: var(--mono); --data-radius: 4px;
  --data-series-1: var(--s1); --data-series-2: var(--s2); --data-series-3: var(--s3); --data-series-4: var(--s4);
  --data-series-5: var(--s5); --data-series-6: var(--s6); --data-series-7: var(--s7); --data-series-8: var(--s8);
  --data-basis-measured: var(--b-measured); --data-basis-datasheet: var(--b-other); --data-basis-derived: var(--b-other);
  --data-basis-estimate: var(--b-other); --data-basis-chosen: var(--b-other);
}
# Actuator model page

One actuator model's own page, the one every model name in the models case links to: what the model is (its
robotparts contract 2.3 kind and equation, what the twin actually runs), its error on each bench, its fitted terms,
the commit the rack's twin code was fitted with and that code's README at the commit, rendered as markdown.
Configuration only: a record view and a table over the models case's data file, the model picked by the rail.

- **App:** robotparts
- **Replaces:** nothing yet: the models proposal had no per-model page (Zach, 2026-10-26: "click it and get
  information on that actual model, perhaps … it renders the README of the model").
- **Shows:** `type: record` in sections (Model, Error, Fitted terms, Code) with `display: markdown` for the
  description and the README; the rail as a single-choice `select` on the model, opened by a readable link
  `?variant=M4.ripple.c1`; a table of the actuators it ran on.
- **Data:** `../robotparts-models/models.json`. The README is `src/dev/bam_rack_twin/README.md` read with `git show
  <commit>:…` from `~/bam_ws` at each actuator's fit commit (budget_can<N>.json `code.git_head`); none of
  those commits is on GitHub's origin, so the page links the local path and no hosted URL.
- **Skin:** robotparts' prototype "Catalog" light and "Signal" dark, mapped onto `--data-*`.

The original UI it replaces

nothing yet: the models proposal had no per-model page (Zach, 2026-10-26: "click it and get information on that actual model, perhaps … it renders the README of the model").

What it shows

type: record in sections (Model, Error, Fitted terms, Code) with display: markdown for the description and the README; the rail as a single-choice select on the model, opened by a readable link ?variant=M4.ripple.c1; a table of the actuators it ran on.

Data

../robotparts-models/models.json. The README is src/dev/bam_rack_twin/README.md read with git show <commit>:… from ~/bam_ws at each actuator's fit commit (budget_can<N>.json code.git_head); none of those commits is on GitHub's origin, so the page links the local path and no hosted URL.

Skin

robotparts' prototype "Catalog" light and "Signal" dark, mapped onto --data-*.