Skip to content

fix(optimization): score the linkage from its initial position, over a full turn - #62

Merged
HugoFara merged 2 commits into
mainfrom
fix/kinematic-test-start-position
Sep 18, 2026
Merged

HugoFara merged 2 commits into
mainfrom
fix/kinematic-test-start-position

Conversation

@HugoFara

Copy link
Copy Markdown
Owner

Unblocks #61, whose "Executable docs" job failed twice on docs/examples/fourbar_linkage.py — as did main right after the 1.2.2 merge (run 35321528470). Not a flake in the example: a bug in the fitness wrapper.

kinematic_default_test scored a state the caller never gets. The wrapper sweeps one revolution in 12 long strides to reject the unbuildable early, then records the loci in a fine run — which started from wherever the coarse sweep had left the joints. A 0.5 rad stride can hop over a position the linkage cannot take, or onto the other assembly branch past a toggle, so the score certified a mechanism that raised UnbuildableError as soon as it was stepped from its initial position. PSO drives designs to the edge of buildability, so the example hit this on ~50% of runs. The fine run now restarts from the initial position; a new test checks its first frame is the first step from init_pos.

It also covers a complete revolution. It was n = 96 steps at dt=1 since 2021 whatever the crank speed — a quarter turn for the default 1°/step crank, so a fitness read the locus of a quarter of the cycle. Now max(96, period) steps. It keeps dt=1, the stride step() and the visualizers use: I tried a finer uniform grid over one turn and it certified different mechanisms (6/10 winners then failed at the natural stride), because which branch the solver follows past a toggle depends on the stride. Timings of the test suite and the docs tests are unchanged.

The example gets a crank with a whole number of steps per turn (tau / 20 instead of 0.31, period 20.27), so the 360-frame animation is 18 exact repeats of the phases the fitness scored, and resets the linkage to its initial position before animating the optimum. Probed over 10 PSO runs: fitness and 360-frame animation agree 10/10 (0/10 disagreed with the old code after the first fix alone, 8/10 before it). Ran the example 6× locally: 6/6.

Also: tests/docs/conftest.py reported a tutorial failure as a ValueError from relative_to() on the tmp path, hiding the real traceback.

…a full turn

The kinematic_minimization / kinematic_maximization wrapper first sweeps
one revolution in 12 long strides to reject the unbuildable early, then
records the loci in a fine run — which started from wherever the coarse
sweep had left the joints. A long stride can hop over a position the
linkage cannot take, or onto the other assembly branch past a toggle,
so the score certified a mechanism that raised UnbuildableError as soon
as it was stepped from its initial position. PSO drives a design to the
edge of buildability, so docs/examples/fourbar_linkage.py hit this on
about half its runs and the "Executable docs" workflow with it. The
fine run now restarts from the initial position.

It also covers a complete revolution. It was 96 steps at dt=1 since
2021, whatever the crank speed: a quarter turn for the default crank of
one degree per step, so a fitness read the locus of a quarter of the
cycle. It keeps dt=1, the stride step() and the visualizers use,
because which branch the solver follows past a toggle depends on the
stride: a finer grid certifies a different mechanism.

The example's crank now turns in a whole number of steps, so the
360-frame animation samples exactly the crank angles the fitness
scored, and the example puts the linkage back in its initial position
before animating the optimum, the position every particle was scored
from.

tests/docs/conftest.py reported a tutorial failure as a ValueError from
relative_to() on the temporary script path, hiding the real traceback.
…ound

Review of #62: set_coords() rewinds joint positions but not actuator
state. An ArcCrank keeps _angle and _direction, a LinearActuator
_extension and _direction, a Mechanism driver its current_angle, and
none of them is derived from the joint's coordinates as a Crank's angle
is. After the 12-stride sweep, the recorded run started with the
actuator a full cycle ahead of joints put back at their start, which is
neither the caller's run nor the old continuous one.

There is no API to rewind an actuator, and the recorded run now covers
the whole cycle, so the sweep has no job left: remove it and let the
recorded run be the first thing that moves. The test now covers an
ArcCrank too. The docstring and changelog state the cost of an
evaluation: get_rotation_period() steps, at least 96.
@HugoFara

Copy link
Copy Markdown
Owner Author

Review turned up that set_coords(start) rewinds joints but not actuators: ArcCrank / LinearActuator / Mechanism drivers keep their phase as internal state, so after the sweep the recorded run started with the actuator a cycle ahead of the joints. Only a plain Crank (angle re-derived from x/y) restarted, which is why the test passed.

Fixed by removing the 12-stride sweep altogether: the recorded run covers the whole cycle now, so the sweep had no job left, and there is no API to rewind an actuator. The test covers an ArcCrank too. Cost of an evaluation — get_rotation_period() steps, the LCM over actuators — is now stated in the docstring and changelog.

Known limitation, pre-existing and unchanged here: across repeated evaluations, set_coords(init_pos) does not reset an ArcCrank / LinearActuator / Mechanism driver's phase either, so in an optimizer each candidate starts where the previous one's actuator stopped. Needs a reset API on the actuators.

@HugoFara
HugoFara merged commit edff8c8 into main Sep 18, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant