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
26 changes: 26 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,32 @@ quotient or whole power with a matrix in it to GenericTensor's operation of that
| `"[[x, 2 x], [x + 1, x^2]]".ToEntity().Compile<double, GenTensor<double, DoubleOperations>>("x")` | `UncompilableNodeException` | a delegate; at `3` the matrix `[[3, 6], [4, 9]]` |
| `"A * B".ToEntity().Compile<GenTensor<…>, GenTensor<…>, GenTensor<…>>("A", "B")` | `UncompilableNodeException` | the matrix product |
| `"[[1, 0]] * [[a, b], [c, d]] * [[0], [1]]".ToEntity().Compile<double, double, double, double, double>("a", "b", "c", "d")` | `2` at `(1, 2, 3, 4)` — simplified to the scalar first | unchanged |
### A product is written letters before functions

`Simplify` wrote `y * sin(x)` as `sin(x) * y` and `x * e^x` as `e ^ x * x`: a product came out in
the order the simplifier sorts by, which puts a sine before every letter. By hand the number comes
first, then the letters and their powers, then the functions: DLMF 5.5.1 is `Γ(z + 1) = z Γ(z)`,
and ISO 80000-2 lets a function's argument go without parentheses (`sin nπ`), so a letter written
after a function reads as part of its argument. The answer of `Simplify` is now written that way:
the number, the letters and their powers, then everything else -- a function, an exponential such
as `e^x` or `2^n`, a sum -- each kind in the order it had, with a leading number taken into a
quotient over a number (`2 / s`, not `2 * 1 / s`). Only the answer is reordered: the simplifier
still sorts a sine before a letter, because the rules that combine factors meet them as neighbours
in that order, and `InnerSimplified` and `Differentiate` return that order as before.
[#1628](https://github.com/asc-community/AngouriMath/issues/1628), Algebrite's issue 29 from
[#180](https://github.com/asc-community/AngouriMath/issues/180). Both columns measured on a build,
`v2.5.0` against this change.

| `"….".ToEntity().Simplify()` of | Was (2.5.0) | Is |
|---|---|---|
| `sin(x) * y` | `sin(x) * y` | `y * sin(x)` |
| `x * e ^ x` | `e ^ x * x` | `x * e ^ x` |
| `2 * sin(x) * y` | `2 * sin(x) * y` | `2 * y * sin(x)` |
| `a / sin(x)` | `csc(x) * a` | `a * csc(x)` |
| `a * log(x) + y * sin(x)` | `sin(x) * y + a * log(10, x)` | `y * sin(x) + a * log(10, x)` |
| `2 * x * sin(x) * cos(x)` | `sin(2 * x) * x` | `x * sin(2 * x)` |
| `derivative(arcsin(2 * x), x)` | `1 / sqrt(1 - (2 * x) ^ 2) * 2` | `2 / sqrt(1 - (2 * x) ^ 2)` |

### `π` is `pi` and the script `ℯ` is `e`

`π` (U+03C0) was a free variable, a Greek letter like any other, so `sin(π)` did not simplify; the
Expand Down
93 changes: 92 additions & 1 deletion Sources/AngouriMath/Functions/Simplification/Simplificator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,100 @@ internal static Entity Simplify(Entity expr, int level)
// run `x/sin(x)` for a minute where it declined in half a second.
// https://github.com/asc-community/AngouriMath/issues/1394
var mark = recording?.Mark() ?? 0;
return Noted(recording, simplified, WithoutTheConditionsItStates(simplified), "ConditionsTheExpressionStates", mark);
var stated = Noted(recording, simplified, WithoutTheConditionsItStates(simplified), "ConditionsTheExpressionStates", mark);
mark = recording?.Mark() ?? 0;
return Noted(recording, stated, InPrintOrder(stated), "ProductsInPrintOrder", mark);
}

/// <summary>
/// The answer with each product's factors in the order a product is written by hand: the
/// number, then the letters and their powers, then the rest -- a function, an exponential,
/// a sum -- each kind in the order it had. So <c>2 y sin(x)</c> and <c>x e^x</c>, not
/// <c>2 sin(x) y</c> and <c>e^x x</c>: the references write it so (DLMF 5.5.1 is
/// <c>Γ(z + 1) = z Γ(z)</c>), and ISO 80000-2 lets a function's argument go without
/// parentheses, <c>sin nπ</c>, so a letter written after a function reads as part of its
/// argument. https://github.com/asc-community/AngouriMath/issues/1628
/// </summary>
/// <remarks>
/// Only the answer is put in this order. The canonical order the search sorts by puts a
/// sine before every letter, and the rules that combine factors are binary: they meet
/// <c>2 cos(x) sin(x)</c> as neighbours only because nothing sorts between them. Sorting
/// letters first inside the search put <c>x</c> there, so <c>2 x sin(x) cos(x)</c> no
/// longer became <c>x sin(2x)</c>, l'Hopital's rule saw its quotients grow a step sooner
/// and <c>lim 1/x^2 - 1/sin(x)^2</c> at 0 was <c>NaN</c> where it is <c>-1/3</c>. Reordering a
/// product changes no node count, so nothing that ranks candidates sees it, and a caller
/// simplifying the answer again has it sorted back before any rule reads it.
/// </remarks>
internal static Entity InPrintOrder(Entity expression)
=> expression.Replace(static node => node is Entity.Mulf product ? FactorsInPrintOrder(product) : node);

private static Entity FactorsInPrintOrder(Entity.Mulf product)
{
// Read before anything is built: this runs on every answer, and most products are
// in order already.
var highest = 0;
if (InOrder(product, ref highest))
return product;
var factors = new System.Collections.Generic.List<Entity>();
Gather(product, factors);
var ordered = new System.Collections.Generic.List<Entity>(factors.Count);
for (var rank = 0; rank <= 2; rank++)
foreach (var factor in factors)
if (FactorRank(factor) == rank)
ordered.Add(factor);
// A number brought to the front of a quotient over a number is that quotient's
// numerator: 2 * (1 / s) is written 2 / s rather than 2 * 1 / s.
if (ordered.Count > 1 && ordered[0] is Entity.Number number
&& ordered[1] is Entity.Divf(Entity.Number numerator, var divisor))
{
ordered[1] = new Entity.Divf((number * numerator).InnerSimplified, divisor);
ordered.RemoveAt(0);
}
Entity rebuilt = ordered[0];
for (var j = 1; j < ordered.Count; j++)
rebuilt = new Entity.Mulf(rebuilt, ordered[j]);
return rebuilt;

static bool InOrder(Entity node, ref int highest)
{
if (node is Entity.Mulf(var left, var right))
return InOrder(left, ref highest) && InOrder(right, ref highest);
var rank = FactorRank(node);
if (rank < highest)
return false;
highest = rank;
return true;
}

static void Gather(Entity node, System.Collections.Generic.List<Entity> into)
{
if (node is Entity.Mulf(var left, var right))
{
Gather(left, into);
Gather(right, into);
}
else
into.Add(node);
}
}

/// <summary>0 for a number, 1 for a letter or a power of one, 2 for anything else.</summary>
/// <remarks>
/// A power with a number for its exponent ranks as its base, so <c>x^2</c> and
/// <c>sqrt(x)</c> are letters and <c>sqrt(2)</c> is a number. A letter raised to a letter
/// is still a power of that letter, as in <c>a x^n</c>; but <c>e</c> raised to anything
/// but a number is the exponential function, and so is a number raised to a letter:
/// <c>x e^x</c>, <c>n 2^n</c>.
/// </remarks>
private static int FactorRank(Entity factor) => factor switch
{
Entity.Number => 0,
Entity.Variable => 1,
Entity.Powf(var @base, Entity.Number) => FactorRank(@base),
Entity.Powf(Entity.Variable @base, _) when @base != Entity.Variable.e => 1,
_ => 2
};

/// <summary>
/// <paramref name="expression"/> with every conjunct of its condition that the
/// expression states on its own taken off. At the top only: a case of a piecewise is
Expand Down
6 changes: 3 additions & 3 deletions Sources/Tests/UnitTests/Calculus/DerivativeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void TestSin()
public void TestCosCustom()
{
var func = MathS.Cos(MathS.Pow(x, 3));
var expected = -3 * MathS.Sin(MathS.Pow(x, 3)) * MathS.Sqr(x);
var expected = -3 * MathS.Sqr(x) * MathS.Sin(MathS.Pow(x, 3));
var actual = func.Differentiate(x).Simplify();
Assert.Equal(expected, actual);
}
Expand Down Expand Up @@ -81,13 +81,13 @@ public void TestArc1()
public void TestArc2()
{
var func = MathS.Arcsin(2 * x);
Assert.Equal(1 / MathS.Sqrt(1 - MathS.Sqr(2 * x)) * 2, func.Differentiate(x).Simplify());
Assert.Equal(2 / MathS.Sqrt(1 - MathS.Sqr(2 * x)), func.Differentiate(x).Simplify());
}
[Fact]
public void TestArc3()
{
var func = MathS.Arccos(2 * x);
Assert.Equal((-1) / MathS.Sqrt(1 - MathS.Sqr(2 * x)) * 2, func.Differentiate(x).Simplify());
Assert.Equal((-2) / MathS.Sqrt(1 - MathS.Sqr(2 * x)), func.Differentiate(x).Simplify());
}
[Fact]
public void TestArc4()
Expand Down
4 changes: 2 additions & 2 deletions Sources/Tests/UnitTests/Calculus/IntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public sealed class IntegrationTest
[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("x cos(x)", "cos(x) + x * sin(x) + C")]
[InlineData("sin(x)cos(x)", "sin(x) ^ 2 / 2 + C")]
[InlineData("ln(x)", "x * (ln(x) - 1) + C")]
[InlineData("log(a, x)", "C + x * (ln(x) - 1) / ln(a)")]
Expand Down Expand Up @@ -333,7 +333,7 @@ public void TestLnAbsSquared()
// ln(abs(x)) uncombined and the 2 * x outside the bracket.
// (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());
Assert.Equal("C + x * (2 + ln(abs(x)) ^ 2 + (-2) * ln(abs(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: 2 additions & 2 deletions Sources/Tests/UnitTests/Common/LambdaCalculusTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ [Fact] public void EtaReduction6()
=> Assert.Equal(
@"lambda(x,
apply(
(34 + sin(x)) * a,
a * (34 + sin(x)),
x
)
)
Expand All @@ -194,7 +194,7 @@ [Fact] public void EtaReduction6()
);
[Fact] public void EtaReduction7()
=> Assert.Equal(
@"(34 + sin(y)) * a".ToEntity(),
@"a * (34 + sin(y))".ToEntity(),
@"lambda(x,
apply(
a * (sin(y) + 34),
Expand Down
2 changes: 1 addition & 1 deletion Sources/Tests/UnitTests/Core/BinomialIdentityTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void AnIdentityIsTrue(string identity)
[InlineData("binomial(n - 1, k) + binomial(n - 1, k - 1)", "binomial(n, k)")]
[InlineData("binomial(n, n - 2)", "binomial(n, 2)")]
[InlineData("binomial(n, n - k)", "binomial(n, k)")]
[InlineData("n * binomial(n - 1, k - 1)", "binomial(n, k) * k")]
[InlineData("n * binomial(n - 1, k - 1)", "k * binomial(n, k)")]
[InlineData("binomial(7, 3) + binomial(7, 2)", "56")]
public void ARewriteCollects(string input, string expected)
=> Assert.Equal(expected.ToEntity(), input.ToEntity().Simplify());
Expand Down
2 changes: 1 addition & 1 deletion Sources/Tests/UnitTests/Core/Sets/IntervalImageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void TheImageOfAnInterval(string expression, string image)
[InlineData("[1; 2] / [-1; 1]")]
[InlineData("arcsin([0; 2])")]
[InlineData("sin([0; 1])")]
[InlineData("(0; 1) * k")]
[InlineData("k * (0; 1)")]
[InlineData("[a; b]^2")]
public void LeftAsWritten(string expression)
=> Assert.Equal(expression.ToEntity(), expression.ToEntity().Simplify());
Expand Down
4 changes: 2 additions & 2 deletions Sources/Tests/UnitTests/Core/Sets/IntervalScalingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,12 @@ public void MembershipIsWhatTheReflectionIsFor(string expression, bool expected)
expression.ToEntity().Simplify());

/// <summary>
/// <b>An unknown sign is answered by not answering.</b> <c>(0; 1) * k</c> is one interval
/// <b>An unknown sign is answered by not answering.</b> <c>k * (0; 1)</c> is one interval
/// when <c>k</c> is positive and the reflected one when it is negative; picking either
/// would be choosing which, so it is left alone — which is what an unevaluated node means.
/// </summary>
[Theory]
[InlineData("(0; 1) * k")]
[InlineData("k * (0; 1)")]
[InlineData("(0; 1) / k")]
public void AnUnknownSignIsLeftAlone(string expression)
=> Assert.Equal(expression.ToEntity(), expression.ToEntity().Simplify());
Expand Down
64 changes: 64 additions & 0 deletions Sources/Tests/UnitTests/PatternsTest/FactorOrderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// 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.Extensions;
using Xunit;

namespace AngouriMath.Tests.PatternsTest
{
/// <summary>
/// A product is written the way it is by hand: the number, then the letters and their powers,
/// then the functions, so <c>y sin(x)</c> and <c>x e^x</c>. Algebrite's issue 29, from #180.
/// https://github.com/asc-community/AngouriMath/issues/1628
/// </summary>
[Trait("Area", "PatternsTest")]
public sealed class FactorOrderTest
{
[Theory]
[InlineData("sin(x) * y", "y * sin(x)")]
[InlineData("sin(x) * b", "b * sin(x)")]
[InlineData("y * 2 * sin(x)", "2 * y * sin(x)")]
[InlineData("log(x) * z", "z * log(10, x)")]
[InlineData("sin(x) * x ^ 2", "x ^ 2 * sin(x)")]
[InlineData("a * log(x) + y * sin(x)", "y * sin(x) + a * log(10, x)")]
public void LettersComeBeforeFunctions(string input, string expected)
=> Assert.Equal(expected, input.ToEntity().Simplify().Stringize());

/// <summary>
/// An exponential is a function, whether its base is <c>e</c> or a number; a letter raised
/// to a letter is a power of that letter.
/// </summary>
[Theory]
[InlineData("e ^ x * x", "x * e ^ x")]
[InlineData("2 ^ n * n", "n * 2 ^ n")]
[InlineData("x ^ n * a", "a * x ^ n")]
public void AnExponentialIsAFunctionAndAPowerOfALetterIsALetter(string input, string expected)
=> Assert.Equal(expected, input.ToEntity().Simplify().Stringize());

/// <summary>
/// Only the answer is put in this order. The rules that combine factors still meet them in
/// the order the search sorts by, where <c>2 cos(x) sin(x)</c> are neighbours: sorting the
/// letters first inside the search put <c>x</c> between them, and this stopped being
/// <c>x sin(2x)</c>.
/// </summary>
[Theory]
[InlineData("2 * x * sin(x) * cos(x)", "x * sin(2 * x)")]
[InlineData("x * (x - 1)!", "x!")]
public void TheRulesStillMeetTheFactorsTheyCombine(string input, string expected)
=> Assert.Equal(expected, input.ToEntity().Simplify().Stringize());

/// <summary>The order is the tree's, so the printed form parses back to the same product.</summary>
[Theory]
[InlineData("sin(x) * y")]
[InlineData("e ^ x * x * 3")]
public void ThePrintedFormReadsBackAsTheSameProduct(string input)
{
var simplified = input.ToEntity().Simplify();
Assert.Equal(simplified, simplified.Stringize().ToEntity());
}
}
}
2 changes: 1 addition & 1 deletion Sources/Tests/UnitTests/PatternsTest/FractionSimplify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public sealed class FractionSimplify
[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("(4a - 2) / (2x) + (1 - 2a) / x", "0 provided not x = 0")]
[InlineData("sin(a) * a * b / a", "sin(a) * b provided not a = 0")] // #311
[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")]
public void TestSimplify(string testeeRaw, string expectedRaw)
{
Expand Down
8 changes: 4 additions & 4 deletions Sources/Tests/UnitTests/PatternsTest/SimplifyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@ [Fact] public void BigSimple1() => AssertSimplifyToString(
[InlineData("sec(x) * cos(x)", "1 provided not cos(x) = 0")]
[InlineData("sin(x) / cos(x)", "tan(x)")]
[InlineData("cos(x) / sin(x)", "cotan(x)")]
[InlineData("a / sec(x)", "cos(x) * a")]
[InlineData("a / sin(x)", "csc(x) * a")]
[InlineData("a / csc(x)", "sin(x) * a")]
[InlineData("a / cos(x)", "sec(x) * a")]
[InlineData("a / sec(x)", "a * cos(x)")]
[InlineData("a / sin(x)", "a * csc(x)")]
[InlineData("a / csc(x)", "a * sin(x)")]
[InlineData("a / cos(x)", "a * sec(x)")]
[InlineData("arcsec(1 / x)", "arccos(x)")]
[InlineData("arccsc(1 / x)", "arcsin(x)")]
[InlineData("arcsin(1 / x)", "arccsc(x)")]
Expand Down
10 changes: 5 additions & 5 deletions Sources/Tests/UnitTests/PatternsTest/SortSimplifyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ public sealed class SortSimplifyTest
// [InlineData("sin(arcsin(c x) + arccos(x c) + c)2 + a + b + sin(x) + 0 + cos(c - -arcsin(c x) - -arccos(-c x * (-1)))2", "1")]
// [InlineData("sin(arcsin(c x) + arccos(x c) + c)2 + a + b + sin(x) + 0 + cos(c - -arcsin(c x) - -arccos(-c x * (-1)))2", ") ^ 2", false)]
[InlineData("sec(x) + a + sin(x) + c + 1 + 0 + 3 + sec(x)", "4 + 2 * sec(x) + sin(x) + a + c")]
[InlineData("tan(x) * a * b / c / sin(h + 0.1) * cotan(x)", "a * b / (sin(h + 1/10) * c) provided not sin(x) = 0 and not cos(x) = 0")]
[InlineData("sin(x) * a * b / c / sin(h + 0.1) * cosec(x)", "a * b / (sin(h + 1/10) * c) provided not sin(x) = 0")]
[InlineData("cos(x) * a * b / c / sin(h + 0.1) * sec(x)", "a * b / (sin(h + 1/10) * c) provided not cos(x) = 0")]
[InlineData("sec(x) * a * b / c / sin(h + 0.1) * cos(x)", "a * b / (sin(h + 1/10) * c) provided not cos(x) = 0")]
[InlineData("cosec(x) * a * b / c / sin(h + 0.1) * sin(x)", "a * b / (sin(h + 1/10) * c) provided not sin(x) = 0")]
[InlineData("tan(x) * a * b / c / sin(h + 0.1) * cotan(x)", "a * b / (c * sin(h + 1/10)) provided not sin(x) = 0 and not cos(x) = 0")]
[InlineData("sin(x) * a * b / c / sin(h + 0.1) * cosec(x)", "a * b / (c * sin(h + 1/10)) provided not sin(x) = 0")]
[InlineData("cos(x) * a * b / c / sin(h + 0.1) * sec(x)", "a * b / (c * sin(h + 1/10)) provided not cos(x) = 0")]
[InlineData("sec(x) * a * b / c / sin(h + 0.1) * cos(x)", "a * b / (c * sin(h + 1/10)) provided not cos(x) = 0")]
[InlineData("cosec(x) * a * b / c / sin(h + 0.1) * sin(x)", "a * b / (c * sin(h + 1/10)) provided not sin(x) = 0")]
public void Test(string exprRaw, string result)
{
var entity = exprRaw.ToEntity();
Expand Down
Loading