Gallery › robotparts › Actuator models · benches, leaderboard, parameters
Actuator models · benches, leaderboard, parameters
Every actuator model BAM's rack has scored, one file of configuration: the model × bench pivot first, then the
leaderboard and the parameters, as tabs of one .base. Each tab opens with its answer (KPI cards and how to read it),
rolls up across actuators by default (Roll up in the toolbar brings them back), and links every model to its own page.
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.
# robotparts' actuator models on BAM's rack: the pivot first, then the leaderboard and the parameters, one file of
# configuration over one data file (models.json: kind model | run | term; each view filters to its kind).
# Every tab opens with its answer (intro: KPI cards + how to read it), rolls up across actuators by default (rollup:),
# and links each model to its page (open: side, url: page -> the robotparts-model case). CONTRACT §16.
# Check it: node bin/data-ui.mjs check <this file>
data: models.json
open: { mode: side, url: page }
facets:
- actuator
- rung
- family
- { property: benches, label: Bench }
properties:
actuator: { displayName: Actuator }
variant: { displayName: Model }
model: { displayName: Model kind }
rung: { displayName: Rung }
family: { displayName: Family }
ladder_step: { displayName: Ladder step }
leader: { displayName: Best model }
headline_x: { displayName: Held-out error }
headline_mrad: { displayName: Held-out q RMS }
dataset_x: { displayName: Dataset }
worst_class_x: { displayName: Worst class }
floor_tape_x: { displayName: Floor tape }
twin_grade: { displayName: Twin-grade }
runner_up_x: { displayName: Runner-up }
rank_desc: { displayName: Models ranked }
n_runs: { displayName: Runs }
bench: { displayName: Bench }
benches: { displayName: Benches }
test: { displayName: Test }
run: { displayName: Run }
q_rms_mrad: { displayName: q RMS }
x_floor: { displayName: × floor }
coulomb: { displayName: Coulomb }
viscous: { displayName: Viscous }
breakaway: { displayName: Breakaway }
stribeck_velocity: { displayName: Stribeck velocity }
weakening: { displayName: Weakening }
capture_fraction: { displayName: Fraction }
capture_speed: { displayName: Speed }
capture_dwell: { displayName: Dwell }
ripple: { displayName: Ripple RMS }
backlash: { displayName: Backlash }
gear_stiffness: { displayName: Gear stiffness }
inertia: { displayName: Inertia }
torque_scale: { displayName: Torque scale }
kt: { displayName: Kt (output) }
current_loop: { displayName: Current loop }
term: { displayName: Term switched off }
delta_mrad: { displayName: Error added }
delta_x: { displayName: Added × floor }
term_note: { displayName: What it models }
description: { displayName: What it is }
readme: { displayName: README (at the fit commit) }
code_commit: { displayName: Fitted with commit }
code_path: { displayName: Code }
code_hosted: { displayName: Hosted }
views:
- type: pivot
name: Benches
intro:
text: |
**How does every model do on every bench?** A row is a model, rolled up across the actuators it ran on; a
column is one bench test; a cell is the median error of every run behind it, in multiples of that actuator's
measurement floor (1 × = as close as the rack repeats itself; twin-grade is ≤ 1.5 ×). In the strip, each dot
is a run and the tall tick their median, on one scale per column (its ends in the header). **Roll up** in the
toolbar brings the actuators back; click a cell for its runs, a model's name for its page.
kpi:
filters:
and:
- 'kind == "run"'
- '!x_floor.isEmpty()'
groupBy: { property: bench }
kpi:
value: { property: x_floor, summary: Median }
gate: { max: 1.5 }
filters:
and:
- 'kind == "run"'
- '!x_floor.isEmpty()'
order: [variant, actuator]
groupBy: { property: rung, direction: ASC }
splitBy: [bench, test]
value: { property: x_floor, summary: Median, display: dist }
rollup: { keep: [variant], dims: [actuator, variant, model, rung, family] }
columnSize: { variant: 200 }
- type: pivot
name: Benches · every actuator
intro:
text: |
The same pivot **without the roll-up**: one row per model per actuator, grouped actuator › rung, so a model's
weak actuator shows. This is the drill-down of **Benches**; models that never ran read "—", never 0.
filters: 'kind == "run"'
order: [variant]
groupBy: { property: actuator }
subGroupBy: { property: rung, direction: ASC }
splitBy: [bench]
value: { property: x_floor, summary: Median, display: dist }
- type: table
name: Leaderboard
intro:
text: |
**Which model wins on each actuator, and is it twin-grade yet?** A card is an actuator's best model on the
Oct 23 held-out tapes (× its floor), the delta its lead over the runner-up, the gate twin-grade's 1.5 ×, the
spark its models worst → best. Below, every model ranked, **rolled up across actuators** (a cell's dots are the
actuators it ran on); un-roll in **Roll up**, click a row for the actuators behind it.
kpi:
filters:
and:
- 'kind == "model"'
- '!headline_x.isEmpty()'
groupBy: { property: leader }
kpi:
value: { property: headline_x }
compare: { against: runner_up_x, better: lower, neutral: 0.02 }
spark: { x: rank_desc, y: headline_x }
gate: { max: 1.5 }
filters:
and:
- 'kind == "model"'
- '!headline_x.isEmpty()'
order: [variant, rung, headline_x, actuator, dataset_x, worst_class_x, floor_tape_x, twin_grade, n_runs]
sort:
- { property: headline_x, direction: ASC }
display: { headline_x: dist }
rollup: { keep: [variant], dims: [actuator, variant, model, ladder_step, rung, family] }
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] }
- { name: Code, properties: [code_commit, code_hosted, code_path, readme] }
- type: chart
name: Rung ladder
filters:
and:
- 'kind == "model"'
- '!headline_x.isEmpty()'
chart:
mark: { type: line, point: true }
encoding:
x: { field: ladder_step, title: "the model ladder, simplest first" }
y: { field: headline_x, scale: { type: log }, title: "held-out error (× floor)" }
color: { field: actuator }
guides:
- { rule: { y: 1.5 }, label: "twin-grade ≤ 1.5 × floor" }
hover: { title: variant, fields: [actuator, headline_x, headline_mrad, twin_grade] }
- type: table
name: Parameters
intro:
text: |
**What does each model run on?** One row per model and actuator with the terms it was fitted with, grouped
under Friction, Capture, Drivetrain and Driver. The mark after a value is its basis (● measured, ○ an estimate
whose fit the model card refused, C chosen); hover a value for its 95 % CI and source. "—" is a term the model
does not run, with the reason on hover. The cards are each actuator's Coulomb friction in its best model.
kpi:
filters:
and:
- 'kind == "model"'
- 'best == true'
groupBy: { property: actuator }
kpi:
value: { property: coulomb }
filters: 'kind == "model"'
order: [variant, actuator, coulomb, viscous, breakaway, stribeck_velocity, weakening, capture_fraction, capture_speed, capture_dwell, ripple, backlash, gear_stiffness, inertia, kt, current_loop, torque_scale]
groupBy: { property: rung }
groupOrder: [M2, M3, M4, M5, M1, M0]
columnGroups:
Friction: [coulomb, viscous, breakaway, stribeck_velocity, weakening]
Capture: [capture_fraction, capture_speed, capture_dwell]
Drivetrain: [ripple, backlash, gear_stiffness, inertia]
Driver: [kt, current_loop, torque_scale]
display: { coulomb: value, viscous: value, weakening: value, ripple: value, backlash: value, inertia: value, kt: value }
record:
sections:
- { name: Model, properties: [variant, actuator, rung, description] }
- { name: Terms, properties: [coulomb, viscous, weakening, ripple, backlash, inertia, kt, current_loop] }
- { name: Code, properties: [code_commit, code_hosted, code_path, readme] }
- type: table
name: Parameters · with CI
intro:
text: |
The friction terms **split into sub-columns**: value, the 95 % CI's low and high, and the basis, each its own
column under its term, instead of the range squeezed into the cell.
filters:
and:
- 'kind == "model"'
- '!coulomb.isEmpty()'
order: [variant, actuator, coulomb, viscous, weakening]
groupBy: { property: rung, direction: ASC }
display: { coulomb: stats, viscous: stats, weakening: stats }
stats:
coulomb: { parts: [value, low, high, basis], labels: { low: 95% CI low, high: high } }
viscous: { parts: [value, low, high, basis], labels: { low: 95% CI low, high: high } }
weakening: { parts: [value, low, high, basis], labels: { low: 95% CI low, high: high } }
- type: table
name: What each rung adds
intro:
text: |
**What does each term buy?** The full twin's ablation: switch one term off and replay the held-out tapes; the
error added is how much worse the twin gets (positive = the term helps). The strip shows every run's change on
one scale per column (dots runs, tall tick their median). The cards: the median added per rung, across actuators.
kpi:
filters: 'kind == "term"'
groupBy: { property: rung }
kpi:
value: { property: delta_x, summary: Median }
filters: 'kind == "term"'
order: [term, actuator, delta_mrad, delta_x, term_note]
groupBy: { property: rung }
groupOrder: [M2, M3, M4, driver]
sort:
- { property: delta_x, direction: DESC }
summaries: { delta_x: Median }
display: { delta_mrad: dist }
- type: table
name: What each rung adds · columns
intro:
text: |
The same ablation with the distribution **as columns**: the median, min and max of the per-run change, how many
runs, and the strip, each under "Error added".
filters: 'kind == "term"'
order: [term, actuator, delta_mrad, delta_x]
groupBy: { property: rung }
groupOrder: [M2, M3, M4, driver]
sort:
- { property: delta_x, direction: DESC }
display: { delta_mrad: stats }
- type: table
name: Runs
filters: 'kind == "run"'
order: [variant, bench, test, run, q_rms_mrad, x_floor]
groupBy: { property: actuator }
subGroupBy: { property: variant }
sort:
- { property: x_floor, direction: ASC }
summaries: { q_rms_mrad: Median, x_floor: Median }
fold: closed
// 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 models · benches, leaderboard, parameters
Every actuator model BAM's rack has scored, one file of configuration: the model × bench pivot first, then the
leaderboard and the parameters, as tabs of one `.base`. Each tab opens with its answer (KPI cards and how to read it),
rolls up across actuators by default (Roll up in the toolbar brings them back), and links every model to its own page.
- **App:** robotparts
- **Replaces:** the hand-built tables of robotparts' models proposal round 2 (lane/datasheet @ ec9a13c,
`docs/proposals/build_models_report_r2.py`: `compare_table`, `model_rows`, `dist_svg`, the verdict cards), the rack's
hand-rendered model cards (`model_cards/can<N>.md`), the table lane's `models.base` and Vibe Tracks' Perspective
results pivot.
- **Shows:** `intro:` (markdown + an inline `type: kpi`) on every tab; `rollup: {keep, dims}` on the pivot and the
leaderboard (the Roll up menu, a rolled-up row's records on click); `type: pivot` with distribution cells on one
scale per column (axis in the header); `columnGroups:` over the parameter columns; `display: stats` splitting a term
into value | 95 % CI low | high | basis and a distribution into median | min | max | n | runs; `display: value`
(CI on hover); numbers and marks in fixed slots; `open: {mode: side, url: page}` so a model opens in the side peek
with its README and its page. The rail: actuator, rung, family, bench.
- **Data:** `models.json`, written by `source/build.py` from BAM's rack analysis (`/archive/datasets/bam_rig`: the
Oct 23 budget and model cards, the Oct 24 dataset and floors, the Oct 25 twin rounds), the twin code's README read
with `git show` at each fit commit from `~/bam_ws`, and contract 2.3's model kinds from robotparts
`contract/v2.3`. Real numbers only; models that ran nothing read "—". `source/oracle.py` recomputes the checks'
expected numbers from the archive.
- **Skin:** robotparts' prototype "Catalog" light and "Signal" dark (lane/colors @ 2b5bf9f) mapped onto `--data-*`.
The original UI it replaces
the hand-built tables of robotparts' models proposal round 2 (lane/datasheet @ ec9a13c, docs/proposals/build_models_report_r2.py: compare_table, model_rows, dist_svg, the verdict cards), the rack's hand-rendered model cards (model_cards/can<N>.md), the table lane's models.base and Vibe Tracks' Perspective results pivot.
What it shows
intro: (markdown + an inline type: kpi) on every tab; rollup: {keep, dims} on the pivot and the leaderboard (the Roll up menu, a rolled-up row's records on click); type: pivot with distribution cells on one scale per column (axis in the header); columnGroups: over the parameter columns; display: stats splitting a term into value | 95 % CI low | high | basis and a distribution into median | min | max | n | runs; display: value (CI on hover); numbers and marks in fixed slots; open: {mode: side, url: page} so a model opens in the side peek with its README and its page. The rail: actuator, rung, family, bench.
Data
models.json, written by source/build.py from BAM's rack analysis (/archive/datasets/bam_rig: the Oct 23 budget and model cards, the Oct 24 dataset and floors, the Oct 25 twin rounds), the twin code's README read with git show at each fit commit from ~/bam_ws, and contract 2.3's model kinds from robotparts contract/v2.3. Real numbers only; models that ran nothing read "—". source/oracle.py recomputes the checks' expected numbers from the archive.
Skin
robotparts' prototype "Catalog" light and "Signal" dark (lane/colors @ 2b5bf9f) mapped onto --data-*.