Spaces:
Running
A newer version of the Gradio SDK is available: 6.26.0
Review: Motion Axis Bias Toward [0, 1, 0]
Date: 2026-06-15
Scope: Does the current prompt (and surrounding pipeline) bias part motion
toward the world Y axis [0, 1, 0]?
Short answer
Yes. The system prompt nudges the model toward [0, 1, 0], and two
non-prompt layers (the renderer default and the coercion fallback) push the
rendered result toward Y even harder. The net effect is that uncertain or
under-specified parts tend to spin/oscillate/slide about world Y regardless of
their actual geometry orientation.
Where the bias comes from
1. Prompt anchoring (snap2sim/prompts.py, VISION_SYSTEM_PROMPT)
[0, 1, 0] is the only concrete axis literal the model ever sees, and it
appears twice with no counterexample:
- Line 29:
Motions, with axis as a numeric vector like [0, 1, 0]: - Line 40:
Every motion axis must be a numeric vector such as [0, 1, 0], never a string like x, y, or z.
The compact example payload (lines 52β68) uses a pulse motion, which has no
axis field, so it adds no balancing example. With a single repeated exemplar
and no contrast cases, an LLM under uncertainty defaults to the most available
pattern β here, [0, 1, 0]. This is textbook few-shot anchoring.
Note: EXAMPLE_ANALYSIS in snap2sim/schema.py is not injected into the
prompt (the prompt is self-contained), so it does not directly steer the model.
But it reflects the same lean: of its three parts that carry an axis, two are
[0, 1, 0] (ratchet_gear rotate, pawl oscillate) and one is [1, 0, 0]
(selector_pin translate). It is also the local sample-mode render, so the
Y-lean is visible there too.
2. Renderer hard default (index.html)
applyMotion, line 1418:
const axis = new THREE.Vector3(...(motion.axis || [0, 1, 0])).normalize();
If the model omits axis entirely (valid β axis is optional in the schema
and coercion never injects one), the renderer animates about Y. So "model is
biased to emit [0, 1, 0]" and "model omits axis" produce the same on-screen
result: Y-axis motion. This makes the bias look even stronger than the model's
raw output.
3. Coercion fallback (snap2sim/model_io.py)
_axis_vector(lines 297β309) returnsNonewhen no usable axis is present;_coerce_partonly addsaxisif truthy (lines 173β175). So omitted axes stay omitted β renderer default (Y) applies._generic_analysis(line 232) hard-codes"axis": [0, 1, 0]for its rotor part. Any fully-degraded response that falls back to the generic payload is pure Y.
Why this is a real visual problem, not just cosmetics
Geometry orientation and motion axis are decided independently, and they can disagree:
- Geometry:
axisProfile+orientYAxisGeometry/orientZAxisGeometry(index.htmllines 1319β1367) bake the primitive's long axis into the geometry buffer using the odd-one-outsizedimension. A cylinder withsize [3, 0.5, 3]... e.g.size [2.0, 0.4, 0.4]lays the shaft along world X. - Motion:
applyMotioncallsmesh.rotateOnAxis(axis, β¦)with the raw motion axis (object space, and with nobaseRotationthat equals world space).
So a shaft whose geometry was oriented along X but whose motion axis is the
biased [0, 1, 0] will spin about Y β i.e. it sweeps end-over-end instead
of rotating about its own length. The size-driven geometry inference added in
docs/reviews/scene-geometry-review.md made geometry orientation smart, but
motion axis was left to the model/default, so the two can visibly contradict.
Conclusion
The prompt is a genuine contributor to the [0, 1, 0] lean, but fixing only the
prompt will not fully resolve the on-screen behavior because the renderer
default and generic fallback also resolve to Y. Address the prompt and make
motion axis follow the geometry's inferred primitive axis when the model is
silent or low-signal.
Implementation plan (completed)
De-anchor the prompt (
snap2sim/prompts.py):- In the motions section, show axis examples across all three world axes,
e.g. spin about a shaft's long axis
[1, 0, 0]/[0, 0, 1]as well as[0, 1, 0], so no single axis is the lone exemplar. - Add one sentence: the motion axis should match the part's real axis of rotation/translation (usually the geometry's long/odd-one-out dimension), not default to vertical.
- Optionally add a second
partsexemplar in the compact shape that carries an explicit non-[0,1,0]axis, to balance thepulseexample.
- In the motions section, show axis examples across all three world axes,
e.g. spin about a shaft's long axis
Make the renderer default geometry-aware (
index.html):- When
motion.axisis absent, derive the default from the sameaxisProfileresult used to orient the geometry (map the inferredprofile.axis0/1/2 to[1,0,0]/[0,1,0]/[0,0,1]) instead of the hard[0, 1, 0]. This aligns spin with the part's long axis for free. - Keep
[0, 1, 0]only as the final fallback when no geometry/profile is available.
- When
Align the coercion fallback (
snap2sim/model_io.py):- Consider deriving
_generic_analysisrotor axis from its own geometry, or at minimum document that the generic part is intentionally Y. - Decide whether
_coerce_partshould inject a geometry-derived axis when the model omits one (vs. leaving it to the renderer). Prefer leaving it to the renderer so there is a single source of truth (option 2).
- Consider deriving
Verification after changes:
EXAMPLE_ANALYSISanddocs/fixtures/mini-fan-analysis.jsonrender with each part rotating about a sensible axis (shafts about their length).- Schema/parser/coercion checks still pass; FastAPI
TestClientsmoke for/,/analyze_image,/generate_scene. - Re-run a representative Modal analysis and inspect the distribution of
emitted motion axes β confirm it is no longer almost entirely
[0, 1, 0].
Implementation status
Completed locally on 2026-06-15:
- Prompt examples now show X, Y, and Z axes and explicitly tell the model to match motion axes to the real rotation or translation axis.
- The deterministic renderer stores the geometry-inferred axis on each mesh
and uses it when
motion.axisis omitted, keeping[0, 1, 0]only as the final no-profile fallback. _generic_analysisno longer hard-codes the rotor motion axis; its disk-like geometry lets the renderer derive the Y axis from size.- Local schema/parser/coercion checks, FastAPI smoke checks, and Playwright renderer probes passed.
- Modal dev
run_analysis_endpoint_checkreturned a rotatingdialpart withaxis: [1, 0, 0], confirming the prompt no longer collapses the synthetic check to[0, 1, 0]. - Stable Modal deployment completed after the prompt/renderer update, and an
unauthenticated deployed
runtime_proberequest returned401 Unauthorized. - GitHub-to-Hugging Face sync run
27544185461deployed commite7369ebto the then-private Space after shortening the READMEshort_descriptionmetadata to meet Hugging Face's 60-character limit. - Authenticated Space verification passed: the root served the trusted
shell, a synthetic target image returned
rotating shaft assemblyat0.65confidence with an emitted[1.0, 0.0, 0.0]motion axis,/generate_scenereturnedrenderer: threeandrender_mode: three, and no HTML field was present. - Final submission note, 2026-06-15: this pass shipped before the public
build-small-hackathon/Snap2Simsubmission.
Files referenced
snap2sim/prompts.pyβVISION_SYSTEM_PROMPT(lines 29, 40), example payload.index.htmlβapplyMotion(line 1418),axisProfile(1319β1344),orientYAxisGeometry/orientZAxisGeometry(1354β1367),buildPartMesh.snap2sim/model_io.pyβ_axis_vector(297β309),_coerce_part(145β204),_generic_analysis(~232).snap2sim/schema.pyβEXAMPLE_ANALYSISaxes, motion schema.