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
34 changes: 34 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,40 @@ one inside another was answered with the empty set, a claim that it has no solut
| `"1/(2 + p^(-3)) = 1/3".Solve("p")` | `{ }` | `{ 1, -1/2 + i * 1/2 * sqrt(3), -1/2 + i * -1/2 * sqrt(3) }` |
| `"1/(1 + x^(-2)) = y".Solve("x")` | `{ }` | `{ ((-1) / (-y) - 1) ^ (-1/2), -((-1) / (-y) - 1) ^ (-1/2) }` |

### Two fractions over a shared factor are added over their least common denominator

**Silent: where the denominators of two fractions share a factor, `Simplify` adds them over the least common
multiple of the denominators, each factor to the highest power either has it.** The sum was cross-multiplied, so a
shared factor was squared and grew with every later sum before anything cancelled, and some sums were left apart. The
value is unchanged. Simplifying `(1 - tanh(x)^2) / tanh(x)` written through exponentials took 1.8 s and takes 0.15 s,
with the same answer. No issue was filed.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"1/(x+1) + 1/(x+1)^2".Simplify()` | `1 / (1 + x) + 1 / (1 + x) ^ 2` | `(2 + x) / (1 + x) ^ 2` |

### A limit reads the sign of a vanishing base on the side approached, and at an infinity

**Silent: a base tending to 0 that is positive on the side approached, to an exponent tending to a negative number,
tends to `+oo`, though the base may be negative elsewhere; and the sign of an expression tending to 0 is read at an
infinity, from its parts, and for a positive base to a real power.** The first was known only for a base never negative
anywhere, so `(1/x)^(2 ((x - 1)/x^2 - 2))` at `+oo` was read as `0^(-4)` and left unevaluated; the second left the sign
of `1/x` at `+oo` unknown, and with it `sgn(1/x)`. Where a left side is now answered and agrees with the right, a
two-sided limit that was `NaN` is that value. Found by limitcheck; no issue was filed.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"(1/x)^(2*((x - 1)/x^2 - 2))".ToEntity().Limit("x", "+oo")` | `limit((1 / x) ^ (2 * ((x - 1) / x ^ 2 - 2)), x, +oo)` | `+oo` |
| `"sin(x)^(x - 2)".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(sin(x) ^ (x - 2), x, 0)` | `+oo` |
| `"x^(x/(-1) - (pi + x))".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(x ^ (x / (-1) - (pi + x)), x, 0)` | `+oo` |
| `"sgn(1/x)".ToEntity().Limit("x", "+oo")` | `limit(sgn(1 / x), x, +oo)` | `1` |
| `"sgn(1/x)".ToEntity().Limit("x", "-oo")` | `limit(sgn(1 / x), x, -oo)` | `-1` |
| `"sgn(e^x)".ToEntity().Limit("x", "-oo")` | `limit(sgn(e ^ x), x, -oo)` | `1` |
| `"sgn(log(x, 1/2))".ToEntity().Limit("x", "+oo")` | `limit(sgn(log(x, 1/2)), x, +oo)` | `-1` |
| `"floor((x - 1/2)/x)".ToEntity().Limit("x", "+oo")` | `limit(floor((x - 1/2) / x), x, +oo)` | `0` |
| `"ln(sin(x^x))".ToEntity().Limit("x", 0, ApproachFrom.Right)` | `limitright(ln(sin(x ^ x)), x, 0)` | `ln(sin(1))` |
| `"arcsin(x)^2 / abs(x/pi)".ToEntity().Limit("x", 0)` | `NaN` | `0` |

### A polynomial with no roots in closed form is answered with all of them

