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
32 changes: 32 additions & 0 deletions Sources/AngouriMath/Functions/Continuous/Limits/Transformations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,41 @@ private static Entity ApplySecondRemarkable(Entity expr, Variable x, Entity dest
EvalAssumingContinuous((xPlusOne - 1).Limit(x, dest)) == 0 && DivergesInMagnitude(xPower, x, dest) =>
MathS.e.Pow(xPower * (xPlusOne - 1)),

// a(x)^n / b(x)^n, the same limit written as a quotient. The simplifier used
// to gather this into (a/b)^n for us, which is how the shape reached the rule
// above at all -- and that gathering is false across the branch cuts, since
// sqrt(2)/sqrt(-3) is -0.8165i where (2/-3)^(1/2) is +0.8165i.
// https://github.com/asc-community/AngouriMath/issues/802
//
// Read here instead, it is sound: a limit only needs the identity to hold in a
// neighbourhood of the destination, and both bases are required to be
// eventually positive on the way there -- where their arguments are both zero
// and there is no turn of the argument to lose. That is checkable, which is
// exactly what it is not in the simplifier, where the expression has no
// destination to be near.
Divf(Powf(var numeratorBase, var power), Powf(var denominatorBase, var powerAgain)) when
power == powerAgain && numeratorBase.ContainsNode(x) && denominatorBase.ContainsNode(x)
&& power.ContainsNode(x)
&& IsEventuallyPositive(numeratorBase, x, dest) && IsEventuallyPositive(denominatorBase, x, dest)
&& EvalAssumingContinuous((numeratorBase / denominatorBase - 1).Limit(x, dest)) == 0
&& DivergesInMagnitude(power, x, dest) =>
MathS.e.Pow(power * (numeratorBase / denominatorBase - 1)),

_ => expr
};

/// <summary>
/// Whether a base stays positive on the approach to <paramref name="dest"/>, which is
/// what makes gathering a quotient of two powers into one power sound *here* when it
/// is not sound in general: two positive bases have argument zero apiece, so their
/// quotient's argument cannot leave the principal branch.
/// </summary>
private static bool IsEventuallyPositive(Entity expr, Variable x, Entity dest)
{
var limit = EvalAssumingContinuous(expr.Limit(x, dest));
return limit == Real.PositiveInfinity || limit is Real { IsPositive: true };
}

/// <summary>
/// How many times over <see cref="ApplySecondRemarkable"/> may be re-read into an
/// expression that <see cref="SimplifyAndComputeLimitToInfinity"/>'s simplification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,20 @@ expr is Sumf(var any1, Mulf(Real { IsNegative: true } const1, var any2))
|| (any1.Evaled is Real { IsPositive: true }
&& any2.Evaled is Real { IsPositive: true }))
=> new Powf(any1 * any2, any3),
// The quotient form has the same hole -- sqrt(2) / sqrt(-3) is -0.8165i while
// (2 / -3)^(1/2) is +0.8165i -- and is deliberately left unguarded, because
// guarding it costs answers rather than only shapes. It is what lets the limit
// machinery read a 1^oo out of a quotient, so `(x^2 + 1)^x / (x^2)^x` stops
// being answered at all, which is the whole of #739 and #740. Which of the two
// to prefer is a maintainer's call and is filed separately rather than taken
// here. https://github.com/asc-community/AngouriMath/issues/802
Divf(Powf(var any1, var any3), Powf(var any2, var any3a)) when any3 == any3a => new Powf(any1 / any2, any3),
// Same condition, same reason -- sqrt(2) / sqrt(-3) is -0.8165i where
// (2 / -3)^(1/2) is +0.8165i. https://github.com/asc-community/AngouriMath/issues/802
//
// This gathering was what let the limit machinery read a 1^oo out of a quotient,
// so guarding it here used to cost `(x^2 + 1)^x / (x^2)^x` its limit. The limit
// reader now recognises the quotient itself, where the identity is checkable
// because there is a destination to be near and the bases can be required to be
// positive on the way to it -- see ApplySecondRemarkable.
Divf(Powf(var any1, var any3), Powf(var any2, var any3a))
when any3 == any3a
&& (any3 is Integer
|| (any1.Evaled is Real { IsPositive: true }
&& any2.Evaled is Real { IsPositive: true }))
=> new Powf(any1 / any2, any3),

