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
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ private Entity InnerSimplifyWithCheck(bool isExact)
/// y ^ 8 + 8 * x * y ^ 7 + 28 * x ^ 2 * y ^ 6 + 56 * x ^ 3 * y ^ 5 + 70 * x ^ 4 * y ^ 4 + 56 * x ^ 5 * y ^ 3 + 28 * x ^ 6 * y ^ 2 + 8 * x ^ 7 * y + x ^ 8
/// </code>
/// </example>

public Entity Expand(int level = 2)
{
static Entity Expand_(Entity e, int level) =>
Expand All @@ -214,7 +215,89 @@ static Entity Expand_(Entity e, int level) =>
expChildren.AddRange(exp);
else
return this; // if one is too complicated, return the current one
return Expand_(TreeAnalyzer.MultiHangBinary(expChildren, (a, b) => new Sumf(a, b)), level).InnerSimplified;
return CollectLikeTerms(
Expand_(TreeAnalyzer.MultiHangBinary(expChildren, (a, b) => new Sumf(a, b)), level).InnerSimplified);
}

/// <summary>
/// Adds up the terms of an expanded sum that differ only by a numeric factor, so
/// that expanding actually finishes: <c>(x+1)^2 * (x+1)^2</c> multiplied out gives
/// sixteen terms, of which only five are distinct.
/// </summary>
/// <remarks>
/// Deliberately not <see cref="Simplify(int)"/>, which is far too expensive to run
/// inside <see cref="Expand(int)"/>. Each term is reduced to a coefficient and a
/// product of powers, and terms whose products agree are added together --
/// enough to collect like terms and nothing more.
/// </remarks>
private static Entity CollectLikeTerms(Entity expanded)
{
if (expanded is not Sumf and not Minusf)
return expanded;

// A term carrying a domain condition must not be folded into another: the
// conditions are what say where the answer holds, and adding coefficients
// together loses them. (4a - 2)/(2x) + (1 - 2a)/x is 0 only where x is not 0,
// and collecting it to a plain 0 would drop exactly that.
if (expanded.Nodes.Any(node => node is Providedf))
return expanded;

var coefficients = new Dictionary<string, Entity>();
var monomials = new Dictionary<string, Entity>();
var order = new List<string>();

foreach (var term in Sumf.LinearChildren(expanded))
{
Entity coefficient = 1;
var exponents = new Dictionary<string, Entity>();
var bases = new Dictionary<string, Entity>();

foreach (var factor in Mulf.LinearChildren(term))
{
// PowerRules folds (x^2)^2 into x^4, without which the two would be
// counted as different monomials.
var reduced = factor.Replace(Functions.Patterns.PowerRules).InnerSimplified;
if (reduced is Number)
{
coefficient = (coefficient * reduced).InnerSimplified;
continue;
}
var (@base, exponent) = reduced is Powf(var b, var e) ? (b, e) : (reduced, (Entity)1);
var key = @base.Stringize();
bases[key] = @base;
exponents[key] = exponents.TryGetValue(key, out var already)
? (already + exponent).InnerSimplified
: exponent;
}

var monomialKey = string.Join(" ", (IEnumerable<string>)exponents.Keys.OrderBy(k => k, System.StringComparer.Ordinal)
.Select(k => k + "^" + exponents[k].Stringize()));

if (coefficients.TryGetValue(monomialKey, out var running))
coefficients[monomialKey] = (running + coefficient).InnerSimplified;
else
{
order.Add(monomialKey);
coefficients[monomialKey] = coefficient;
Entity monomial = 1;
foreach (var key in exponents.Keys.OrderBy(k => k, System.StringComparer.Ordinal))
monomial = (monomial * bases[key].Pow(exponents[key])).InnerSimplified;
monomials[monomialKey] = monomial;
}
}

Entity result = 0;
foreach (var key in order)
{
// A term whose coefficients cancelled is still written out rather than
// dropped. Where the monomial is defined everywhere it simplifies to 0 and
// costs nothing; where it is not -- x^-1, say -- InnerSimplified turns
// 0 * x^-1 into `0 provided not x = 0`, and dropping the term would have
// thrown that condition away. (4a - 2)/(2x) + (1 - 2a)/x is the case.
var term = (coefficients[key] * monomials[key]).InnerSimplified;
result = result == Integer.Create(0) ? term : result + term;
}
return result.InnerSimplified;
}

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions Sources/Tests/UnitTests/Calculus/IntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class IntegrationTest
[InlineData("2x", "C + x ^ 2")]
[InlineData("x2", "x ^ 3 / 3 + C")]
[InlineData("x2 + x", "x ^ 3 / 3 + x ^ 2 / 2 + C")]
[InlineData("x2 - x", "x ^ 3 / 3 - x ^ 2 / 2 + C")]
[InlineData("x2 - x", "C + x ^ 3 / 3 - x ^ 2 / 2")]
[InlineData("a / x", "a * ln(abs(x)) + C")]
[InlineData("x cos(x)", "cos(x) + sin(x) * x + C")]
[InlineData("sin(x)cos(x)", "sin(x) ^ 2 / 2 + C")]
Expand Down Expand Up @@ -252,7 +252,9 @@ public void TestLnAbsSquared()
// (ln|x| - 1)^2 + 1 = ln^2|x| - 2ln|x| + 2, so this is the antiderivative
// written above. Term collection used to stop short, leaving the repeated
// ln(abs(x)) uncombined and the 2 * x outside the bracket.
Assert.Equal("C + ((ln(abs(x)) - 1) ^ 2 + 1) * x", result.Stringize());
// (ln|x| - 1)^2 + 1 multiplied out is ln^2|x| - 2ln|x| + 2, which is what
// Expand now returns, having collected the like terms it used to leave apart.
Assert.Equal("C + (2 + ln(abs(x)) ^ 2 + (-2) * ln(abs(x))) * x", result.Stringize());

