Skip to content

refactor(synthesis): replace n_orientation_samples with orientation_resolution - #45

Merged
HugoFara merged 1 commit into
mainfrom
refactor/orientation-resolution
Sep 4, 2026
Merged

HugoFara merged 1 commit into
mainfrom
refactor/orientation-resolution

Conversation

@HugoFara

@HugoFara HugoFara commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The last piece of #29, before 1.2.0.

The problem

n_orientation_samples never denoted a number of samples. It was folded into a per-axis grid resolution:

per_axis = max(6, int(round(n_samples ** (1.0 / free))))

With four precision points free = 3, so every value from 6 to 216 gives per_axis = 6. Measured on the README's points, 6, 12, 36 and 72 all take the same time and return the same ten solutions.

The replacement

path_generation(points, orientation_resolution=6)   # angles per free orientation

The virtue is that the cost model becomes readable from the signature: the grid holds orientation_resolution ** (n_points - 1) candidates. A user can see the exponential rather than discovering it at five points and nine seconds.

The default of 6 reproduces the old default exactly. The old argument still works, warns, is translated through the old formula, and is scheduled for removal in 2.0.0 under the policy from #38. Verified identical on five point sets:

README       default=10  legacy36=10  identical=True
docs/bench   default=10  legacy36=10  identical=True
3 points     default= 6  legacy36= 6  identical=True
steep        default= 2  legacy36= 2  identical=True
wide         default=10  legacy36=10  identical=True

Two alternatives, implemented and rejected on measurement

A total candidate budget — what the name implies. Rejected: on three of six point sets the search returned nothing where it had found ten. Behaviour is a cliff, not a gradient, and the cliff sits somewhere problem-dependent that nobody can predict. A knob whose failure mode is silence is worse than a confusing one.

Coarse-to-fine or shuffled grid ordering — on the theory that truncating a lexicographic product() samples a biased corner, so a budget might work if any prefix were representative. Measured across five point sets:

Case lexicographic progressive shuffled
README 176 ms, 10 sol 131 ms, 10 sol 284 ms, 10 sol
docs/bench 748 ms, 10 sol 857 ms, 10 sol 876 ms, 10 sol
steep 1151 ms, 2 sol 1146 ms, 2 sol 1159 ms, 2 sol
wide 988 ms, 10 sol 980 ms, 10 sol 1073 ms, 10 sol
5 points 8964 ms, 0 sol 9075 ms, 0 sol 9035 ms, 0 sol

Same solutions, differences within noise. Solutions are spread evenly through the grid rather than clustered, so there is no cheaper subset to find and no free speedup in the ordering. The dense grid earns its cost — the search is unchanged here, and only the name was ever wrong.

That finding is recorded as a comment in the generator, so the constant does not read as arbitrary and nobody repeats the experiment.

A warning where there was silence

Cost grows exponentially in the number of precision points, and nothing said so. Five points reach a four-dimensional grid that can run for seconds and return nothing. Calls whose grid exceeds a thousand candidates now warn up front, and the message is repeated in SynthesisResult.warnings. Four points and fewer stay silent.

Scope

Sibling functions (six_bar_path_generation, generalized_synthesis, multi_topology.synthesize) keep n_orientation_samples, because there it is honest: they bound the number of orientations tried with their own loop counters, so the name matches the behaviour. Their calls into the generator go through an explicit _legacy_orientation_args translation, so their results are unchanged.

Verification

  • 17 new tests in tests/synthesis/test_orientation_resolution.py: the deprecation warns and names its replacement and removal release, the translation reproduces the old formula on five parameter/point combinations, the documented grid-size model is the real one, the candidate cap holds for six or more points, and the cost warning fires at five points but not at four or three.
  • Full suite 2505 passed, 5 skipped with all extras. Lint and mypy --strict clean on 134 files. Docs build clean.
  • The deprecations page and the benchmarks page are updated; the benchmarks note now describes a fixed parameter instead of a broken one.

One tidy-up carried along: tests/synthesis/test_core_extended.py still imported synthesis.core.Dyad, so our own suite tripped the deprecation #38 introduced. It uses BurmesterDyad now.

…esolution

n_orientation_samples never denoted a number of samples. It was folded
into a per-axis grid resolution through

    per_axis = max(6, int(round(n_samples ** (1.0 / free))))

so the floor swallowed it: with four precision points every value from 6
to 216 produced an identical search. Measured on the README's points, 6,
12, 36 and 72 all took the same time and returned the same ten solutions.

orientation_resolution is that per-axis number directly, which makes the
cost model readable from the signature: the grid holds
orientation_resolution ** (n_points - 1) candidates. The default of 6
reproduces the old default exactly, and a value passed to the old
argument is translated through the old formula, so no result moves.
Verified identical on five point sets.

The old name stays, warns, and is scheduled for removal in 2.0.0 under
the policy in docs/source/deprecations.md.

Two alternatives were implemented and rejected on measurement:

- Redefining the parameter as a total candidate budget, which its name
  implies. On three of six point sets the search then returned nothing
  where it had found ten.
- Reordering the grid coarse-to-fine, or shuffling it, in case a
  truncated lexicographic product was merely sampling a biased corner.
  Neither changed the solutions found nor the time taken, so candidates
  are spread evenly through the grid and there is no cheaper subset.

The dense grid earns its cost. The search is unchanged; only the name was
wrong.

Also adds a warning when the grid exceeds a thousand candidates. Cost
grows exponentially in the number of precision points and nothing said
so: five points reach a four-dimensional grid that can run for seconds
and still return nothing.

Sibling functions keep n_orientation_samples, where it genuinely bounds
the number of orientations tried through their own loop counters. Their
calls into the generator are translated so their behaviour is unchanged.
@HugoFara HugoFara added the code issue Code is not wrong, but something is off label Sep 4, 2026
@HugoFara
HugoFara merged commit 8545df4 into main Sep 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code issue Code is not wrong, but something is off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant