Skip to content

Commit 181a42a

Browse files
Let a numeric factor reach the coefficient when collecting like terms (#855) (#862)
CollectLikeTerms failed on the expression its own documentation cites: (x+1)^2 * (x+1)^2 came back with two x^2 terms side by side. (2 * x)^2 reduces to 4 * x^2, a product and not a power, so it fell to the exponent-of-one branch and the 4 was baked into the monomial key instead of the coefficient. A factor that reduces to a product is now split again, and an integer power of a product is distributed over its factors. Only for polynomial-shaped factors. Opening a product is what lets like terms meet and also what stops one cancelling as a whole: lifting the 1/2 out of (1/2 * sin(2t))^2 * csc(t)^2 costs the cancellation to cos(t)^2, which DoubleAngleOverSineTest is there to catch. Splitting only products that carry a number does not help, since 1/2 is one; and choosing between the collected and expanded forms by complexity cannot work, because the payoff of leaving a product intact appears only after passes that run later.
1 parent 50fc43f commit 181a42a

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,22 @@ static Entity Expand_(Entity e, int level) =>
239239
/// product of powers, and terms whose products agree are added together --
240240
/// enough to collect like terms and nothing more.
241241
/// </remarks>
242+
/// <summary>
243+
/// Whether every node of the expression is one that monomial collection describes:
244+
/// numbers, variables, and the operations that build a polynomial out of them.
245+
/// </summary>
246+
/// <remarks>
247+
/// Opening a product to reach its numeric factor is what lets like terms meet, and
248+
/// it is also what stops a product cancelling as a whole -- lifting the 1/2 out of
249+
/// <c>(1/2 * sin(2t))^2 * csc(t)^2</c> costs the cancellation to <c>cos(t)^2</c>.
250+
/// Collection is a statement about polynomials, so it opens polynomials and leaves
251+
/// anything carrying a function intact for the rules that do know it.
252+
/// https://github.com/asc-community/AngouriMath/issues/855
253+
/// </remarks>
254+
private static bool IsPolynomialShaped(Entity expression)
255+
=> expression.Nodes.All(node =>
256+
node is Number or Variable or Mulf or Powf or Sumf or Minusf or Divf);
257+
242258
private static Entity CollectLikeTerms(Entity expanded)
243259
{
244260
if (expanded is not Sumf and not Minusf)
@@ -261,16 +277,39 @@ private static Entity CollectLikeTerms(Entity expanded)
261277
var exponents = new Dictionary<string, Entity>();
262278
var bases = new Dictionary<string, Entity>();
263279

264-
foreach (var factor in Mulf.LinearChildren(term))
280+
var pending = new Stack<Entity>(Mulf.LinearChildren(term));
281+
while (pending.Count > 0)
265282
{
266283
// PowerRules folds (x^2)^2 into x^4, without which the two would be
267284
// counted as different monomials.
268-
var reduced = factor.Rewrite(RewriteRules.Power).InnerSimplified;
285+
var reduced = pending.Pop().Rewrite(RewriteRules.Power).InnerSimplified;
269286
if (reduced is Number)
270287
{
271288
coefficient = (coefficient * reduced).InnerSimplified;
272289
continue;
273290
}
291+
// Reducing a factor can turn it into a product -- (2 * x)^2 becomes
292+
// 4 * x^2 -- and taken whole that is a monomial of its own, keyed on
293+
// "4 * x ^ 2" and unable to meet the plain x^2 terms it belongs with.
294+
// Split it again so the 4 reaches the coefficient. Each child is
295+
// smaller than the product it came from, so this terminates.
296+
// https://github.com/asc-community/AngouriMath/issues/855
297+
if (reduced is Mulf && IsPolynomialShaped(reduced))
298+
{
299+
foreach (var inner in Mulf.LinearChildren(reduced))
300+
pending.Push(inner);
301+
continue;
302+
}
303+
// The same one shape out: (2 * x * y)^2 is a power of a product that
304+
// the rules above do not distribute. Only for an integer exponent,
305+
// where (a * b)^n = a^n * b^n holds for every a and b; for any other
306+
// exponent it is a statement about branches.
307+
if (reduced is Powf(Mulf product, Integer wholePower) && IsPolynomialShaped(product))
308+
{
309+
foreach (var inner in Mulf.LinearChildren(product))
310+
pending.Push(inner.Pow(wholePower));
311+
continue;
312+
}
274313
var (@base, exponent) = reduced is Powf(var b, var e) ? (b, e) : (reduced, (Entity)1);
275314
var key = @base.Stringize();
276315
bases[key] = @base;
@@ -327,6 +366,7 @@ private static Entity CollectLikeTerms(Entity expanded)
327366
if (collected.Nodes.Any(node => node == MathS.NaN)
328367
&& !expanded.Nodes.Any(node => node == MathS.NaN))
329368
return expanded;
369+
330370
return collected;
331371
}
332372

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,40 @@ public void ATermThatTakesNoPowerIsNotLost(string input)
9191
Assert.NotEqual(MathS.NaN, input.ToEntity().Simplify());
9292
}
9393

94+
// Collecting keys a term on its monomial, so a numeric factor has to reach the
95+
// coefficient rather than stay inside the key. A factor that reduces to a product
96+
// -- (2 * x)^2 becomes 4 * x^2 -- or that is a power of one -- (2 * x * y)^2 --
97+
// was taken whole, keyed on "4 * x ^ 2", and could not meet the plain x^2 terms it
98+
// belonged with. https://github.com/asc-community/AngouriMath/issues/855
99+
[Theory]
100+
[InlineData("(x+1)^2 * (x+1)^2", "1 + 4 * x + 6 * x ^ 2 + 4 * x ^ 3 + x ^ 4")]
101+
[InlineData("(x+1)^4", "1 + 4 * x + 6 * x ^ 2 + 4 * x ^ 3 + x ^ 4")]
102+
[InlineData("(2x+1)^2 * (2x+1)^2", "1 + 8 * x + 24 * x ^ 2 + 32 * x ^ 3 + 16 * x ^ 4")]
103+
[InlineData("(3x)^2 + x^2", "10 * x ^ 2")]
104+
public void ANumericFactorReachesTheCoefficient(string input, string expected) =>
105+
Assert.Equal(expected.ToEntity(), input.ToEntity().Expand());
106+
107+
// Compared as printed rather than as trees: the two agree in value and in every
108+
// coefficient and differ only in how the products associate.
109+
[Fact]
110+
public void APowerOfAProductIsSplitOverItsFactors() =>
111+
Assert.Equal("y ^ 4 + 4 * x * y ^ 3 + 6 * x ^ 2 * y ^ 2 + 4 * x ^ 3 * y + x ^ 4",
112+
"(x+y)^2 * (x+y)^2".ToEntity().Expand().Stringize());
113+
114+
// A term carrying a function is left whole, because opening it costs a cancellation
115+
// the trigonometric rules would otherwise make: (1/2 * sin(2t))^2 * csc(t)^2
116+
// collapses to cos(t)^2 while it is intact and does not once the 1/2 is lifted out.
117+
[Fact]
118+
public void ATermCarryingAFunctionIsLeftWhole()
119+
{
120+
// The domain condition is stripped the way DoubleAngleOverSineTest does: the
121+
// cosecant makes this undefined where sin(x) is 0, which is a true statement
122+
// about the expression and not what this test is about.
123+
var simplified = "(sin(2 * x) * cosec(x)) ^ 2 / 4 - cos(2 * x) - sin(x) ^ 2".ToEntity().Simplify();
124+
while (simplified is Entity.Providedf(var inner, _)) simplified = inner;
125+
Assert.Equal(Entity.Number.Integer.Create(0), simplified);
126+
}
127+
94128
// The sets that can be shifted elementwise still are -- the guard above must not
95129
// turn collecting off for them.
96130
[Theory]

0 commit comments

Comments
 (0)