**Silent: a polynomial equation with rational coefficients and no roots in closed form is answered with every root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,11 @@ partial record Powf
// A base that is never negative and vanishes, to an exponent tending to a negative
// number, tends to +oo: 0 to a negative power has no value, and abs(x)^(-3/2) at 0
// was left unevaluated from each side for it. Where the base vanishes, it does from
// above.
// above. So does one that is positive near the point on each side approached, though it
// may be negative elsewhere: (1/x)^(2 ((x - 1)/x^2 - 2)) at +oo was read as 0^(-4), which
// has no value, and left unevaluated.
if (baseLimit?.Evaled is Real { IsZero: true } && exponentLimit?.Evaled is Real { IsNegative: true, IsFinite: true }
&& IsNeverNegative(Base, x))
&& (IsNeverNegative(Base, x) || SignOnEachSide(Base, x, dist, side) is 1))
return Real.PositiveInfinity;
// A base that keeps a sign as it vanishes is real and not 0 near the point, so to a
// negative whole power it is 1 over a power tending to 0 from that sign's side: +oo for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1227,9 +1227,35 @@ private static ApproachFrom[] Sides(Entity dest, ApproachFrom side)
// those margins keeps it on the same side of zero.
if (!limit.IsZero)
return limit.IsNegative ? -1 : 1;
return SignFromVanishingOrder(expr, x, dest, side) ?? SignFromTheSlope(expr, x, dest, side);
// Near an infinity, the sign at 0 from the right after putting 1/x, or -1/x, for x,
// which is where the readers below are stated: 1/x at +oo had no sign. Not through a
// slope: that of sinh(e^(-1/x)) at 0 is a limit l'Hopital's rule took 24 seconds
// over, and sgn(sinh(e^x)) at -oo asked it. The factors are read at the infinity.
if (dest.Evaled is Real { IsFinite: false, IsNaN: false } infinity)
return SignFromVanishingOrder(expr.Substitute(x, infinity.IsNegative ? -1 / x : 1 / x), x, Integer.Zero, ApproachFrom.Right)
?? SignOfTheFactors(expr, x, dest, side);
return SignFromVanishingOrder(expr, x, dest, side) ?? SignFromTheSlope(expr, x, dest, side)
?? SignOfTheFactors(expr, x, dest, side);
}

// A product or a quotient has the product of its parts' signs: sqrt(x) * x at 0 from the
// right has the slope sqrt(x), which tends to 0, and so had no sign. A positive base to a
// real power is positive: sqrt(1/x) at +oo has no slope to read, and e^x at -oo none either.
private static int? SignOfTheFactors(Entity expr, Variable x, Entity dest, ApproachFrom side)
=> expr switch
{
Mulf(var left, var right) => SignOfTheProduct(left, right, x, dest, side),
Divf(var left, var right) => SignOfTheProduct(left, right, x, dest, side),
Powf(var @base, var exponent) when IsRealNear(exponent, x, dest, side)
=> SignNear(@base, x, dest, side) is 1 ? 1 : null,
_ => null
};

private static int? SignOfTheProduct(Entity left, Entity right, Variable x, Entity dest, ApproachFrom side)
=> SignNear(left, x, dest, side) is { } leftSign && SignNear(right, x, dest, side) is { } rightSign
? leftSign * rightSign
: null;

/// <summary>
/// The sign an expression tending to 0 keeps near a finite destination, read off the
/// limit of its slope from the destination, <c>g(x) / (x - a)</c>, or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ internal static Entity SumOfFractions(Entity expr,
return (leftNum + rightNum).InnerSimplified / leftDen;

var twoInt = ((leftNum + leftDen).Vars, (rightNum + rightDen).Vars).IntersectSequences().Any();
if (twoInt)
return (leftNum * rightDen + rightNum * leftDen).Simplify() / (rightDen * leftDen).Simplify();
else
if (!twoInt)
return expr;
// Over the least common multiple where the denominators share a factor, for the same
// reason: (e^(2x) + 1)^2 (e^(2x) - 1) and e^(2x) - 1 cross-multiplied square the
// shared factor, and each sum after it multiplies the denominators again before
// anything cancels. (1 - tanh(x)^2) / tanh(x) written through exponentials took two
// seconds to simplify, nearly all of it in two sums whose denominators had grown to
// (e^(2x) + 1)^6 (e^(2x) - 1)^3. The multiple is 0 exactly where the product is.
if (SingleQuotient.LeastCommonMultipleOfSharing(leftDen, rightDen) is { } multiple)
return (leftNum * multiple.ForA + rightNum * multiple.ForB).Simplify() / multiple.Common.Simplify();
return (leftNum * rightDen + rightNum * leftDen).Simplify() / (rightDen * leftDen).Simplify();
}

// a * b * c / (b * c * d)
Expand Down
39 changes: 39 additions & 0 deletions Sources/AngouriMath/Functions/SingleQuotient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,45 @@ private static (Entity Numerator, Entity Denominator) Of(Entity expr, List<Entit
}
}

/// <summary>
/// The least common multiple of two denominators as written, each factor to the highest
/// power either has it, with what each is multiplied by to reach it; <see langword="null"/>
/// where they share no factor other than a whole number, and their product is that multiple.
/// </summary>
internal static (Entity Common, Entity ForA, Entity ForB)? LeastCommonMultipleOfSharing(Entity a, Entity b)
{
(a, b) = (AsFactors(a), AsFactors(b));
var (_, aFactors) = Factors(a);
var (_, bFactors) = Factors(b);
if (!aFactors.Any(factor => bFactors.Any(other => other.Base == factor.Base)))
return null;
return Over(a, b, leastCommon: true);
}

