Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,22 @@ static Entity Expand_(Entity e, int level) =>
/// product of powers, and terms whose products agree are added together --
/// enough to collect like terms and nothing more.
/// </remarks>
/// <summary>
/// Whether every node of the expression is one that monomial collection describes:
/// numbers, variables, and the operations that build a polynomial out of them.
/// </summary>
/// <remarks>
/// Opening a product to reach its numeric factor is what lets like terms meet, and
/// it is also what stops a product cancelling as a whole -- lifting the 1/2 out of
/// <c>(1/2 * sin(2t))^2 * csc(t)^2</c> costs the cancellation to <c>cos(t)^2</c>.
/// Collection is a statement about polynomials, so it opens polynomials and leaves
/// anything carrying a function intact for the rules that do know it.
/// https://github.com/asc-community/AngouriMath/issues/855
/// </remarks>
private static bool IsPolynomialShaped(Entity expression)
=> expression.Nodes.All(node =>
node is Number or Variable or Mulf or Powf or Sumf or Minusf or Divf);

private static Entity CollectLikeTerms(Entity expanded)
{
if (expanded is not Sumf and not Minusf)
Expand All @@ -261,16 +277,39 @@ private static Entity CollectLikeTerms(Entity expanded)
var exponents = new Dictionary<string, Entity>();
var bases = new Dictionary<string, Entity>();

foreach (var factor in Mulf.LinearChildren(term))
var pending = new Stack<Entity>(Mulf.LinearChildren(term));
while (pending.Count > 0)
{
// PowerRules folds (x^2)^2 into x^4, without which the two would be
// counted as different monomials.
var reduced = factor.Rewrite(RewriteRules.Power).InnerSimplified;
var reduced = pending.Pop().Rewrite(RewriteRules.Power).InnerSimplified;
if (reduced is Number)
{
coefficient = (coefficient * reduced).InnerSimplified;
continue;
}
// Reducing a factor can turn it into a product -- (2 * x)^2 becomes
// 4 * x^2 -- and taken whole that is a monomial of its own, keyed on
// "4 * x ^ 2" and unable to meet the plain x^2 terms it belongs with.
// Split it again so the 4 reaches the coefficient. Each child is
// smaller than the product it came from, so this terminates.
// https://github.com/asc-community/AngouriMath/issues/855
if (reduced is Mulf && IsPolynomialShaped(reduced))
{
foreach (var inner in Mulf.LinearChildren(reduced))
pending.Push(inner);
continue;
}
// The same one shape out: (2 * x * y)^2 is a power of a product that
// the rules above do not distribute. Only for an integer exponent,
// where (a * b)^n = a^n * b^n holds for every a and b; for any other
// exponent it is a statement about branches.
if (reduced is Powf(Mulf product, Integer wholePower) && IsPolynomialShaped(product))
{
foreach (var inner in Mulf.LinearChildren(product))
pending.Push(inner.Pow(wholePower));
continue;
}
var (@base, exponent) = reduced is Powf(var b, var e) ? (b, e) : (reduced, (Entity)1);
var key = @base.Stringize();
bases[key] = @base;
Expand Down Expand Up @@ -327,6 +366,7 @@ private static Entity CollectLikeTerms(Entity expanded)
if (collected.Nodes.Any(node => node == MathS.NaN)
&& !expanded.Nodes.Any(node => node == MathS.NaN))
return expanded;

return collected;
}

Expand Down
34 changes: 34 additions & 0 deletions Sources/Tests/UnitTests/Common/ExpandCollectsTermsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ public void ATermThatTakesNoPowerIsNotLost(string input)
Assert.NotEqual(MathS.NaN, input.ToEntity().Simplify());
}

// Collecting keys a term on its monomial, so a numeric factor has to reach the
// coefficient rather than stay inside the key. A factor that reduces to a product
// -- (2 * x)^2 becomes 4 * x^2 -- or that is a power of one -- (2 * x * y)^2 --
// was taken whole, keyed on "4 * x ^ 2", and could not meet the plain x^2 terms it
// belonged with. https://github.com/asc-community/AngouriMath/issues/855
[Theory]
[InlineData("(x+1)^2 * (x+1)^2", "1 + 4 * x + 6 * x ^ 2 + 4 * x ^ 3 + x ^ 4")]
[InlineData("(x+1)^4", "1 + 4 * x + 6 * x ^ 2 + 4 * x ^ 3 + x ^ 4")]
[InlineData("(2x+1)^2 * (2x+1)^2", "1 + 8 * x + 24 * x ^ 2 + 32 * x ^ 3 + 16 * x ^ 4")]
[InlineData("(3x)^2 + x^2", "10 * x ^ 2")]
public void ANumericFactorReachesTheCoefficient(string input, string expected) =>
Assert.Equal(expected.ToEntity(), input.ToEntity().Expand());

// Compared as printed rather than as trees: the two agree in value and in every
// coefficient and differ only in how the products associate.
[Fact]
public void APowerOfAProductIsSplitOverItsFactors() =>
Assert.Equal("y ^ 4 + 4 * x * y ^ 3 + 6 * x ^ 2 * y ^ 2 + 4 * x ^ 3 * y + x ^ 4",
"(x+y)^2 * (x+y)^2".ToEntity().Expand().Stringize());

// A term carrying a function is left whole, because opening it costs a cancellation
// the trigonometric rules would otherwise make: (1/2 * sin(2t))^2 * csc(t)^2
// collapses to cos(t)^2 while it is intact and does not once the 1/2 is lifted out.
[Fact]
public void ATermCarryingAFunctionIsLeftWhole()
{
// The domain condition is stripped the way DoubleAngleOverSineTest does: the
// cosecant makes this undefined where sin(x) is 0, which is a true statement
// about the expression and not what this test is about.
var simplified = "(sin(2 * x) * cosec(x)) ^ 2 / 4 - cos(2 * x) - sin(x) ^ 2".ToEntity().Simplify();
while (simplified is Entity.Providedf(var inner, _)) simplified = inner;
Assert.Equal(Entity.Number.Integer.Create(0), simplified);
}

// The sets that can be shifted elementwise still are -- the guard above must not
// turn collecting off for them.
[Theory]
Expand Down
Loading