// {1} ^ n / {2} ^ (c * n) = ({1} / {2} ^ c) ^ n, and the same the other way up.
//
Expand Down
53 changes: 40 additions & 13 deletions Sources/Tests/UnitTests/Common/PowerQuotientGatheringTest.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//
//
// 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 System.Linq;
using System.Threading.Tasks;
using AngouriMath;
using AngouriMath.Extensions;
Expand All @@ -19,6 +20,19 @@ namespace AngouriMath.Tests.Common
/// <c>b^(c*p)</c> on the child, on the way up, so by the time the pair is looked at it
/// has already happened -- and where it applies to only one of the two, the exponents
/// no longer match. https://github.com/asc-community/AngouriMath/issues/740
/// <para/>
/// <b>That gathering no longer happens in <c>Simplify</c>, and these tests moved with
/// it.</b> <c>(a/b)^p</c> is not <c>a^p / b^p</c> across the branch cuts --
/// <c>sqrt(2)/sqrt(-3)</c> is <c>-0.8165i</c> where <c>(2/-3)^(1/2)</c> is
/// <c>+0.8165i</c> -- so the rule is now conditioned like the rest of its family
/// (https://github.com/asc-community/AngouriMath/issues/802).
/// <para/>
/// Nothing is lost by that, because the gathering was a *means*: #740 wanted it so the
/// limit machinery could read a <c>1^oo</c> out of a quotient. The limit reader now
/// recognises the quotient itself, where the identity is checkable -- there is a
/// destination to be near, and both bases can be required to stay positive on the way to
/// it. So the assertions below moved from the mechanism to the outcome it existed for:
/// they ask whether the limit is answered, not whether <c>Simplify</c> prints one power.
/// </summary>
[Trait("Area", "Common")]
public sealed class PowerQuotientGatheringTest
Expand Down Expand Up @@ -56,15 +70,23 @@ private static void AssertGathersIntoOnePower(string expr)
$"{expr} came back as {simplified.Stringize()}, which is not a single power");
}

/// <summary>
/// The quotients #740 was about, asked as the question it was really asking: does the
/// limit come out? The shapes it used to assert -- a single power in Simplify's
/// output -- are no longer produced, and should not be: see the note on the class.
/// </summary>
[Theory]
[InlineData("(a ^ 2 + 1) ^ x / (a ^ 2) ^ x")]
[InlineData("(x ^ 2 + 1) ^ x / (x ^ 2) ^ x")]
[InlineData("(x ^ 3 + 1) ^ x / (x ^ 3) ^ x")]
[InlineData("(a ^ 2) ^ x / b ^ x")]
[InlineData("(x ^ 2) ^ x / (x ^ 2 + 1) ^ x")]
[InlineData("x ^ (2 * a) / y ^ a")]
public void AQuotientOfPowersGathersWhenOneBaseIsItselfAPower(string expr) =>
AssertGathersIntoOnePower(expr);
[InlineData("(sqrt(x) + 1) ^ x / sqrt(x) ^ x")]
public void AQuotientOfPowersIsStillAnsweredAsALimit(string expr)
{
var limit = Task.Run(() => expr.ToEntity().Limit("x", "+oo").Simplify());
Assert.True(limit.Wait(System.TimeSpan.FromSeconds(30)), $"{expr} did not terminate");
Assert.False(limit.Result.Nodes.Any(node => node is Entity.Limitf),
$"{expr} came back unevaluated: {limit.Result.Stringize()}");
}

[Theory]
[InlineData("(a ^ 2 + 1) ^ x / (a ^ 2) ^ x")]
Expand Down Expand Up @@ -101,14 +123,19 @@ public void AQuotientWithNumericExponentsIsUnaffected(string expr) =>
Assert.False(Simplified(expr) is Entity.Powf,
$"{expr} was gathered into {Simplified(expr).Stringize()}");

// What already worked, and still does.
/// <summary>
/// These used to be asserted to gather into one power. They no longer do, because
/// their bases are symbolic and nothing can say the quotient stays on the principal
/// branch -- which is the whole of #802. What must still hold is that simplifying
/// them does not change what they are, so that is what is asserted.
/// </summary>
[Theory]
[InlineData("(y + 1) ^ x / y ^ x")]
[InlineData("a ^ x / b ^ x")]
[InlineData("(a ^ 2) ^ x / (b ^ 2) ^ x")]
[InlineData("(x + 1) ^ (2 * x) / x ^ (2 * x)")]
public void TheQuotientsThatAlreadyGatheredStillDo(string expr) =>
AssertGathersIntoOnePower(expr);
public void TheQuotientsThatNoLongerGatherKeepTheirValue(string expr) =>
AssertSameValueAt(expr, ("a", "17/10"), ("b", "23/10"), ("x", "13/10"), ("y", "31/10"));