/// <summary>
/// A denominator written so that a factor shared with another is written alike: a whole
/// power of a product as the product of its factors' powers, so that
/// <c>((u - 1) (u + 1))^2</c> shares <c>u - 1</c>, and each factor inner-simplified, with a
/// negative number added written as one subtracted, so that <c>u + -1</c> is <c>u - 1</c>,
/// which inner simplification leaves as it is.
/// </summary>
private static Entity AsFactors(Entity denominator)
{
Entity product = Integer.One;
foreach (var factor in Mulf.LinearChildren(denominator))
product *= factor is Powf(Mulf inner, Integer { EInteger.Sign: > 0 } power)
? Mulf.LinearChildren(inner).Aggregate((Entity)Integer.One, (built, each) => built * MathS.Pow(Written(each), power))
: factor is Powf(var @base, Integer { EInteger.Sign: > 0 } raisedTo) ? MathS.Pow(Written(@base), raisedTo) : Written(factor);
return product;

static Entity Written(Entity factor)
=> factor.InnerSimplified switch
{
Sumf(var augend, Real { IsNegative: true } negative) => (augend - (-negative)).InnerSimplified,
var simplified => simplified,
};
}

private static void Carry(Entity denominator, List<Entity>? carried)
{
if (carried is not null && denominator != Integer.One)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// 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.Core;
using Xunit;

namespace AngouriMath.Tests.Calculus
{
/// <summary>
/// A base tending to 0, to an exponent tending to a negative number, tends to +oo where the base is
/// positive on the way in. The descent knew it only for a base that is never negative anywhere, so
/// <c>(1/x)^(2 ((x - 1)/x^2 - 2))</c> at +oo was read as <c>0^(-4)</c>, which has no value, and left
/// unevaluated. A one-sided limit at 0 is taken at +oo through x = 1/x, so
/// <c>(sqrt(x) x)^(x - 2 - x^2)</c> at 0 from the right came to the same place once <c>Simplify</c>
/// wrote the exponent over its least common denominator. The sign is now read at an infinity, and for
/// a product from its factors. Found by limitcheck at <c>DEPTH=3</c>, <c>SEED=77</c>; no issue was filed.
/// </summary>
[Trait("Area", "Calculus")]
public sealed class AVanishingBaseToANegativePowerTest
{
[Theory]
[InlineData("(1/x)^(2*((x - 1)/x^2 - 2))", "+oo", ApproachFrom.BothSides)]
[InlineData("(sqrt(x) * x)^(x - 2 - x * x)", "0", ApproachFrom.Right)]
[InlineData("x^(2*(x - x^2 - 2))", "0", ApproachFrom.Right)]
public void IsPositiveInfinity(string input, string destination, ApproachFrom side)
=> Assert.Equal(Entity.Number.Real.PositiveInfinity, MathS.FromString(input).Limit("x", destination, side));

// A positive base to a real power is positive, at an infinity too, where no slope is read.
[Fact]
public void APositiveBaseToARealPowerIsPositiveAtAnInfinity()
=> Assert.Equal(Entity.Number.Integer.One, MathS.FromString("sgn(e^x)").Limit("x", "-oo", ApproachFrom.Right));

[Fact]
public void ThatSignIsTakenThroughAFunction()
=> Assert.Equal(MathS.FromString("sinh(1)").EvalNumerical(),
MathS.FromString("sinh(sgn(e^x))").Limit("x", "-oo").EvalNumerical());

// Read at 0 after putting -1/x for x, sinh(e^x)'s slope is that of a flat function, and asking
// it took 24 seconds before the limit was declined.
[Fact]
public void ASignNotReadAtAnInfinityIsDeclinedPromptly()
{
var limit = System.Threading.Tasks.Task.Run(() => MathS.FromString("sgn(sinh(e^x))").Limit("x", "-oo"));
Assert.True(limit.Wait(System.TimeSpan.FromSeconds(10)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,19 @@ public void TendsToAnInfinity(string input, ApproachFrom side, string expected)
public void AnOddPowerHasNoTwoSidedLimit()
=> Assert.Equal(MathS.NaN, MathS.FromString("sin(x)^(-1)").Limit("x", 0, ApproachFrom.BothSides));

// An exponent that only tends to a whole number is not one near the point, and is left alone.
[Fact]
public void AnExponentThatOnlyTendsToAWholeNumberIsLeftAlone()
=> Assert.IsType<Entity.Limitf>(MathS.FromString("sin(x)^(x - 2)").Limit("x", 0, ApproachFrom.Right));
// An exponent that only tends to a whole number is not one near the point. Under a negative base
// the power is complex there, so it is left alone rather than given the odd power's sign.
[Theory]
[InlineData("sin(x)^(x - 2)")]
[InlineData("sin(x)^(x - 1)")]
public void AnExponentThatOnlyTendsToAWholeNumberIsLeftAloneUnderANegativeBase(string input)
=> Assert.IsType<Entity.Limitf>(MathS.FromString(input).Limit("x", 0, ApproachFrom.Left));

// Under a base positive on the way in, any exponent tending to a negative number gives +oo.
[Theory]
[InlineData("sin(x)^(x - 2)")]
[InlineData("sin(x)^(x - 1)")]
public void AnExponentThatOnlyTendsToAWholeNumberTendsToPositiveInfinityUnderAPositiveBase(string input)
=> Assert.Equal(Entity.Number.Real.PositiveInfinity, MathS.FromString(input).Limit("x", 0, ApproachFrom.Right));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// 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 AngouriMath.Functions;
using Xunit;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// Two fractions added inside <c>Simplify</c> went over the product of their denominators,
/// which squares a factor the two share, and every sum after it multiplied the denominators
/// again before anything cancelled. <c>(1 - tanh(x)^2) / tanh(x)</c> written through
/// exponentials took two seconds to simplify, nearly all of it in two sums whose denominators
/// had grown to <c>(e^(2x) + 1)^6 (e^(2x) - 1)^3</c>. Where the denominators share a factor,
/// the sum now goes over their least common multiple. Found while tracing a slow limit; no
/// issue was filed.
/// </summary>
/// <remarks>
/// Checked by value: the denominator is a constant multiple of the least common multiple, and
/// the sum keeps its value. The spelling the simplifier gives either is not what is asked.
/// </remarks>
[Trait("Area", "Common")]
public sealed class ASumOfFractionsTakesTheLeastCommonDenominatorTest
{
private static readonly double[] Points = { 0.37, 1.13, -0.8 };

[Theory]
// A factor of one denominator is the other.
[InlineData("1", "(e^(2x) + 1)^2 * (e^(2x) - 1)", "x", "e^(2x) - 1", "(e^(2x) + 1)^2 * (e^(2x) - 1)")]
// A factor shared under a power of a product.
[InlineData("1", "((e^(2x) - 1) * (e^(2x) + 1))^2", "x", "(e^(2x) + 1)^2 * (e^(2x) - 1)", "(e^(2x) - 1)^2 * (e^(2x) + 1)^2")]
// The same factor written as a sum with -1 in one and a difference in the other.
[InlineData("x", "e^(2x) + -1", "1", "(e^(2x) - 1) * (e^(2x) + 1)^2", "(e^(2x) - 1) * (e^(2x) + 1)^2")]
public void GoesOverTheLeastCommonMultiple(string leftNum, string leftDen, string rightNum, string rightDen, string multiple)
{
var (ln, ld, rn, rd) = (leftNum.ToEntity(), leftDen.ToEntity(), rightNum.ToEntity(), rightDen.ToEntity());
var sum = Patterns.SumOfFractions(ln / ld + rn / rd, ln, ld, rn, rd);
var denominator = Assert.IsType<Entity.Divf>(sum).Divisor;
var ratio = denominator / multiple.ToEntity();
var first = Value(ratio, Points[0]);
foreach (var point in Points)
{
Assert.Equal(first, Value(ratio, point), 9);
Assert.Equal(Value(ln / ld + rn / rd, point), Value(sum, point), 9);
}
}

// Denominators that share nothing are still cross-multiplied, as before.
[Fact]
public void LeavesDenominatorsThatShareNothing()
{
var (ln, ld, rn, rd) = ("1".ToEntity(), "x + 1".ToEntity(), "x".ToEntity(), "x - 2".ToEntity());
var sum = Patterns.SumOfFractions(ln / ld + rn / rd, ln, ld, rn, rd);
Assert.Equal((rd * ld).Simplify(), Assert.IsType<Entity.Divf>(sum).Divisor);
}

private static double Value(Entity expr, double at)
=> (double)((Entity.Number.Real)expr.Substitute("x", at).EvalNumerical()).RealPart;
}
}
Loading