Skip to content

fix(precision): evaluation complies with its arguments instead of refusing them - #394

Open
ofloveandhate wants to merge 1 commit into
developfrom
fix/slp-precision-follows-eval-args
Open

fix(precision): evaluation complies with its arguments instead of refusing them#394
ofloveandhate wants to merge 1 commit into
developfrom
fix/slp-precision-follows-eval-args

Conversation

@ofloveandhate

Copy link
Copy Markdown
Contributor

Fixes ask 1 of #377.

A System doesn't just complain — it gets WEDGED

An SLP's Memory takes its precision from the ambient DefaultPrecision() when the
program is lazily compiled, while the owning System keeps whatever it was told. They
diverge the moment anything moves the ambient default — which an AMP tracker or endgame
does as a matter of course. Once they have, both directions throw:

built at 30, ambient default moved to 16 (what an AMP endgame leaves behind)
  eval@30  (the system's OWN precision)   FAIL  variable_values and SLP must be of same precision: 30 16
  eval@16  (the ambient precision)        FAIL  input point (16) must match the system (30)
  eval@30 after S.precision(16); S.precision(30)   FAIL  ...unchanged

That third line is the point. System::precision(n) cannot repair it: it and
StraightLineProgram::precision(n) both short-circuit when handed the value they already
hold, so the System — already believing it is at 30 — pushes nothing down to the SLP. The
only escape is setting the ambient global, which means System::precision() is a report of
something that may no longer be true.

Comply, don't refuse

The doctrine: a Memory's precision is an artifact of the current evaluation, never an
invariant to defend. The compiled Program is a precision-independent tape of operations;
only the Memory holding values carries digits. Likewise a System's precision is the fan-out
point to its blocks' working coefficients and its patch — each of which already keeps an
exact master (constant_recipes_, coefficients_highest_precision_) beside its working
copy. Nothing irreplaceable lives in either.

  • SetVariableValues re-tags the SLP's memory to the incoming precision, reusing
    precision(), which refills constants from their exact recipes rather than padding
    with zeros — accuracy is rebuilt, not faked.
  • SetPathVariable only ever raises: the variables are already in memory by then, so
    re-tagging downward would truncate them. Memory ends an evaluation at the max of its
    arguments' precisions. The time slot is re-tagged after assignment, since assigning an mp
    value adopts the source's precision.
  • System::SetVariables aligns the System to the incoming point, pushing precision down
    to every block and the patch. Deliberately not behind
    BERTINI_DISABLE_PRECISION_CHECKS — alignment is required behaviour, not an assertion.

Threading

Unaffected. ZeroDimSolver already hands each worker a deep-cloned System, and the comment
there says why: "a target-system copy (residual evaluation mutates System precision
state)"
. The codebase already accepts that evaluating mutates precision state and solves it
by ownership.

Tests, both languages

core/test/classes/precision_propagation_test.cpp and
python/test/classes/slp_precision_follows_args_test.py: the wedge reproduction for Eval
and Jacobian, one system evaluated up and down a ladder of precisions, and the
max-of-arguments rule for the path variable.

The pointed one asserts that a constant inexact in decimal1/3 — compiled at 16
digits returns residual exactly 0 when evaluated at 100 digits. That holds only if
re-tagging rebuilds constants from their recipes; padding a 16-digit 1/3 out to 100
would leave the tail zero and the residual at ~1e-17.

Verification

  • ctest --test-dir build/core10/10 suites pass, including test_tracking_basics,
    test_endgames and test_nag_algorithms, the heaviest users of precision changes
  • pytest python/test/933 passed, 1 skipped
  • Numerically inert. Four decomposition tests in a downstream consumer — witness-point
    counts, warning counts, Euler characteristic — fail byte-identically with and without
    this change: same face count, same midslice count, same warning count, same χ. The
    baseline binary was confirmed to be the baseline by checking that it still refuses, rather
    than by trusting build timing.

Scope

This is ask 1 only. Asks 2 and 3 — removing System::precision as load-bearing
caller-managed state, and one documented ownership rule — are now cheaper rather than
harder: with evaluation aligning on its own, the fan-out System::precision() performs is
triggered by the arguments instead of by hand, so removal is a sweep over ~161 call sites
(69 core/include, 46 core/test, 37 python, 1 binding) plus an ADR, not new machinery.
Left for its own PR since it is an API break.

ADR

None proposed. Both non-obvious choices are load-bearing but explained inline at their
sites: why SetPathVariable may only raise, and why re-tagging must rebuild constants
rather than pad. The regression tests fail loudly if either is undone. The ADR that is
warranted is the ownership rule from ask 3, which belongs with the removal PR.

…using them

A System was WEDGED after anything moved the ambient default precision, with no way
out.  An SLP's Memory takes its precision from DefaultPrecision() when the program is
lazily compiled, while the owning System keeps whatever it was told, so the two diverge
the moment an AMP tracker or endgame moves the ambient default -- which they do as a
matter of course.  Both directions then threw:

    eval at the system's OWN precision  ->  "variable_values and SLP must be of same
                                             precision.  respective precisions: 30 16"
    eval at the ambient precision       ->  "precision of input point in SetVariables
                                             (16) must match the precision of the
                                             system (30)."

and neither setter could repair it, because System::precision(n) and
StraightLineProgram::precision(n) both short-circuit when handed the value they already
hold.  Measured: the endgame -> Jacobian-spectrum path hits this every single time,
since the endgame is exactly what moved the default.

The Memory's precision is an ARTIFACT OF THE CURRENT EVALUATION, never an invariant to
defend: the compiled Program is a precision-independent tape of operations, and only the
Memory holding values carries digits.  So evaluating at whatever precision the caller
brings is always meaningful, and the right response to a mismatch is to re-tag, not to
refuse.  Likewise a System, whose precision is the fan-out point to its blocks' working
coefficients and its patch -- each of which already keeps an exact master
(constant_recipes_, coefficients_highest_precision_) beside its working copy.

  - SetVariableValues re-tags the SLP's memory to the incoming precision, reusing
    precision(), which REFILLS CONSTANTS FROM THEIR EXACT RECIPES rather than padding
    them with zeros -- so accuracy is rebuilt, not faked.
  - SetPathVariable only ever RAISES: the variables are already in memory by then, so
    re-tagging downward would truncate them.  Memory ends an evaluation at the max of
    its arguments' precisions, and the time slot is re-tagged after assignment because
    assigning an mp value adopts the source's precision.
  - System::SetVariables aligns the System to the incoming point, pushing the precision
    down to every block and the patch.  Deliberately NOT behind
    BERTINI_DISABLE_PRECISION_CHECKS: alignment is required behaviour, not an assertion.

Threading is unaffected: ZeroDimSolver already gives every worker its own deep-cloned
System, precisely because "residual evaluation mutates System precision state".

Tests in both languages.  Beyond the wedge reproduction for Eval and Jacobian, one
system evaluated up and down a ladder of precisions, and the max-of-arguments rule for
the path variable, the mean one asserts that a constant INEXACT in decimal -- 1/3 --
compiled at 16 digits returns residual 0 when evaluated at 100 digits.  That only holds
if re-tagging REBUILDS constants from their recipes; padding a 16-digit 1/3 would leave
the tail zero and the residual at ~1e-17.

Part of #377.  Does not yet address its asks 2 and 3 (removing System precision as
load-bearing caller-managed state, and one documented ownership rule) -- with evaluation
now aligning on its own, System::precision() has nothing left that callers need, so
removing it is a follow-up sweep over ~161 call sites plus an ADR.

Verified: ctest 10/10 suites pass, pytest 933 passed 1 skipped.
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