/// <summary>
/// The limits #739 fixed go through the same gathering, so they are pinned here as
Expand Down Expand Up @@ -141,11 +168,11 @@ public void TheSecondRemarkableLimitStillReadsWhatGatheringGivesIt(
/// run ahead of it.
/// </summary>
[Fact]
public void AFractionalExponentRatioGathersOnceNothingFlattensItFirst()
public void AFractionalExponentRatioKeepsItsValue()
{
var simplified = Simplified("(sqrt(x) + 1) ^ x / sqrt(x) ^ x");
Assert.True(simplified is Entity.Powf,
$"came back as {simplified.Stringize()}");
// The gathering this used to assert is gone with #802, and the limit it was
// wanted for is covered by AQuotientOfPowersIsStillAnsweredAsALimit above. What
// is checked here is that simplifying still does not change the value.
AssertSameValueAt("(sqrt(x) + 1) ^ x / sqrt(x) ^ x", ("x", "13/10"));
}
}
Expand Down
22 changes: 11 additions & 11 deletions Sources/Tests/UnitTests/PatternsTest/PowerProductBranchTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class PowerProductBranchTest
[Theory]
[InlineData("x ^ (1/2) * y ^ (1/2)")]
[InlineData("sqrt(x) * sqrt(y)")]
[InlineData("sqrt(x) / sqrt(y)")]
[InlineData("x ^ (1/3) * y ^ (1/3)")]
[InlineData("x ^ (3/2) * y ^ (3/2)")]
public void SimplifyingKeepsTheValueAtNegativeBases(string expr)
Expand All @@ -51,25 +52,24 @@ public void SimplifyingKeepsTheValueAtNegativeBases(string expr)
}

/// <summary>
/// The quotient form has the same hole and is deliberately still open:
/// <c>sqrt(2) / sqrt(-3)</c> is <c>-0.8165i</c> while <c>(2 / -3)^(1/2)</c> is
/// <c>+0.8165i</c>. It is left because guarding it costs *answers* rather than only
/// shapes -- it is what lets the limit machinery read a 1^oo out of a quotient, and
/// with it guarded <c>(x^2 + 1)^x / (x^2)^x</c> stops having a limit at all, which
/// is the whole of #739 and #740. Pinned here so that the day it is fixed, this
/// test fails and says where to look.
/// The quotient form had the same hole -- <c>sqrt(2) / sqrt(-3)</c> is
/// <c>-0.8165i</c> where <c>(2 / -3)^(1/2)</c> is <c>+0.8165i</c> -- and is fixed
/// too. It was left out of the first pass because guarding it cost *answers*: the
/// gathering was what let the limit machinery read a <c>1^oo</c> out of a quotient.
/// The limit reader now recognises the quotient itself, where the identity is
/// checkable, so the guard costs nothing.
/// https://github.com/asc-community/AngouriMath/issues/802
/// </summary>
[Fact]
public void TheQuotientFormIsStillWrongAndThatIsRecorded()
public void TheQuotientFormKeepsItsValueToo()
{
var simplified = "sqrt(x) / sqrt(y)".ToEntity().Simplify();
var before = "sqrt(x) / sqrt(y)".ToEntity().Substitute("x", 2).Substitute("y", -3).EvalNumerical();
var after = simplified.Substitute("x", 2).Substitute("y", -3).EvalNumerical();
Assert.True(
Math.Abs(before.ImaginaryPart.EDecimal.ToDouble() - after.ImaginaryPart.EDecimal.ToDouble()) > 1e-9,
$"the quotient form now agrees at x = 2, y = -3 -- #802 looks fixed, so this test "
+ $"should become an assertion that it stays fixed. Simplified: {simplified.Stringize()}");
Math.Abs(before.ImaginaryPart.EDecimal.ToDouble() - after.ImaginaryPart.EDecimal.ToDouble()) < 1e-9,
$"sqrt(x) / sqrt(y) simplified to {simplified.Stringize()}, which at x = 2, y = -3 "
+ $"is {after.Stringize()} rather than {before.Stringize()}");
}

/// <summary>
Expand Down
Loading