cozyclay 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const IDENTITY_3 = [
|
|
2
|
+
1, 0, 0,
|
|
3
|
+
0, 1, 0,
|
|
4
|
+
0, 0, 1,
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
function flat3(m) {
|
|
8
|
+
return Array.isArray(m[0]) ? m.flat() : m;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function rows3(m) {
|
|
12
|
+
return [m.slice(0, 3), m.slice(3, 6), m.slice(6, 9)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mat3Vec(m, v) {
|
|
16
|
+
return [
|
|
17
|
+
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
|
|
18
|
+
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
|
|
19
|
+
m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mat3Mul(a, b) {
|
|
24
|
+
return [
|
|
25
|
+
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
|
|
26
|
+
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
|
|
27
|
+
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
|
28
|
+
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
|
|
29
|
+
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
|
|
30
|
+
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
|
31
|
+
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
|
|
32
|
+
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
|
|
33
|
+
a[6] * b[2] + a[7] * b[5] + a[8] * b[8],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transpose3(m) {
|
|
38
|
+
return [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function add3(a, b) {
|
|
42
|
+
return a.map((x, i) => x + b[i]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scale3(m, s) {
|
|
46
|
+
return m.map((x) => x * s);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function skew(v) {
|
|
50
|
+
return [
|
|
51
|
+
0, -v[2], v[1],
|
|
52
|
+
v[2], 0, -v[0],
|
|
53
|
+
-v[1], v[0], 0,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function axisAngleToMatrix(axisAngle) {
|
|
58
|
+
const theta = Math.hypot(...axisAngle);
|
|
59
|
+
const W = skew(axisAngle);
|
|
60
|
+
const W2 = mat3Mul(W, W);
|
|
61
|
+
let a;
|
|
62
|
+
let b;
|
|
63
|
+
if (theta < 1e-8) {
|
|
64
|
+
const theta2 = theta * theta;
|
|
65
|
+
a = 1 - theta2 / 6 + theta2 * theta2 / 120;
|
|
66
|
+
b = 0.5 - theta2 / 24 + theta2 * theta2 / 720;
|
|
67
|
+
} else {
|
|
68
|
+
a = Math.sin(theta) / theta;
|
|
69
|
+
b = (1 - Math.cos(theta)) / (theta * theta);
|
|
70
|
+
}
|
|
71
|
+
return add3(add3(IDENTITY_3, scale3(W, a)), scale3(W2, b));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matrixToAxisAngle(m) {
|
|
75
|
+
const trace = m[0] + m[4] + m[8];
|
|
76
|
+
const qw = Math.sqrt(Math.max(0, 1 + trace)) / 2;
|
|
77
|
+
let qx;
|
|
78
|
+
let qy;
|
|
79
|
+
let qz;
|
|
80
|
+
if (qw > 1e-6) {
|
|
81
|
+
const scale = 1 / (4 * qw);
|
|
82
|
+
qx = (m[7] - m[5]) * scale;
|
|
83
|
+
qy = (m[2] - m[6]) * scale;
|
|
84
|
+
qz = (m[3] - m[1]) * scale;
|
|
85
|
+
} else if (m[0] >= m[4] && m[0] >= m[8]) {
|
|
86
|
+
qx = Math.sqrt(Math.max(0, 1 + m[0] - m[4] - m[8])) / 2;
|
|
87
|
+
const scale = qx > 1e-8 ? 1 / (4 * qx) : 0;
|
|
88
|
+
qy = (m[1] + m[3]) * scale;
|
|
89
|
+
qz = (m[2] + m[6]) * scale;
|
|
90
|
+
} else if (m[4] >= m[8]) {
|
|
91
|
+
qy = Math.sqrt(Math.max(0, 1 - m[0] + m[4] - m[8])) / 2;
|
|
92
|
+
const scale = qy > 1e-8 ? 1 / (4 * qy) : 0;
|
|
93
|
+
qx = (m[1] + m[3]) * scale;
|
|
94
|
+
qz = (m[5] + m[7]) * scale;
|
|
95
|
+
} else {
|
|
96
|
+
qz = Math.sqrt(Math.max(0, 1 - m[0] - m[4] + m[8])) / 2;
|
|
97
|
+
const scale = qz > 1e-8 ? 1 / (4 * qz) : 0;
|
|
98
|
+
qx = (m[2] + m[6]) * scale;
|
|
99
|
+
qy = (m[5] + m[7]) * scale;
|
|
100
|
+
}
|
|
101
|
+
const qNorm = Math.hypot(qw, qx, qy, qz);
|
|
102
|
+
if (!(qNorm > 0)) return [0, 0, 0];
|
|
103
|
+
const q = [qw / qNorm, qx / qNorm, qy / qNorm, qz / qNorm];
|
|
104
|
+
if (q[0] < 0) q.forEach((_, i) => { q[i] = -q[i]; });
|
|
105
|
+
const angle = 2 * Math.atan2(Math.hypot(q[1], q[2], q[3]), q[0]);
|
|
106
|
+
if (angle < 1e-12) return [0, 0, 0];
|
|
107
|
+
const scale = angle / Math.hypot(q[1], q[2], q[3]);
|
|
108
|
+
return [q[1] * scale, q[2] * scale, q[3] * scale];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rigidWorldToCamera(camera) {
|
|
112
|
+
const matrix = camera?.worldToCamera;
|
|
113
|
+
if (!Array.isArray(matrix) || matrix.length !== 4 || !matrix.every((row) => Array.isArray(row) && row.length === 4 && row.every(Number.isFinite))) {
|
|
114
|
+
throw new Error("camera.json needs a finite 4x4 worldToCamera matrix");
|
|
115
|
+
}
|
|
116
|
+
if (matrix[3][0] !== 0 || matrix[3][1] !== 0 || matrix[3][2] !== 0 || matrix[3][3] !== 1) {
|
|
117
|
+
throw new Error("camera.json worldToCamera must be an affine 4x4 matrix");
|
|
118
|
+
}
|
|
119
|
+
return matrix;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cameraFromJson(camera) {
|
|
123
|
+
const matrix = rigidWorldToCamera(camera);
|
|
124
|
+
const R_w2c = [
|
|
125
|
+
matrix[0][0], matrix[0][1], matrix[0][2],
|
|
126
|
+
matrix[1][0], matrix[1][1], matrix[1][2],
|
|
127
|
+
matrix[2][0], matrix[2][1], matrix[2][2],
|
|
128
|
+
];
|
|
129
|
+
const t_w2c = [matrix[0][3], matrix[1][3], matrix[2][3]];
|
|
130
|
+
const R_c2w = transpose3(R_w2c);
|
|
131
|
+
const t_c2w = scale3(mat3Vec(R_c2w, t_w2c), -1);
|
|
132
|
+
const K = camera.K?.map((row) => row.slice()) ?? [
|
|
133
|
+
[camera.fx, 0, camera.cx],
|
|
134
|
+
[0, camera.fy, camera.cy],
|
|
135
|
+
[0, 0, 1],
|
|
136
|
+
];
|
|
137
|
+
if (!Array.isArray(K) || K.length !== 3 || !K.every((row) => Array.isArray(row) && row.length === 3 && row.every(Number.isFinite))) {
|
|
138
|
+
throw new Error("camera.json needs a finite 3x3 K matrix");
|
|
139
|
+
}
|
|
140
|
+
return { K, R_c2w: rows3(R_c2w), t_c2w, R_w2c: rows3(R_w2c), t_w2c };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function camToWorldPoint(p, cam) {
|
|
144
|
+
return add3(mat3Vec(flat3(cam.R_c2w), p), cam.t_c2w);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function worldToCameraPoint(p, cam) {
|
|
148
|
+
return add3(mat3Vec(flat3(cam.R_w2c), p), cam.t_w2c);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function camToWorldOrient(axisAngleCam, cam) {
|
|
152
|
+
return matrixToAxisAngle(mat3Mul(flat3(cam.R_c2w), axisAngleToMatrix(axisAngleCam)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function worldToCamOrient(axisAngleWorld, cam) {
|
|
156
|
+
return matrixToAxisAngle(mat3Mul(flat3(cam.R_w2c), axisAngleToMatrix(axisAngleWorld)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function worldToPixel(p, cam) {
|
|
160
|
+
const [x, y, depth] = worldToCameraPoint(p, cam);
|
|
161
|
+
return [cam.K[0][0] * x / depth + cam.K[0][2], cam.K[1][1] * y / depth + cam.K[1][2], depth];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function pixelRay(u, v, cam) {
|
|
165
|
+
const rayCamera = [
|
|
166
|
+
(u - cam.K[0][2]) / cam.K[0][0],
|
|
167
|
+
(v - cam.K[1][2]) / cam.K[1][1],
|
|
168
|
+
1,
|
|
169
|
+
];
|
|
170
|
+
const direction = mat3Vec(flat3(cam.R_c2w), rayCamera);
|
|
171
|
+
const length = Math.hypot(...direction);
|
|
172
|
+
return { origin: cam.t_c2w.slice(), direction: direction.map((x) => x / length) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { axisAngleToMatrix, matrixToAxisAngle };
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fit the ten SuperMotion SMPL-X shape coefficients to the y-bot rest rig.
|
|
3
|
+
|
|
4
|
+
The production playback path does not deform cskel27 to every authored rig
|
|
5
|
+
bone. It measures the rig's leg height, divides by the canonical cskel27 toe
|
|
6
|
+
height, and applies that one factor to the neutral cskel27 offsets. This
|
|
7
|
+
script uses those same scaled neutral offsets as the shape targets. The
|
|
8
|
+
public cskel27-rest.json supplies the bind-joint inventory/rest contract; its
|
|
9
|
+
entries are rotations, while the neutral positions used by playback are the
|
|
10
|
+
canonical CSKEL27_NEUTRAL coordinates below.
|
|
11
|
+
|
|
12
|
+
Run this from the GVHMR checkout (or pass --gvhmr-root):
|
|
13
|
+
CUDA_VISIBLE_DEVICES="" .venv/bin/python /path/to/fit_mannequin_betas.py --device cpu --output /tmp/cclay-betas-out.json
|
|
14
|
+
|
|
15
|
+
The generated JSON is intentionally independent of a video or ground-truth
|
|
16
|
+
motion. Only the neutral rig measurements and the SMPL-X body model are used.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
|
|
29
|
+
# cskel27-neutral.js, copied as numerical data so this runner has no JS
|
|
30
|
+
# runtime dependency on the GPU box. Coordinates are metres, hips-origin,
|
|
31
|
+
# exactly as used by src/ardy/playback.js.
|
|
32
|
+
CSKEL27_NEUTRAL = torch.tensor([
|
|
33
|
+
[0.0, 0.0, 0.0],
|
|
34
|
+
[0.0, 0.0709891, -0.0473261],
|
|
35
|
+
[0.0, 0.1642033, -0.0637623],
|
|
36
|
+
[0.0, 0.2584953, -0.0720118],
|
|
37
|
+
[0.0, 0.3531475, -0.0720119],
|
|
38
|
+
[0.0, 0.6016096, -0.0365176],
|
|
39
|
+
[0.0, 0.7297793, -0.0139179],
|
|
40
|
+
[-0.0319949, 0.5259196, -0.0186873],
|
|
41
|
+
[-0.1909029, 0.5259195, -0.0186873],
|
|
42
|
+
[-0.4863389, 0.5259194, -0.0186873],
|
|
43
|
+
[-0.7189909, 0.5259193, -0.0186873],
|
|
44
|
+
[-0.7886024, 0.5259193, -0.0186873],
|
|
45
|
+
[-0.7468355, 0.5073563, 0.0277204],
|
|
46
|
+
[0.0319949, 0.5259196, -0.0186873],
|
|
47
|
+
[0.1909029, 0.5259196, -0.0186873],
|
|
48
|
+
[0.4863389, 0.5259196, -0.0186873],
|
|
49
|
+
[0.7189909, 0.5259196, -0.0186873],
|
|
50
|
+
[0.7886024, 0.5259196, -0.0186873],
|
|
51
|
+
[0.7468355, 0.5073565, 0.0277204],
|
|
52
|
+
[-0.0949182, -0.0277289, 0.0],
|
|
53
|
+
[-0.0949182, -0.4398469, 0.0],
|
|
54
|
+
[-0.0949182, -0.8959379, 0.0],
|
|
55
|
+
[-0.0949182, -0.9544128, 0.1606583],
|
|
56
|
+
[0.0949182, -0.0277289, 0.0],
|
|
57
|
+
[0.0949182, -0.4398469, 0.0],
|
|
58
|
+
[0.0949182, -0.8959379, 0.0],
|
|
59
|
+
[0.0949182, -0.9544128, 0.1606583],
|
|
60
|
+
], dtype=torch.float32)
|
|
61
|
+
|
|
62
|
+
# The values are measured from public/models/y-bot-tpose.fbx in armature
|
|
63
|
+
# metres: hips y 0.9979193878, lowest toe y 0.0328365048. The playback
|
|
64
|
+
# code uses the same lowest-toe convention and canonical height 0.9544128.
|
|
65
|
+
YBOT_LEG_HEIGHT_M = 0.9650828829792554
|
|
66
|
+
CANONICAL_LEG_HEIGHT_M = 0.9544128
|
|
67
|
+
|
|
68
|
+
# SMPL-24 regressed-joint indices, matching the OBS contract and
|
|
69
|
+
# tools/ardy/smpl-cskel27.mjs.
|
|
70
|
+
SEGMENTS = {
|
|
71
|
+
"thigh_left": ((23, 24), (1, 4)),
|
|
72
|
+
"thigh_right": ((19, 20), (2, 5)),
|
|
73
|
+
"shin_left": ((24, 25), (4, 7)),
|
|
74
|
+
"shin_right": ((20, 21), (5, 8)),
|
|
75
|
+
"foot_left": ((25, 26), (7, 10)),
|
|
76
|
+
"foot_right": ((21, 22), (8, 11)),
|
|
77
|
+
"pelvis_width": ((23, 19), (1, 2)),
|
|
78
|
+
"spine_length": ((0, 4), (0, 9)),
|
|
79
|
+
# Playback's Shoulder/Arm scale spans cskel Spine3 -> Arm, because the
|
|
80
|
+
# rig's collar and upper-arm chain are authored as one visible shoulder
|
|
81
|
+
# reach. The corresponding SMPL source is spine3 -> shoulder.
|
|
82
|
+
"upper_arm_left": ((4, 14), (9, 16)),
|
|
83
|
+
"upper_arm_right": ((4, 8), (9, 17)),
|
|
84
|
+
"forearm_left": ((14, 15), (16, 18)),
|
|
85
|
+
"forearm_right": ((8, 9), (17, 19)),
|
|
86
|
+
"shoulder_width": ((14, 8), (16, 17)),
|
|
87
|
+
"total_height": (None, None),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def parse_args() -> argparse.Namespace:
|
|
92
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
93
|
+
parser.add_argument("--output", required=True, type=Path)
|
|
94
|
+
parser.add_argument("--gvhmr-root", type=Path, default=Path.cwd())
|
|
95
|
+
parser.add_argument("--device", choices=("cpu",), default="cpu")
|
|
96
|
+
parser.add_argument("--adam-steps", type=int, default=5000)
|
|
97
|
+
parser.add_argument("--adam-lr", type=float, default=0.005)
|
|
98
|
+
parser.add_argument("--beta-limit", type=float, default=3.0)
|
|
99
|
+
parser.add_argument("--smooth-max-weight", type=float, default=0.0)
|
|
100
|
+
parser.add_argument("--smooth-max-temperature", type=float, default=0.002)
|
|
101
|
+
parser.add_argument("--minimax-steps", type=int, default=350)
|
|
102
|
+
parser.add_argument("--prior", type=float, default=1e-5)
|
|
103
|
+
# Segment lengths of the rendered rig (tools/bench/obs/ybot-targets.mjs).
|
|
104
|
+
# Segments absent from the file are left out of the fit.
|
|
105
|
+
parser.add_argument("--targets-json", type=Path)
|
|
106
|
+
return parser.parse_args()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_model(root: Path, device: torch.device):
|
|
110
|
+
root = root.resolve()
|
|
111
|
+
if str(root) not in sys.path:
|
|
112
|
+
sys.path.insert(0, str(root))
|
|
113
|
+
os.chdir(root)
|
|
114
|
+
from cclay_gvhmr_extract import make_smplx # pylint: disable=import-outside-toplevel
|
|
115
|
+
|
|
116
|
+
model = make_smplx("supermotion").to(device).eval()
|
|
117
|
+
sparse = torch.load(root / "hmr4d/utils/body_model/smplx2smpl_sparse.pt", map_location=device).to(device)
|
|
118
|
+
regressor = torch.load(root / "hmr4d/utils/body_model/smpl_neutral_J_regressor.pt", map_location=device).to(device)
|
|
119
|
+
return model, sparse, regressor
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def length(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
|
123
|
+
return torch.linalg.vector_norm(a - b)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def targets(device: torch.device) -> tuple[dict[str, float], float]:
|
|
127
|
+
neutral = CSKEL27_NEUTRAL.to(device)
|
|
128
|
+
scale = YBOT_LEG_HEIGHT_M / CANONICAL_LEG_HEIGHT_M
|
|
129
|
+
values: dict[str, float] = {}
|
|
130
|
+
for name, spec in SEGMENTS.items():
|
|
131
|
+
if name == "total_height":
|
|
132
|
+
values[name] = float((neutral[:, 1].max() - neutral[:, 1].min()) * scale)
|
|
133
|
+
continue
|
|
134
|
+
cskel_pair, _ = spec
|
|
135
|
+
values[name] = float(length(neutral[cskel_pair[0]], neutral[cskel_pair[1]]) * scale)
|
|
136
|
+
return values, scale
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def rest_joints(model, sparse, regressor, betas: torch.Tensor) -> torch.Tensor:
|
|
140
|
+
output = model(
|
|
141
|
+
**{
|
|
142
|
+
"betas": betas,
|
|
143
|
+
"body_pose": torch.zeros((1, 63), device=betas.device),
|
|
144
|
+
"global_orient": torch.zeros((1, 3), device=betas.device),
|
|
145
|
+
"transl": torch.zeros((1, 3), device=betas.device),
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
smpl_vertices = torch.stack([torch.matmul(sparse, vertices) for vertices in output.vertices])
|
|
149
|
+
return torch.einsum("jv,tvc->tjc", regressor, smpl_vertices)[0]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def measured_lengths(joints: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
153
|
+
result: dict[str, torch.Tensor] = {}
|
|
154
|
+
for name, spec in SEGMENTS.items():
|
|
155
|
+
if name == "total_height":
|
|
156
|
+
result[name] = joints[:, 1].max() - joints[:, 1].min()
|
|
157
|
+
continue
|
|
158
|
+
_, smpl_pair = spec
|
|
159
|
+
result[name] = length(joints[smpl_pair[0]], joints[smpl_pair[1]])
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def fit(args: argparse.Namespace) -> dict:
|
|
164
|
+
if args.device != "cpu":
|
|
165
|
+
raise RuntimeError("mannequin beta fitting is CPU-only")
|
|
166
|
+
device = torch.device("cpu")
|
|
167
|
+
model, sparse, regressor = load_model(args.gvhmr_root, device)
|
|
168
|
+
target, playback_scale = targets(device)
|
|
169
|
+
target_source = "cskel27 neutral offsets scaled by playback leg-height rule"
|
|
170
|
+
if args.targets_json is not None:
|
|
171
|
+
global SEGMENTS # pylint: disable=global-statement
|
|
172
|
+
rig = json.loads(args.targets_json.read_text(encoding="utf-8"))
|
|
173
|
+
target = {name: float(value) for name, value in rig["targets"].items() if name in SEGMENTS}
|
|
174
|
+
SEGMENTS = {name: spec for name, spec in SEGMENTS.items() if name in target}
|
|
175
|
+
target_source = f"rendered rig rest pose ({rig.get('model', args.targets_json.name)}), segments: {', '.join(SEGMENTS)}"
|
|
176
|
+
target_tensor = torch.tensor([target[name] for name in SEGMENTS], device=device)
|
|
177
|
+
|
|
178
|
+
if args.beta_limit <= 0:
|
|
179
|
+
raise ValueError("--beta-limit must be positive")
|
|
180
|
+
betas = torch.zeros((1, 10), device=device, requires_grad=True)
|
|
181
|
+
if args.adam_lr <= 0 or args.smooth_max_weight < 0 or args.smooth_max_temperature <= 0:
|
|
182
|
+
raise ValueError("optimizer parameters must be positive")
|
|
183
|
+
optimizer = torch.optim.Adam([betas], lr=args.adam_lr)
|
|
184
|
+
for _ in range(args.adam_steps):
|
|
185
|
+
fitted = measured_lengths(rest_joints(model, sparse, regressor, betas))
|
|
186
|
+
fitted_tensor = torch.stack([fitted[name] for name in SEGMENTS])
|
|
187
|
+
residual = fitted_tensor - target_tensor
|
|
188
|
+
smooth_max = args.smooth_max_temperature * torch.logsumexp(
|
|
189
|
+
torch.abs(residual) / args.smooth_max_temperature, dim=0
|
|
190
|
+
)
|
|
191
|
+
loss = (
|
|
192
|
+
torch.sum(residual**2)
|
|
193
|
+
+ args.smooth_max_weight * smooth_max
|
|
194
|
+
+ args.prior * torch.sum(betas**2)
|
|
195
|
+
)
|
|
196
|
+
optimizer.zero_grad()
|
|
197
|
+
loss.backward()
|
|
198
|
+
optimizer.step()
|
|
199
|
+
with torch.no_grad():
|
|
200
|
+
betas.clamp_(-args.beta_limit, args.beta_limit)
|
|
201
|
+
# Refine the projected-Adam point with a CPU-only minimax solve. SLSQP's
|
|
202
|
+
# explicit bounds are the hard box constraint; t bounds every signed
|
|
203
|
+
# residual, while the small quadratic term keeps the requested L2 prior.
|
|
204
|
+
from scipy.optimize import minimize # pylint: disable=import-outside-toplevel
|
|
205
|
+
import numpy as np # pylint: disable=import-outside-toplevel
|
|
206
|
+
|
|
207
|
+
def residual_and_jacobian(beta_values):
|
|
208
|
+
beta_tensor = torch.tensor(beta_values, dtype=torch.float32, device=device, requires_grad=True).reshape(10)
|
|
209
|
+
|
|
210
|
+
def values(value):
|
|
211
|
+
measured = measured_lengths(rest_joints(model, sparse, regressor, value[None]))
|
|
212
|
+
return torch.stack([measured[name] for name in SEGMENTS])
|
|
213
|
+
|
|
214
|
+
residual = values(beta_tensor) - target_tensor
|
|
215
|
+
jacobian = torch.autograd.functional.jacobian(values, beta_tensor)
|
|
216
|
+
return residual.detach().numpy(), jacobian.detach().numpy()
|
|
217
|
+
|
|
218
|
+
def minimax_objective(variables):
|
|
219
|
+
residual, jacobian = residual_and_jacobian(variables[:10])
|
|
220
|
+
beta_values = variables[:10]
|
|
221
|
+
value = variables[10] + 1e-3 * (
|
|
222
|
+
float((residual * residual).sum()) + args.prior * float((beta_values * beta_values).sum())
|
|
223
|
+
)
|
|
224
|
+
gradient = np.zeros(11, dtype=float)
|
|
225
|
+
gradient[:10] = 1e-3 * (2 * jacobian.T @ residual + 2 * args.prior * beta_values)
|
|
226
|
+
gradient[10] = 1.0
|
|
227
|
+
return value, gradient
|
|
228
|
+
|
|
229
|
+
def minimax_constraints(variables):
|
|
230
|
+
residual, _ = residual_and_jacobian(variables[:10])
|
|
231
|
+
return np.concatenate((variables[10] - residual, variables[10] + residual))
|
|
232
|
+
|
|
233
|
+
start_beta = betas.detach().cpu().numpy().astype(float).reshape(10)
|
|
234
|
+
seeds = [start_beta]
|
|
235
|
+
active_face_seed = start_beta.copy()
|
|
236
|
+
active_face_seed[6:10] = [-args.beta_limit, -args.beta_limit, args.beta_limit, args.beta_limit]
|
|
237
|
+
seeds.append(active_face_seed)
|
|
238
|
+
seeds.append(np.array([
|
|
239
|
+
2.50849517, 0.85280948, 0.30636654, 1.80294858, 1.24747668,
|
|
240
|
+
0.64413053, -args.beta_limit, -args.beta_limit, args.beta_limit, args.beta_limit,
|
|
241
|
+
], dtype=float))
|
|
242
|
+
best_candidate = betas.detach().clone()
|
|
243
|
+
best_residual = torch.max(torch.abs(torch.stack([
|
|
244
|
+
measured_lengths(rest_joints(model, sparse, regressor, best_candidate))[name] - target[name]
|
|
245
|
+
for name in SEGMENTS
|
|
246
|
+
])))
|
|
247
|
+
result = None
|
|
248
|
+
for seed_beta in seeds:
|
|
249
|
+
seed_residual, _ = residual_and_jacobian(seed_beta)
|
|
250
|
+
start = np.r_[seed_beta, max(abs(seed_residual))]
|
|
251
|
+
trial = minimize(
|
|
252
|
+
lambda variables: minimax_objective(variables)[0],
|
|
253
|
+
start,
|
|
254
|
+
jac=lambda variables: minimax_objective(variables)[1],
|
|
255
|
+
constraints={"type": "ineq", "fun": minimax_constraints},
|
|
256
|
+
bounds=[(-args.beta_limit, args.beta_limit)] * 10 + [(0.0, 1.0)],
|
|
257
|
+
method="SLSQP",
|
|
258
|
+
options={"maxiter": args.minimax_steps, "ftol": 1e-10},
|
|
259
|
+
)
|
|
260
|
+
candidate = torch.tensor(trial.x[:10], dtype=torch.float32, device=device)
|
|
261
|
+
with torch.no_grad():
|
|
262
|
+
candidate.clamp_(-args.beta_limit, args.beta_limit)
|
|
263
|
+
candidate_residual = torch.stack([
|
|
264
|
+
measured_lengths(rest_joints(model, sparse, regressor, candidate.reshape(1, 10)))[name] - target[name]
|
|
265
|
+
for name in SEGMENTS
|
|
266
|
+
])
|
|
267
|
+
candidate_max = torch.max(torch.abs(candidate_residual))
|
|
268
|
+
if torch.isfinite(candidate).all() and candidate_max < best_residual:
|
|
269
|
+
best_candidate = candidate.clone()
|
|
270
|
+
best_residual = candidate_max
|
|
271
|
+
result = trial
|
|
272
|
+
|
|
273
|
+
with torch.no_grad():
|
|
274
|
+
betas = best_candidate.reshape(1, 10)
|
|
275
|
+
fitted = measured_lengths(rest_joints(model, sparse, regressor, betas))
|
|
276
|
+
|
|
277
|
+
beta_values = betas.detach().cpu().reshape(-1).tolist()
|
|
278
|
+
rows = {}
|
|
279
|
+
residuals = []
|
|
280
|
+
for name in SEGMENTS:
|
|
281
|
+
target_m = target[name]
|
|
282
|
+
fitted_m = float(fitted[name].detach().cpu())
|
|
283
|
+
residual_m = fitted_m - target_m
|
|
284
|
+
residuals.append(residual_m)
|
|
285
|
+
rows[name] = {
|
|
286
|
+
"target_m": target_m,
|
|
287
|
+
"fitted_m": fitted_m,
|
|
288
|
+
"residual_m": residual_m,
|
|
289
|
+
"abs_residual_m": abs(residual_m),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"schema": "obs.mannequin-betas.v1",
|
|
294
|
+
"method": {
|
|
295
|
+
"model": "smpl-x supermotion neutral",
|
|
296
|
+
"optimizer": "projected Adam warm start + bounded SLSQP minimax",
|
|
297
|
+
"adam_steps": args.adam_steps,
|
|
298
|
+
"adam_lr": args.adam_lr,
|
|
299
|
+
"minimax_steps": args.minimax_steps,
|
|
300
|
+
"minimax_starts": 3,
|
|
301
|
+
"minimax_success": bool(result.success) if result is not None else False,
|
|
302
|
+
"minimax_status": int(result.status) if result is not None else -1,
|
|
303
|
+
"beta_limit": args.beta_limit,
|
|
304
|
+
"smooth_max_weight": args.smooth_max_weight,
|
|
305
|
+
"smooth_max_temperature": args.smooth_max_temperature,
|
|
306
|
+
"l2_prior": args.prior,
|
|
307
|
+
"device": str(device),
|
|
308
|
+
"source": target_source,
|
|
309
|
+
},
|
|
310
|
+
"betas": beta_values,
|
|
311
|
+
"target_geometry": {
|
|
312
|
+
"ybot_leg_height_m": YBOT_LEG_HEIGHT_M,
|
|
313
|
+
"canonical_cskel27_leg_height_m": CANONICAL_LEG_HEIGHT_M,
|
|
314
|
+
"playback_scale": playback_scale,
|
|
315
|
+
"rest_contract": "public/ardy/cskel27-rest.json",
|
|
316
|
+
},
|
|
317
|
+
"residuals": rows,
|
|
318
|
+
"summary": {
|
|
319
|
+
"segment_count": len(rows),
|
|
320
|
+
"max_abs_beta": max(abs(value) for value in beta_values),
|
|
321
|
+
"max_abs_residual_m": max(abs(value) for value in residuals),
|
|
322
|
+
"rms_residual_m": (sum(value * value for value in residuals) / len(residuals)) ** 0.5,
|
|
323
|
+
"max_bone_length_residual_m": max(abs(value) for value in residuals),
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def main() -> None:
|
|
329
|
+
args = parse_args()
|
|
330
|
+
result = fit(args)
|
|
331
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
332
|
+
args.output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
|
333
|
+
print(json.dumps(result["summary"], sort_keys=True), flush=True)
|
|
334
|
+
print("betas=" + json.dumps(result["betas"]), flush=True)
|
|
335
|
+
for name, row in result["residuals"].items():
|
|
336
|
+
print(f"{name}: target={row['target_m']:.9f} fitted={row['fitted_m']:.9f} residual={row['residual_m']:+.9f}", flush=True)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
main()
|