Skip to content

Commit 5a55cb5

Browse files
Stop Expand losing a term that takes no power (#851) (#852)
Expand turned RR + 1 into NaN, and with it ZZ + 1, CC + 1, QQ + 1, BB + 1 and true + 1. A 2.0 regression: 1.4.0 leaves all of them alone. Expand is public, so this was not only a Simplify artefact. CollectLikeTerms reduces every factor to a base and an exponent and then puts the term back together. A factor that appeared once came back as base^1, and raising to the first power is an identity only where the power is defined at all -- RR^1, ZZ^1 and true^1 are NaN. A factor that appeared once now goes back as itself, and a second guard discards any collection that introduced NaN, which covers the same fault above the first power. Sets that can be shifted still are: { 1, 2 } + 1 is { 2, 3 }. Found by running CSharpMath's suite against 2.0.0-preview.1 -- 955 green on 1.4.0, two red on the preview. Rebuilt against this change it is one.
1 parent 0f73690 commit 5a55cb5

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,17 @@ private static Entity CollectLikeTerms(Entity expanded)
290290
coefficients[monomialKey] = coefficient;
291291
Entity monomial = 1;
292292
foreach (var key in exponents.Keys.OrderBy(k => k, System.StringComparer.Ordinal))
293-
monomial = (monomial * bases[key].Pow(exponents[key])).InnerSimplified;
293+
{
294+
// A factor that appeared once goes back as itself, not as base^1.
295+
// Raising to the first power is an identity only where the power is
296+
// defined at all: RR^1, ZZ^1 and true^1 are all NaN, so writing the
297+
// factor that way turns an expression that had a value into one that
298+
// does not. https://github.com/asc-community/AngouriMath/issues/851
299+
var factor = exponents[key] == Integer.Create(1)
300+
? bases[key]
301+
: bases[key].Pow(exponents[key]);
302+
monomial = (monomial * factor).InnerSimplified;
303+
}
294304
monomials[monomialKey] = monomial;
295305
}
296306
}
@@ -306,7 +316,18 @@ private static Entity CollectLikeTerms(Entity expanded)
306316
var term = (coefficients[key] * monomials[key]).InnerSimplified;
307317
result = result == Integer.Create(0) ? term : result + term;
308318
}
309-
return result.InnerSimplified;
319+
var collected = result.InnerSimplified;
320+
321+
// Collecting is an improvement, never a requirement, so a collection that
322+
// introduced NaN is discarded rather than returned. The decomposition into
323+
// coefficient and monomial only describes a node that multiplies and takes
324+
// powers; a factor of any other kind -- a set raised above the first power,
325+
// say -- reassembles into something undefined. Keeping the expanded form is
326+
// always sound. https://github.com/asc-community/AngouriMath/issues/851
327+
if (collected.Nodes.Any(node => node == MathS.NaN)
328+
&& !expanded.Nodes.Any(node => node == MathS.NaN))
329+
return expanded;
330+
return collected;
310331
}
311332

312333
/// <summary>

‎Sources/Tests/UnitTests/Common/ExpandCollectsTermsTest.cs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,34 @@ public void ExpansionPreservesValue(string input)
6969
public void CancellingTermsKeepTheirDomainCondition() =>
7070
Assert.Equal("0 provided not x = 0".ToEntity(),
7171
"(4a - 2) / (2x) + (1 - 2a) / x".ToEntity().Simplify());
72+
73+
// Collecting reduces each factor to a base and an exponent and then puts it back
74+
// together. Writing a factor that appeared once as base^1 is an identity only where
75+
// the power is defined at all -- RR^1 and true^1 are NaN -- so a term of a kind that
76+
// does not take powers used to come back undefined.
77+
// https://github.com/asc-community/AngouriMath/issues/851
78+
[Theory]
79+
[InlineData("RR + 1")]
80+
[InlineData("1 + RR")]
81+
[InlineData("RR + x")]
82+
[InlineData("ZZ + 1")]
83+
[InlineData("QQ + 1")]
84+
[InlineData("CC + 1")]
85+
[InlineData("BB + 1")]
86+
[InlineData("true + 1")]
87+
[InlineData("RR * RR + 1")]
88+
public void ATermThatTakesNoPowerIsNotLost(string input)
89+
{
90+
Assert.NotEqual(MathS.NaN, input.ToEntity().Expand());
91+
Assert.NotEqual(MathS.NaN, input.ToEntity().Simplify());
92+
}
93+
94+
// The sets that can be shifted elementwise still are -- the guard above must not
95+
// turn collecting off for them.
96+
[Theory]
97+
[InlineData("{ 1, 2 } + 1", "{ 2, 3 }")]
98+
[InlineData("[0; 1] + 1", "[1; 2]")]
99+
public void ASetThatCanBeShiftedStillIs(string input, string expected) =>
100+
Assert.Equal(expected.ToEntity(), input.ToEntity().Simplify());
72101
}
73102
}

0 commit comments

Comments
 (0)