The bug
PowerOperator::Degree(VariableGroup const&) computes the total degree by summing the
per-variable degrees:
auto multideg = MultiDegree(vars);
auto deg = 0;
std::for_each(multideg.begin(), multideg.end(), [&](int n){
if (n < 0) deg = -1; else deg += n; });
return deg;
Summing is valid only for a monomial. For (x+y)^2 the degree in x is 2 and the
degree in y is 2, but the total degree is 2, not 4. The overcount scales with the
number of variables appearing in the base:
| expression |
true degree |
reported |
x^4 |
4 |
4 |
(x+y)^2 |
2 |
4 |
(x^2+y^2)^2 |
4 |
8 |
(x^2+y^2+z^2+3)^2 |
4 |
12 |
Why it stayed hidden
The same expression takes two different paths. An integer exponent written in C++ or Python
builds an IntegerPowerOperator, whose Degree(VariableGroup) is
base_deg * exponent and has always been right. The classic-input parser builds the
generic PowerOperator, which has this bug.
Both print identically, and both give the correct ungrouped Degrees(). Only
Degrees(Variables()) differs — and that is exactly what System::DegreeBound() calls.
S = System with f0 = (x+y)^2 built via Python's **
P = bertini.parse.system("variable_group x, y;\nfunction f0;\nf0 = (x+y)^2;\n")
str(S) == str(P) # both print f_0 = (x+y)^2
list(S.degrees()) == list(P.degrees()) # both [2]
list(S.degrees(vg)), list(P.degrees(vg)) # [2] vs [4] <-- here
Why it matters
DegreeBound() feeds AMP. So a system round-tripped through classic input tracks with a
different degree bound than the one it was built from — silently, with no error, in the
direction of demanding more precision than necessary. Any records/replay layer that stores
systems as classic-input text therefore replays them under different adaptive-precision
decisions than the original run, which undermines reproducibility.
Fix
Give Degree(VariableGroup) the same shape as PowerOperator::Degree(std::shared_ptr<Variable>)
directly above it, which is already correct: analyse the exponent, and use the base's
group degree in place of its degree in one variable —
return base_deg * exponent for a non-negative integer exponent, -1 for a variable or
negative or non-integer exponent, 0 when the base is constant with respect to the group.
Fixed on fix/classic-input-roundtrip alongside #395 and #396, which is how it was found:
the classic-input round trip produced degreebound: 4 -> degreebound: 12 for
(x^2+y^2+z^2+3)^2-16*(x^2+y^2).
Version: 3.5.0.dev0 (develop @ 8f0cd0f).
The bug
PowerOperator::Degree(VariableGroup const&)computes the total degree by summing theper-variable degrees:
Summing is valid only for a monomial. For
(x+y)^2the degree inxis 2 and thedegree in
yis 2, but the total degree is 2, not 4. The overcount scales with thenumber of variables appearing in the base:
x^4(x+y)^2(x^2+y^2)^2(x^2+y^2+z^2+3)^2Why it stayed hidden
The same expression takes two different paths. An integer exponent written in C++ or Python
builds an
IntegerPowerOperator, whoseDegree(VariableGroup)isbase_deg * exponentand has always been right. The classic-input parser builds thegeneric
PowerOperator, which has this bug.Both print identically, and both give the correct ungrouped
Degrees(). OnlyDegrees(Variables())differs — and that is exactly whatSystem::DegreeBound()calls.Why it matters
DegreeBound()feeds AMP. So a system round-tripped through classic input tracks with adifferent degree bound than the one it was built from — silently, with no error, in the
direction of demanding more precision than necessary. Any records/replay layer that stores
systems as classic-input text therefore replays them under different adaptive-precision
decisions than the original run, which undermines reproducibility.
Fix
Give
Degree(VariableGroup)the same shape asPowerOperator::Degree(std::shared_ptr<Variable>)directly above it, which is already correct: analyse the exponent, and use the base's
group degree in place of its degree in one variable —
return base_deg * exponentfor a non-negative integer exponent,-1for a variable ornegative or non-integer exponent,
0when the base is constant with respect to the group.Fixed on
fix/classic-input-roundtripalongside #395 and #396, which is how it was found:the classic-input round trip produced
degreebound: 4->degreebound: 12for(x^2+y^2+z^2+3)^2-16*(x^2+y^2).Version:
3.5.0.dev0(develop @ 8f0cd0f).