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
15 changes: 15 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,21 @@ taken through its decimal, which had already rounded it onto the integer beside
| `"round(10^120 + 3/2)".ToEntity().Evaled` | `10^120` | `10^120 + 2` |
| `"round(+oo)".ToEntity().Evaled` | throws `OverflowException` | `+oo` |

### A factor a numerator's terms hold to a power cancels

**Silent: a quotient whose numerator's terms each hold a factor of the denominator, as it is or to a whole power,
is cancelled by it.** A term holding the factor only to a power was not read as holding it, so
`(x (y + 1) + (y + 1)^2) / (y + 1)^4` was left as written while `(x (y + 1) + (y + 1)) / (y + 1)^2` cancelled. The
quotient rule squares the denominator at each derivative, so a Taylor expansion of `e^x / (1 + x^2)` took its seventh
derivative in 5,117 nodes, and takes it in 149: 9 terms took 8 s and take 1 s
([#2301](https://github.com/asc-community/AngouriMath/issues/2301)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"(x * (y + 1) + (y + 1)^2) / (y + 1)^4".ToEntity().Simplify()` | `(x * (y + 1) + (y + 1) ^ 2) / (y + 1) ^ 4` | `(x + y + 1) / (y + 1) ^ 3` |
| `"(x * (x^2 + 1) + (x^2 + 1)^2) / (x^2 + 1)^4".ToEntity().Simplify()` | `(x * (x ^ 2 + 1) + (x ^ 2 + 1) ^ 2) / (x ^ 2 + 1) ^ 4` | `(x ^ 2 + 1 + x) / (x ^ 2 + 1) ^ 3` |
| `"(a * (x^2 + 1) + b * (x^2 + 1)^2) / (x^2 + 1)^3".ToEntity().Simplify()` | `(b * (x ^ 2 + 1) + a) / (x ^ 4 + 2 * x ^ 2 + 1) provided not x ^ 2 + 1 = 0` | `(a + b * (x ^ 2 + 1)) / (x ^ 2 + 1) ^ 2` |

### A squared sine and cosine with one coefficient are found anywhere in a sum

**Silent: an identity reaches its value however the sum is written.** `k sin(x)^2 + k cos(x)^2` was reduced to `k`
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/DivisionByZeroOverDoubleTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Common/AFactorHeldToAPowerCancelsTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Common/HyperbolicTangentPastTheRangeTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,75 @@ internal static Entity FractionCommonDenominatorRules(Entity expr, TreeAnalyzer.
/// </summary>
internal static Entity PairwiseGroupedQuotient(
Entity whole, Entity num, Entity den, TreeAnalyzer.SortLevel level)
=> PairwiseGrouping(num, den, level).Select(PowerRules).MultiplyAll()
=> PairwiseGrouping(WithAFactorOfTheOtherOut(num, den), WithAFactorOfTheOtherOut(den, num), level)
.Select(PowerRules).MultiplyAll()
.InnerSimplified.Replace(CollapseMultipleFractions);

/// <summary>
/// <paramref name="sum"/> with a factor of <paramref name="other"/> taken out of it, where
/// every term of the sum holds that factor, as it is or to a whole power, so that the
/// pairing meets it: <c>x (y + 1) + (y + 1)^2</c> beside <c>(y + 1)^4</c> is
/// <c>(y + 1) (x + y + 1)</c>. The sum as it is where no polynomial factor is in every term.
/// </summary>
/// <remarks>
/// A term that held the factor only to a power was not read as holding it, so
/// <c>(x (y + 1) + (y + 1)^2) / (y + 1)^4</c> was left as written while
/// <c>(x (y + 1) + (y + 1)) / (y + 1)^2</c> cancelled. The quotient rule squares the
/// denominator at each derivative, so the Taylor expansion of <c>e^x / (1 + x^2)</c> took
/// its derivatives over <c>(x^2 + 1)^2</c>, <c>^4</c>, <c>^8</c>.
/// https://github.com/asc-community/AngouriMath/issues/2301
/// </remarks>
private static Entity WithAFactorOfTheOtherOut(Entity sum, Entity other)
{
if (sum is not (Sumf or Minusf))
return sum;
var terms = Sumf.LinearChildren(sum);
foreach (var candidate in Mulf.LinearChildren(other))
{
var factor = candidate is Powf(var @base, Integer { IsPositive: true }) ? @base : candidate;
if (factor is Number || !factor.Vars.Any() || !IsAPolynomial(factor))
continue;
var lowest = terms.Min(term => PowerOf(factor, term));
if (lowest < 1)
continue;
return MathS.Pow(factor, lowest) * Sumf.Sum(terms.Select(term => Without(factor, lowest, term)).ToList());
}
return sum;

// A polynomial factor only. A power of a sine has a cancellation of its own, through the
// opened angles and a common denominator, and taking it out first left the reporter's
// second expression of https://github.com/asc-community/AngouriMath/issues/557 short of 0.
static bool IsAPolynomial(Entity factor)
=> factor.Nodes.All(node => node is Number or Variable or Sumf or Minusf or Mulf
|| node is Powf(_, Integer { IsPositive: true }));

// How many times the factor is in the product, counting a whole power of it as that many.
static int PowerOf(Entity factor, Entity term)
=> Mulf.LinearChildren(term).Sum(child =>
child == factor ? 1
: child is Powf(var b, Integer { IsPositive: true } k) && b == factor && k.EInteger.CanFitInInt32() ? k.EInteger.ToInt32Checked()
: 0);

// The product with that many of the factor taken off it.
static Entity Without(Entity factor, int count, Entity term)
{
var kept = new List<Entity>();
foreach (var child in Mulf.LinearChildren(term))
{
var here = child == factor ? 1
: child is Powf(var b, Integer { IsPositive: true } k) && b == factor && k.EInteger.CanFitInInt32() ? k.EInteger.ToInt32Checked()
: 0;
var taken = System.Math.Min(here, count);
count -= taken;
if (here - taken > 0)
kept.Add(here - taken == 1 ? factor : MathS.Pow(factor, here - taken));
else if (here == 0)
kept.Add(child);
}
return kept.Count == 0 ? Integer.One : kept.Aggregate((left, right) => left * right);
}
}

/// <summary>n^(p/q) for a q of 2 or more -- a root that is not a whole power.</summary>
private static bool IsSurd(Entity node)
=> node is Powf(_, Rational and not Integer);
Expand Down
55 changes: 55 additions & 0 deletions Sources/Tests/UnitTests/Common/AFactorHeldToAPowerCancelsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using System.Linq;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// A factor that every term of a quotient's numerator holds, as it is or to a whole power, is taken out of the
/// numerator and paired off against the denominator. A term holding it only to a power was not read as holding it,
/// so <c>(x (y + 1) + (y + 1)^2) / (y + 1)^4</c> was left as written while <c>(x (y + 1) + (y + 1)) / (y + 1)^2</c>
/// cancelled. <a href="https://github.com/asc-community/AngouriMath/issues/2301">#2301</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class AFactorHeldToAPowerCancelsTest
{
[Theory]
[InlineData("(x * (y + 1) + (y + 1)^2) / (y + 1)^4", "(x + y + 1) / (y + 1) ^ 3")]
[InlineData("(x * (x^2 + 1) + (x^2 + 1)^2) / (x^2 + 1)^4", "(x ^ 2 + 1 + x) / (x ^ 2 + 1) ^ 3")]
[InlineData("(x * (y + 1) + (y + 1)) / (y + 1)^2", "(x + 1) / (y + 1)")]
public void TheFactorCancels(string quotient, string expected)
{
var simplified = quotient.ToEntity().Simplify();
// As printed: the answer's sums are grouped as Simplify builds them, not as the parser does.
Assert.Equal(expected, simplified.Stringize());
foreach (var (x, y) in new[] { (2.0, 0.5), (-1.5, 3.0), (0.25, -0.75) })
{
var before = quotient.ToEntity().Substitute("x", x).Substitute("y", y).EvalNumerical();
var after = simplified.Substitute("x", x).Substitute("y", y).EvalNumerical();
Assert.True((before - after).Abs() < 1e-9, $"{after} where the quotient is {before} at x = {x}, y = {y}");
}
}

/// <summary>
/// The quotient rule squares the denominator at each derivative, and what the new numerator shares with it was
/// not taken back: the seventh derivative of <c>e^x / (1 + x^2)</c>, simplified at each step, had 5,117 nodes,
/// and has 149 once the factor cancels.
/// </summary>
[Fact]
public void TheDerivativesOfAQuotientStaySmall()
{
Entity derivative = "e^x / (1 + x^2)";
for (var i = 0; i < 7; i++)
derivative = derivative.Differentiate("x").Simplify();
Assert.True(derivative.Nodes.Count() < 500, $"{derivative.Nodes.Count()} nodes: {derivative}");
}
}
}
Loading