// Verify the result by differentiation
var derivative = result.Differentiate("x"); // TODO: Make this simplify to expr with Simplify()
Expand Down
4 changes: 3 additions & 1 deletion Sources/Tests/UnitTests/Common/ExpandCollapseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ [Fact] public void Factorize1() =>
public void Factorial()
{
var expr = MathS.Factorial(x + 3) / MathS.Factorial(x + 1);
Assert.Equal(MathS.Pow(x, 2) + x * 3 + (2 * x + 6), expr.Expand());
// Expand adds up like terms now, so 3x and 2x arrive as 5x rather than
// side by side.
Assert.Equal(MathS.Pow(x, 2) + 5 * x + 6, expr.Expand());
expr = MathS.Factorial(x + -3) / MathS.Factorial(x + -1);
Assert.Equal(1 / (x + -2) / (x + -1), expr.Expand());
}
Expand Down
72 changes: 72 additions & 0 deletions Sources/Tests/UnitTests/Common/ExpandCollectsTermsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// Expand adds up the terms it produces that differ only by a numeric factor.
/// </summary>
public sealed class ExpandCollectsTermsTest
{
// https://github.com/asc-community/AngouriMath/issues/164
// Reported as "cannot be expanded": multiplying it out gave sixteen terms, of
// which only five are distinct.
[Fact]
public void Issue164_RepeatedFactorExpands() =>
Assert.Equal("1 + 4 * x + 6 * x ^ 2 + 4 * x ^ 3 + x ^ 4",
"(x+1)^2*(x+2-1)^2".ToEntity().Expand().Stringize());

// The maintainer asked for this one on the issue as well.
[Fact]
public void Issue164_CubeOfAProductExpands() =>
Assert.Equal("8 + (-36) * x + 66 * x ^ 2 + (-63) * x ^ 3 + 33 * x ^ 4 + (-9) * x ^ 5 + x ^ 6",
"((x-1)*(x-2))^3".ToEntity().Expand().Stringize());

[Theory]
[InlineData("(x + 1) ^ 2", "1 + 2 * x + x ^ 2")]
[InlineData("(x + 1) ^ 3", "1 + 3 * x + 3 * x ^ 2 + x ^ 3")]
[InlineData("x * (x + 1)", "x ^ 2 + x")]
public void OrdinaryProductsCollect(string input, string expected) =>
Assert.Equal(expected, input.ToEntity().Expand().Stringize());

// Nothing may be collected across different variables.
[Fact]
public void UnlikeTermsAreLeftApart() =>
Assert.Equal("a * c + a * d + b * c + b * d", "(a+b)*(c+d)".ToEntity().Expand().Stringize());

// Whatever comes out has to be the same expression.
[Theory]
[InlineData("(x+1)^2*(x+2-1)^2")]
[InlineData("((x-1)*(x-2))^3")]
[InlineData("(x + 1) ^ 3")]
[InlineData("(x + y) ^ 2")]
[InlineData("(x + 1) ^ 2 * (x + 2)")]
public void ExpansionPreservesValue(string input)
{
var before = input.ToEntity();
var after = before.Expand();
foreach (var point in new[] { 0.37, 1.41, -0.8 })
{
var a = before.Substitute("x", point).Substitute("y", point + 0.5).EvalNumerical();
var b = after.Substitute("x", point).Substitute("y", point + 0.5).EvalNumerical();
Assert.Equal(a.RealPart.EDecimal.ToDouble(), b.RealPart.EDecimal.ToDouble(), 9);
}
}

// Terms whose coefficients cancel are still written out, because the monomial may
// carry a domain condition that dropping the term would throw away: this is zero
// only where x is not.
[Fact]
public void CancellingTermsKeepTheirDomainCondition() =>
Assert.Equal("0 provided not x = 0".ToEntity(),
"(4a - 2) / (2x) + (1 - 2a) / x".ToEntity().Simplify());
}
}
3 changes: 2 additions & 1 deletion Sources/Tests/UnitTests/Core/PolyParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public void TestLinear3()
{
if (MathS.Utils.TryGetPolyLinear("a x - b + 3x + x", "x", out var a, out var b))
{
AssertEntityEqual("a + 3 + 1", a);
// 3 and 1 are added together now that Expand collects like terms.
AssertEntityEqual("a + 4", a);
AssertEntityEqual("-b", b);
}
else
Expand Down
Loading