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
19 changes: 19 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ except where shown.
| `([[1, 2], [2, 4]] ^ (-1)).Evaled` | throws `InvalidDeterminantException` | `[[1, 2], [2, 4]] ^ (-1)` |
| `([[1, 2], [2, 4]] ^ (-1)).Simplify()` | throws `InvalidDeterminantException` | `1 / [[1, 2], [2, 4]]` |

### A sum of fractions that is a constant over one denominator simplifies to it

**Silent: an identity in fractions reaches its value, and conditions are written shorter.** `Simplify` gathered
fractions a pair or a sort level at a time and left some sums apart that are a constant over one denominator. It now
also offers the rational canonical form (`Transformation.RationalCanonicalization`) as a candidate, where the
expression is a rational function with a quotient in it and of complexity at most 64, and not while the integrator is
running. The candidate wins only where it rates shorter, and a tie keeps the old answer. Where it wins, the condition
is the canonical one: square-free, and without what the answer's own denominator already excludes
([#2137](https://github.com/asc-community/AngouriMath/issues/2137)).

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

### A polynomial system is not answered with some of its solutions missing

**Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,14 @@ internal static class RationalFunction
/// <see langword="false"/> where it is not a rational function over <c>Q</c> in its
/// free variables, or where a bound is reached.
/// </summary>
internal static bool TryCanonicalize(Entity expr, [NotNullWhen(true)] out Entity? canonical)
/// <param name="expr">The expression.</param>
/// <param name="canonical">Its canonical form.</param>
/// <param name="wholeCoefficients">Whether the quotient is scaled at the end so that its
/// denominator has whole coprime coefficients and a positive leading one, rather than a
/// leading one of 1: <c>2/(4x^2 + 1)</c>, not <c>(1/2)/(x^2 + 1/4)</c>. The same quotient
/// either way, and still one tree per function; the monic form is the specification's,
/// and this is the one written for a reader, as <c>Simplify</c> offers it.</param>
internal static bool TryCanonicalize(Entity expr, [NotNullWhen(true)] out Entity? canonical, bool wholeCoefficients = false)
{
canonical = null;
if (expr.Complexity > MaxComplexity)
Expand Down Expand Up @@ -140,6 +147,11 @@ internal static bool TryCanonicalize(Entity expr, [NotNullWhen(true)] out Entity
denominator = denominator.ScaleBy(inverse);
}

if (wholeCoefficients && !denominator.IsConstant)
{
denominator = denominator.Normalized(out var whole);
numerator = numerator.ScaleBy(whole);
}
// The denominator is now monic, so a constant one is exactly 1 and is dropped.
var quotient = denominator.IsConstant
? numerator.ToEntity(variables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
/// </remarks>
internal static bool AnsweringTheQuestionAsked => descentDepth == 1;

/// <summary>
/// Whether the integrator is running on this thread: for a <c>Simplify</c> candidate that
/// is wanted by its callers and not by the integrator's rules, which read the shape of what
/// <c>Simplify</c> returns.
/// </summary>
internal static bool IsIntegrating => descentDepth > 0;

/// <summary>
/// The question asked or one step below it: the remainder by parts leaves, the
/// integrand a substitution makes. For a rewriting rule that lands on the chain and is
Expand Down
59 changes: 59 additions & 0 deletions Sources/AngouriMath/Functions/Simplification/Simplificator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,68 @@ not solve this issue completely and yet too slow to be accepted.
}
}

// A rational function's canonical form, offered as one more candidate. The passes
// above gather fractions a pair or a sort level at a time, which reaches most sums of
// fractions and not all: x/(x + y) + y/(x + y) stayed as it was, and so did
// 1/(1 - (b/a)^2) - a^2/(a^2 - b^2), whose canonical forms are 1 and 0 with their
// conditions. RationalCanonicalization decides every such identity, and the complexity
// metric decides whether its quotient is the better answer, which as an expanded
// quotient it often is not. Last, so that a tie goes to the form the rules reached;
// with whole coefficients, so that it reads 2/(4x^2 + 1) and not (1/2)/(x^2 + 1/4);
// and only where there is a quotient to gather, since a polynomial's canonical form is
// its expansion, which is offered above. That is asked by a walk that allocates
// nothing: SimplifyEasy, which has a logarithm in it and is never a rational function,
// allocated 3.4% more when it was asked through Nodes. Only up to a complexity of 64,
// well above the 11 to 39 of the identities this is for, so that a large quotient is
// not gathered for a form that will mostly lose. And not while the integrator is
// running: its rules read the shape of what Simplify hands back, and a gathered
// quotient that rated shorter sent 1/(b cosh x + c sinh x + sqrt(b^2 - c^2))^4 down a
// route that took 4.8 s to an answer of 737 characters, where master's took 1.1 s to
// one of 427.
// https://github.com/asc-community/AngouriMath/issues/2137
var canonicalMark = recording?.Mark() ?? 0;
if (!Algebra.Integration.IsIntegrating && stage1.Complexity <= 64 && IsARationalFunctionWithAQuotient(stage1)
&& RationalFunction.TryCanonicalize(stage1, out var canonical, wholeCoefficients: true))
AddHistory(Noted(recording, stage1, canonical, "RationalCanonicalization", canonicalMark));

return history.Values.SelectMany(x => x);
}

/// <summary>
/// Whether <paramref name="expr"/> is made of numbers, variables, sums, differences,
/// products, quotients and whole powers alone, with a quotient or a negative power among
/// them: the expressions <see cref="RationalFunction.TryCanonicalize"/> may gather into
/// one quotient.
/// </summary>
private static bool IsARationalFunctionWithAQuotient(Entity expr)
{
var quotient = false;
return IsARationalFunction(expr, ref quotient) && quotient;
}

private static bool IsARationalFunction(Entity node, ref bool quotient)
{
switch (node)
{
case Number or Variable:
return true;
case Divf(var dividend, var divisor):
quotient = true;
return IsARationalFunction(dividend, ref quotient) && IsARationalFunction(divisor, ref quotient);
case Powf(var @base, Integer exponent):
quotient |= exponent.EInteger.Sign < 0;
return IsARationalFunction(@base, ref quotient);
case Sumf(var left, var right):
return IsARationalFunction(left, ref quotient) && IsARationalFunction(right, ref quotient);
case Minusf(var left, var right):
return IsARationalFunction(left, ref quotient) && IsARationalFunction(right, ref quotient);
case Mulf(var left, var right):
return IsARationalFunction(left, ref quotient) && IsARationalFunction(right, ref quotient);
default:
return false;
}
}

/// <summary>
/// Sorts an expression into a polynomial.
/// See more at <see cref="MathS.TryPolynomial"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ public void Issue403_FractionsOverACommonDenominatorCollapse(string input, strin
// The general case still brings unlike denominators together, which needs the
// numerator fully simplified to reach 0.
[Fact]
// The condition is the square-free part of (b - 1)(1 - b), which vanishes at the same
// points, since #2137 offers the rational canonical form as a candidate.
public void Issue403_UnlikeDenominatorsStillCombine() =>
Assert.Equal("0 provided not (b - 1) * (1 - b) = 0".ToEntity(),
Assert.Equal("0 provided not b - 1 = 0".ToEntity(),
"(a - 1) / (1 - b) - (1 - a) / (b - 1)".ToEntity().Simplify());

// Three separate defects sat on top of each other in the integrator,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// 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 AngouriMath;
using AngouriMath.Extensions;
using Xunit;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// <c>Simplify</c> gathered fractions a pair or a sort level at a time, and left some sums of
/// fractions apart that are a constant over one denominator. The rational canonical form
/// decides them, and is offered as a candidate since
/// <a href="https://github.com/asc-community/AngouriMath/issues/2137">#2137</a>; the first
/// row of <a href="https://github.com/asc-community/AngouriMath/issues/2133">#2133</a> is the
/// same shape.
/// </summary>
[Trait("Area", "Common")]
public sealed class SumOfFractionsOverOneDenominatorTest
{
[Theory]
[InlineData("x/(x + y) + y/(x + y)", 1)]
[InlineData("1/(1 - (b/a)^2) - a^2/(a^2 - b^2)", 0)]
[InlineData("x/c + (y*c - x*d)/(c^2*(z + d/c)) - (x*z + y)/(c*z + d)", 0)]
[InlineData("R^2 - R^2*(a + b)*(1/a + 1/b)/4 - R^2*(a - b)*(1/a - 1/b)/4", 0)]
public void ItIsTheConstantItIs(string input, int constant)
{
var simplified = input.Simplify();
while (simplified is Providedf(var inner, _))
simplified = inner;
Assert.Equal(Integer.Create(constant), simplified);
}

/// <summary>
/// The condition the answer carries is the input's: defined where the input is.
/// </summary>
[Theory]
[InlineData("x/(x + y) + y/(x + y)", "x", "1", "y", "2")]
[InlineData("1/(1 - (b/a)^2) - a^2/(a^2 - b^2)", "a", "3", "b", "1")]
[InlineData("R^2 - R^2*(a + b)*(1/a + 1/b)/4 - R^2*(a - b)*(1/a - 1/b)/4", "a", "2", "b", "5")]
public void ItIsDefinedWhereTheInputIs(string input, string first, string firstValue, string second, string secondValue)
{
var at = input.Simplify().Substitute(first, firstValue.ToEntity()).Substitute(second, secondValue.ToEntity()).Substitute("R", 7);
Assert.IsAssignableFrom<Complex>(at.Evaled);
Assert.NotEqual(MathS.NaN, at.Evaled);
}
}
}
2 changes: 1 addition & 1 deletion Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public sealed class FractionSimplify
[InlineData("x/x-x/x", "0 provided not x = 0")]
[InlineData("(xy)/(xy)", "1 provided not xy = 0")]
[InlineData("0*(x+3)^-4", "0 provided not 3 + x = 0")]
[InlineData("(a - 1) / (1 - b) - (1 - a) / (b - 1)", "0 provided not (b - 1) * (1 - b) = 0")] // #254
[InlineData("(a - 1) / (1 - b) - (1 - a) / (b - 1)", "0 provided not b - 1 = 0")] // #254, and #2137 for the square-free condition
[InlineData("(4a - 2) / (2x) + (1 - 2a) / x", "0 provided not x = 0")]
[InlineData("sin(a) * a * b / a", "b * sin(a) provided not a = 0")] // #311
[InlineData("sin(a) * cos(b) * tan(c) / (tan(c)3 * sin(a)2 * cos(b)^(-2))", "csc(a) * cos(b) ^ 3 * cotan(c) ^ 2")]
Expand Down
6 changes: 4 additions & 2 deletions Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ [Fact] public void Patt8Negative() =>
// The condition used to read `not c = 0 and not a * b * c * x ^ 2 = 0`, which says
// the same thing twice: a product is nonzero exactly when each of its factors is,
// so a b c x^2 != 0 already carries c != 0. Cancelling the common divisor in one
// step states the domain once, over the same set of points.
[Fact] public void Patt12() => AssertSimplify((x * y * a * b * c) / (c * b * a * x * x), "y / x provided not a * b * c * x = 0", 4);
// step states the domain once, over the same set of points. And x is not repeated in
// the condition, since y/x already has no value at x = 0: the rational canonical form
// writes the condition without what its own denominator excludes (#2137).
[Fact] public void Patt12() => AssertSimplify((x * y * a * b * c) / (c * b * a * x * x), "y / x provided not a * b * c = 0", 4);
[Fact] public void Frac1() => AssertSimplify("x / (y / z)", "x * z / y");
[Fact] public void Frac2() => AssertSimplify("x / y / z", "x / (y * z)");
[Fact] public void Factorial2() => AssertSimplify(MathS.Factorial(2), 2);
Expand Down
Loading