Summary
MakeMovingHomotopy decides whether two moving rows are "identical" by comparing the
strings that operator<< produces for them. That stream uses the default ostream
precision — 6 significant digits — so two genuinely different linear forms that agree
to 6 digits are declared identical and the homotopy is refused:
MakeMovingHomotopy: moving row 0 (`0.735966*x+(-2408.97)+0.593277*y+(-0.890232)*z`)
is identical in start_moving and end_moving, so it does not move; put non-moving
equations in `fixed` instead.
The threshold is relative: ~1e-6 of the constant's magnitude. At coefficients of order
1 it is 1e-6 and invisible; at order 2400 it is ~2.4e-3, which is easily inside the range
of legitimately distinct slice values. So this only bites at larger coordinate scales,
which is what makes it look sporadic.
This is a false positive in a guard that aborts a valid computation.
Reproducer
import bertini, bertini.nag_algorithm as na
bertini.default_precision(30)
x, y, z = bertini.variables(list('xyz'))
fixed = bertini.System(); fixed.add_variable_group([x, y, z])
fixed.add_functions([x**2 - y**2*z])
def lin(const):
S = bertini.System(); S.add_variable_group([x, y, z])
S.add_functions([bertini.coefficient('0.735966')*x
+ bertini.coefficient('0.593277')*y
- bertini.coefficient('0.890232')*z
- bertini.coefficient(const)])
return S
for d in ('1e-12', '1e-8', '1e-5', '1e-3', '1e-2', '1e-1'):
v2 = repr(2408.97 + float(d))
try:
na.moving_homotopy(fixed, lin('2408.97'), lin(v2))
print(f"delta={d:>6} moved OK")
except RuntimeError as e:
print(f"delta={d:>6} REFUSED: {'identical' in str(e)}")
Output:
delta= 1e-12 REFUSED: True
delta= 1e-8 REFUSED: True
delta= 1e-5 REFUSED: True
delta= 1e-3 REFUSED: True <-- genuinely different rows, 1e-3 apart
delta= 1e-2 moved OK
delta= 1e-1 moved OK
A delta of 1e-3 at a constant of 2408.97 is a perfectly meaningful separation — those
are two different slices — and it is refused.
Cause
core/src/system/system.cpp (~1840):
auto function_strings = [](System const& s) {
std::vector<std::string> out;
for (auto const& f : s.NaturalFunctionsAsNodes())
{
std::ostringstream ss;
ss << f; // default precision: 6 significant digits
out.push_back(ss.str());
}
return out;
};
...
for (size_t i = 0; i < start_funcs.size(); ++i)
if (start_funcs[i] == end_funcs[i])
throw std::runtime_error("MakeMovingHomotopy: moving row ... is identical ...");
The same function_strings output also backs the check just above it (the "function appears
in both the fixed system and the moving rows" throw), so both guards share the defect: a
fixed function that merely renders like a moving one would also throw spuriously.
Why this is the wrong tool, by the project's own rule
docs/adr/0042 and the repo's own guidance are explicit that the canonical encodings are
digest preimages, never presentation, and that human-readable renders are derived views
free to change. operator<< is presentation. Using it to make an identity decision is
exactly the inversion that rule forbids — and it silently couples a correctness guard to a
formatting default.
The exact-value encoder this needs already exists in tree:
// core/include/bertini2/function_tree/canonical_encoding.hpp
std::string CanonicalEncoding(std::shared_ptr<Node> const& root);
Suggested fix
- Compare with
CanonicalEncoding(f) instead of ss << f in function_strings — exact
values, no formatting dependence. Keep operator<< for the message text, which is
presentation and appropriate there.
- Apply to both guards (the fixed-vs-moving check and the start-vs-end check).
- Consider whether string identity is the right predicate at all: two structurally different
but mathematically identical rows still pass. Evaluating start - end at a random point
would be a stronger test. At minimum, exact encoding removes the false positives.
- A regression test with two rows differing at the 1e-3 level on a ~1e3-magnitude constant
would have caught this; the scale dependence is the part that makes it easy to miss.
Impact — measured on the real failure, not just the synthetic case
Hit in a surface cell decomposition (whitney x^2 - y^2 z, a component whose bounding
sphere has radius ~1275, so projection values run to a few thousand). A slice curve's fiber
move between two adjacent critical values was refused, aborting the whole decomposition.
I caught the refusal and evaluated start_moving and end_moving at two independent random
points to check whether the rows really were identical:
trial0 row0: start = -2404.4890203631344
end = -2404.4890010320519
DIFFERENCE = -1.933108e-05 (relative 8.04e-09)
trial1 row0: start = -2406.9979659498613
end = -2406.9979466187788
DIFFERENCE = -1.933108e-05 (relative 8.03e-09)
rendered start: 0.735966*x+(-2408.97)+0.593277*y+(-0.890232)*z
rendered end : 0.735966*x+(-2408.97)+0.593277*y+(-0.890232)*z
The two rows differ by a consistent 1.93e-05 and render identically. The caller was moving
between two distinct slices; the guard asserted otherwise and killed the run.
Environment
- b2
VERSION 3.5.0.dev0, branch feature/eval-precision-tolerant @ c24bdc2e
- installed dist
bertini2 2.0.2
- Python 3.14.5, Linux aarch64
Summary
MakeMovingHomotopydecides whether two moving rows are "identical" by comparing thestrings that
operator<<produces for them. That stream uses the defaultostreamprecision — 6 significant digits — so two genuinely different linear forms that agree
to 6 digits are declared identical and the homotopy is refused:
The threshold is relative: ~1e-6 of the constant's magnitude. At coefficients of order
1 it is 1e-6 and invisible; at order 2400 it is ~2.4e-3, which is easily inside the range
of legitimately distinct slice values. So this only bites at larger coordinate scales,
which is what makes it look sporadic.
This is a false positive in a guard that aborts a valid computation.
Reproducer
Output:
A delta of
1e-3at a constant of2408.97is a perfectly meaningful separation — thoseare two different slices — and it is refused.
Cause
core/src/system/system.cpp(~1840):The same
function_stringsoutput also backs the check just above it (the "function appearsin both the fixed system and the moving rows" throw), so both guards share the defect: a
fixed function that merely renders like a moving one would also throw spuriously.
Why this is the wrong tool, by the project's own rule
docs/adr/0042and the repo's own guidance are explicit that the canonical encodings aredigest preimages, never presentation, and that human-readable renders are derived views
free to change.
operator<<is presentation. Using it to make an identity decision isexactly the inversion that rule forbids — and it silently couples a correctness guard to a
formatting default.
The exact-value encoder this needs already exists in tree:
Suggested fix
CanonicalEncoding(f)instead ofss << finfunction_strings— exactvalues, no formatting dependence. Keep
operator<<for the message text, which ispresentation and appropriate there.
but mathematically identical rows still pass. Evaluating
start - endat a random pointwould be a stronger test. At minimum, exact encoding removes the false positives.
would have caught this; the scale dependence is the part that makes it easy to miss.
Impact — measured on the real failure, not just the synthetic case
Hit in a surface cell decomposition (whitney
x^2 - y^2 z, a component whose boundingsphere has radius ~1275, so projection values run to a few thousand). A slice curve's fiber
move between two adjacent critical values was refused, aborting the whole decomposition.
I caught the refusal and evaluated
start_movingandend_movingat two independent randompoints to check whether the rows really were identical:
The two rows differ by a consistent
1.93e-05and render identically. The caller was movingbetween two distinct slices; the guard asserted otherwise and killed the run.
Environment
VERSION3.5.0.dev0, branchfeature/eval-precision-tolerant@c24bdc2ebertini22.0.2