docs(zoom): the ties both round up, they do not oppose

Review caught the explanation of the 0.015 round-trip break, not the
fix. Three copies of it claimed 16.015 rounds up while 16.005 rounds
down --- "opposite directions". Both round UP.

Verified rather than reasoned about: at the point the quantizer sees
them, 16.015 * 100 is exactly 1601.5 and 16.005 * 100 is exactly
1600.5. Both are exact ties, and half-up sends both away from zero.

So the mechanism is not opposed rounding, it is that half-up is not
symmetric under negation. Rounding up on the way in adds half a
centi-pixel; rounding up on the way out adds another, so the two
errors ACCUMULATE instead of cancelling, and 16.00 -> 16.02 -> 16.01
ends one centi-pixel high. "Opposite directions" would have predicted
them cancelling, which is the reverse of what happens.

Corrected in all three places that carried it: the module comment, the
test's doc comment, and framing section 3.2.

Comments only --- no behavior change, and the witness values in the
tests were already right. Zoom suite still 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-06 17:50:03 +02:00
parent 828f57debb
commit 1e054f7109
No known key found for this signature in database
3 changed files with 21 additions and 11 deletions

View File

@ -77,12 +77,16 @@ end
-- workaround for one. -- workaround for one.
-- --
-- It also RESTORES the round-trip contract, which a raw step breaks: -- It also RESTORES the round-trip contract, which a raw step breaks:
-- with 0.015 the sequence is 16.00 -> 16.02 -> 16.01, because each -- with 0.015 the sequence is 16.00 -> 16.02 -> 16.01. Each operation
-- operation rounds independently and 16.015 and 16.005 round in -- rounds independently, and both intermediates land on an EXACT tie ---
-- opposite directions. Quantizing first makes every step exact -- 16.015 and 16.005 are 1601.5 and 1600.5 centi-px --- which this
-- addition in the quantized domain, so n in and n out returns to the -- half-up quantizer sends UP. Half-up is not symmetric under negation:
-- starting value for ANY accepted step, not only for the ones that -- rounding up on the way in adds half a centi-pixel, and rounding up on
-- happened to be representable. -- the way out adds another, so the two errors accumulate instead of
-- cancelling. Quantizing first makes every step exact addition in the
-- quantized domain, so n in and n out returns to the starting value for
-- ANY accepted step, not only for the ones that happened to be
-- representable.
local function effective_step() local function effective_step()
return quantize(pmacs.config.get("ui.gpu-zoom-step")) return quantize(pmacs.config.get("ui.gpu-zoom-step"))
end end

View File

@ -133,9 +133,13 @@ validates **finiteness and bounds and nothing else**
*after* a value is stored — they cannot veto. So `0.015` is a *after* a value is stored — they cannot veto. So `0.015` is a
perfectly settable step, and nothing in the registry can refuse it. perfectly settable step, and nothing in the registry can refuse it.
Used raw it breaks the guarantee below, because each operation rounds Used raw it breaks the guarantee below. Each operation rounds
independently and `16.015` and `16.005` round in **opposite independently, and both intermediates land on an **exact tie**
directions**: `16.015` and `16.005` are `1601.5` and `1600.5` centi-pixels — which the
half-up quantizer sends **up**. Half-up is not symmetric under negation:
rounding up on the way in adds half a centi-pixel, rounding up on the
way out adds another, so the two errors accumulate instead of
cancelling:
``` ```
step 0.015: 16.00 -> 16.02 -> 16.01 round trip broken step 0.015: 16.00 -> 16.02 -> 16.01 round trip broken

View File

@ -128,8 +128,10 @@ fn n_steps_in_then_n_out_returns_exactly() {
/// The registry accepts any finite number in range — `ConfigKind::Number` /// The registry accepts any finite number in range — `ConfigKind::Number`
/// validates finiteness and bounds and nothing else, and `on_change` /// validates finiteness and bounds and nothing else, and `on_change`
/// cannot veto — so 0.015 is a settable step. Used raw it breaks the /// cannot veto — so 0.015 is a settable step. Used raw it breaks the
/// contract: each operation rounds independently, 16.015 rounds up and /// contract: each operation rounds independently, and both intermediates
/// 16.005 rounds down, giving 16.00 -> 16.02 -> 16.01. /// land on an exact tie — 16.015 and 16.005 — which half-up rounding
/// sends *up*. Up on the way in and up on the way out accumulate rather
/// than cancel, giving 16.00 -> 16.02 -> 16.01.
/// ///
/// Quantizing the step at the point of use restores exactness for every /// Quantizing the step at the point of use restores exactness for every
/// accepted step, not just the representable ones. /// accepted step, not just the representable ones.