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
16 changes: 16 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,22 @@ compiled expressions against evaluation found it.
| `"(10^200) mod 7".EvalNumerical()` | `NaN` | `2` |
| `"(10^100 + 1/2) mod 1".EvalNumerical()` | `NaN` | `1/2` |

### A complex product past the exponent range has its parts, not NaN

**Silent: a different answer.** A complex number's parts are decimals in the precision context, whose exponents end at
a thousand. A product or a whole power of finite numbers, a part of which passed that end on the way, had `oo - oo` in
it and was `NaN`, which says the value does not exist; `Simplify` returned that `NaN` as well. Each part past the range
is the infinity of its sign now, as a real past it is `+oo`, and a part within it keeps its value. An exact
simplification leaves the expression as written, since the infinity is the context's and not the value's. No issue was
filed.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"(3+4i)^2000".EvalNumerical()` | `NaN` | `+oo + +ooi` |
| `"(1+i)^(10^6)".EvalNumerical()` | `NaN` | `+oo` |
| `"(1e600+1e600i)*(1e600+1e600i)".EvalNumerical()` | `NaN` | `+ooi` |
| `"(3+4i)^2000".Simplify()` | `NaN` | `(3 + 4i) ^ 2000` |

### A binomial whose factorials leave the exponent range has its value

**Silent: a binomial that was `+oo` or NaN is its value.** `binomial(n, k)` for a `k` that is not whole is
Expand Down
72 changes: 57 additions & 15 deletions Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,40 @@ static EDecimal ModifiedMultiply(EDecimal a, EDecimal b) =>
a.IsInfinity() && b.IsZero || b.IsInfinity() && a.IsZero ? EDecimal.Zero : CtxMultiply(a, b);
var (ac, bd) = (ModifiedMultiply(a.RealPart.EDecimal, b.RealPart.EDecimal), ModifiedMultiply(a.ImaginaryPart.EDecimal, b.ImaginaryPart.EDecimal));
var (ad, bc) = (ModifiedMultiply(a.RealPart.EDecimal, b.ImaginaryPart.EDecimal), ModifiedMultiply(a.ImaginaryPart.EDecimal, b.RealPart.EDecimal));
return Complex.CreateAtScales(CtxSubtract(ac, bd), CtxAdd(ad, bc), CancelledScaleOfADifference(ac, bd), CancelledScale(ad, bc));
var (real, imaginary) = (CtxSubtract(ac, bd), CtxAdd(ad, bc));
if (!(real.IsFinite && imaginary.IsFinite) && a.IsFinite && b.IsFinite)
return ProductPastTheRange(a, b);
return Complex.CreateAtScales(real, imaginary, CancelledScaleOfADifference(ac, bd), CancelledScale(ad, bc));
}
);

/// <summary>
/// Whether both parts are finite, read from the parts: <see cref="Entity.IsFinite"/> is a cache made on
/// an entity's first read of it, and asked of every power built, and of every exact value an evaluation
/// takes, it was half a megabyte on each solve of the kernel benchmark's SolveHard. A rational part is
/// finite and is not written out to a decimal to say so.
/// </summary>
internal static bool PartsAreFinite(Complex number) => PartIsFinite(number.RealPart) && PartIsFinite(number.ImaginaryPart);

private static bool PartIsFinite(Real part) => part is Rational || part.EDecimal.IsFinite;

/// <summary>
/// The product of two finite numbers a part of which leaves the context's exponent range. Within it,
/// <c>(1e600 + 1e600 i)^2</c> had <c>1e1200 - 1e1200</c> for its real part, which is <c>oo - oo</c> there,
/// and the product was NaN; the parts are formed past the range and rounded into it, so that it is
/// <c>2e1200 i</c> rounded, <c>oo i</c>, as a real product past the range is <c>+oo</c>.
/// </summary>
private static Complex ProductPastTheRange(Complex a, Complex b)
{
var context = MathS.Settings.DecimalPrecisionContext.Value;
var unbounded = InternalAMExtensions.WithUnboundedExponents(context);
var (ar, ai, br, bi) = (a.RealPart.EDecimal, a.ImaginaryPart.EDecimal, b.RealPart.EDecimal, b.ImaginaryPart.EDecimal);
var (ac, bd) = (ar.Multiply(br, unbounded), ai.Multiply(bi, unbounded));
var (ad, bc) = (ar.Multiply(bi, unbounded), ai.Multiply(br, unbounded));
return Complex.CreateAtScales(ac.Subtract(bd, unbounded).RoundToPrecision(context), ad.Add(bc, unbounded).RoundToPrecision(context),
CancelledScaleOfADifference(ac, bd), CancelledScale(ad, bc));
}

internal static Complex OpDiv<T>(T a, T b) where T : Number =>
SuperSwitch<Complex>(a, b,
(a, b) => CtxDivide(a.ERational, b.ERational) is { } n ? Rational.Create(n) : Real.NaN,
Expand Down Expand Up @@ -615,7 +645,18 @@ static Complex BinaryIntPow(Complex num, EInteger val)
var raised = decimalBase.Pow(pow.Abs().ToInt32Checked(), wholeContext);
return Real.CreateAtScale(pow.Sign < 0 ? EDecimal.One.Divide(raised, wholeContext) : raised, EDecimal.Zero);
}
return BinaryIntPow(@base, pow);
var built = BinaryIntPow(@base, pow);
if (@base is Real || PartsAreFinite(built))
return built;
// A finite complex base whose power leaves the exponent range on the way: each squaring past
// it was oo - oo in a part, and (3 + 4i)^2000 and (1 + i)^(10^6) were NaN. The squarings are
// made past the range and the parts rounded into it at the end.
var rangeContext = MathS.Settings.DecimalPrecisionContext.Value;
Complex pastTheRange;
using (MathS.Settings.DecimalPrecisionContext.Set(InternalAMExtensions.WithUnboundedExponents(rangeContext)))
pastTheRange = BinaryIntPow(@base, pow);
return Complex.Create(pastTheRange.RealPart.EDecimal.RoundToPrecision(rangeContext),
pastTheRange.ImaginaryPart.EDecimal.RoundToPrecision(rangeContext));
}

// A half power of a nonnegative decimal is its square root, not its exponential
Expand Down Expand Up @@ -715,7 +756,7 @@ private static Complex ByThePolarForm(EDecimal baseReal, EDecimal baseImaginary,
/// The most bits an exact power is built to: <c>3^(10^7)</c>, at 15.8 million, takes three seconds,
/// and each doubling of the exponent doubles the size and more than doubles the time.
/// </summary>
[ConstantField] private static readonly EInteger MaxExactPowerBits = EInteger.FromInt32(1 << 24);
private const long MaxExactPowerBits = 1 << 24;

/// <summary>
/// About how many bits an exact <paramref name="base"/> to the whole <paramref name="power"/> has: for
Expand All @@ -724,20 +765,21 @@ private static Complex ByThePolarForm(EDecimal baseReal, EDecimal baseImaginary,
/// and a power of one is as small as it is. A base that is not exact is 0, being a decimal.
/// </summary>
internal static bool IsTooLargeToBuild(Complex @base, EInteger power)
=> ExactPowerBits(@base, power).CompareTo(MaxExactPowerBits) > 0;

private static EInteger ExactPowerBits(Complex @base, EInteger power)
{
// In longs: the bound is asked of every exact power, and as EIntegers it was a quarter of a
// megabyte on each solve of the kernel benchmark's SolveHard.
if (@base.RealPart is not Rational { ERational: var re } || @base.ImaginaryPart is not Rational { ERational: var im })
return EInteger.Zero;
static EInteger Bits(ERational part)
=> part.IsZero ? EInteger.Zero
: part.Numerator.Abs().GetUnsignedBitLengthAsEInteger().Subtract(1)
.Add(part.Denominator.GetUnsignedBitLengthAsEInteger().Subtract(1));
var bits = Bits(re).CompareTo(Bits(im)) >= 0 ? Bits(re) : Bits(im);
if (!re.IsZero && !im.IsZero)
bits = bits.Add(1);
return bits.Multiply(power.Abs());
return false;
static long Bits(ERational part)
=> part.IsZero ? 0 : part.Numerator.GetUnsignedBitLengthAsInt64() - 1 + part.Denominator.GetUnsignedBitLengthAsInt64() - 1;
var bits = Math.Max(Bits(re), Bits(im)) + (!re.IsZero && !im.IsZero ? 1 : 0);
if (bits == 0)
return false;
var powerBits = power.GetUnsignedBitLengthAsInt64();
if (powerBits > 40)
return true;
var magnitude = power.Sign < 0 ? -power.ToInt64Unchecked() : power.ToInt64Unchecked();
return magnitude > MaxExactPowerBits / bits;
}

/// <summary>e.g. Log(2, 32) = 5</summary>
Expand Down
23 changes: 14 additions & 9 deletions Sources/AngouriMath/Functions/Evaluation/Evaluation.Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,22 @@ private static bool IsAZeroOnlyRoundingMade(Entity node, Entity value)
&& (node is Mulf or Divf or Powf && CannotBeZero(node) || node is Sumf or Minusf && SignOfANonzero(node) is not null);

/// <summary>
/// Whether <paramref name="value"/> is an infinity that only the decimals made: an exact power too
/// large to build is evaluated as a decimal, which is <c>+oo</c> past the context's range, and an
/// infinity counts as exact, so that <c>2^(10^10)</c>, and every expression holding it, was
/// simplified to <c>+oo</c>. Asked only of a value that is not finite.
/// Whether <paramref name="value"/> is an infinity that only the decimals made. A sum, a product or a
/// power of exact finite numbers, the power's base not zero, is exact and finite, and is an infinity only
/// where the context's exponent range rounds it onto one: an exact power too large to build is the
/// decimal, and a complex product's parts are decimals, so <c>2^(10^10)</c> and <c>(3 + 4i)^2000</c> are
/// infinite as evaluated. An infinity counts as exact, and every expression holding one was simplified to
/// it. An operand that is a decimal already is not asked about: its power past the range is the
/// decimal's <c>+oo</c>, as it was. Asked only of a value that is not finite.
/// </summary>
private static bool IsAnInfinityOnlyRoundingMade(Entity node, Entity value)
=> value is Complex { IsFinite: false } && !value.IsNaN
&& node.Nodes.Any(static part => part is Powf(var @base, var exponent)
&& @base.Evaled is Complex { IsExact: true, IsFinite: true } exactBase
&& exponent.Evaled is Integer { EInteger: var power }
&& Number.IsTooLargeToBuild(exactBase, power));
=> value is Complex number && !Number.PartsAreFinite(number) && !value.IsNaN && node.Nodes.Any(IsFiniteRoundedOntoAnInfinity);

private static bool IsFiniteRoundedOntoAnInfinity(Entity part)
=> part is Sumf or Minusf or Mulf or Powf
&& part.Evaled is Complex { IsFinite: false } evaluated && !evaluated.IsNaN
&& part.DirectChildren.All(static operand => operand.Evaled is Complex { IsFinite: true, IsExact: true })
&& !(part is Powf(var @base, _) && @base.Evaled is Complex { IsZero: true });

/// <summary>
/// Whether <paramref name="expr"/> is a number or a constant that is not zero, or a product,
Expand Down
91 changes: 91 additions & 0 deletions Sources/Tests/UnitTests/Core/AComplexOverflowIsNotNaNTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// 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;
using System.Threading.Tasks;
using AngouriMath.Extensions;
using PeterO.Numbers;
using Xunit;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Tests.Core
{
/// <summary>
/// A complex number's parts are decimals in the precision context, whose exponents end at a thousand. A
/// product or a power of finite numbers a part of which passed that end had <c>oo - oo</c> in it and was
/// NaN, which says the value does not exist: <c>(3 + 4 * i)^2000</c>, <c>(1 + i)^(10^6)</c> and
/// <c>(1e600 + 1e600 * i)^2</c> among them. Each part past the range is the infinity of its sign now, as a
/// real past it is <c>+oo</c>, and an exact simplification leaves the expression as written. No issue was
/// filed.
/// </summary>
[Trait("Area", "Core")]
public sealed class AComplexOverflowIsNotNaNTest
{
[Theory]
[InlineData("(1e600 + 1e600 * i)^2")]
[InlineData("(1e600 + 1e600 * i) * (1e600 + 1e600 * i)")]
public void AProductPastTheRangeHasItsParts(string input)
{
var value = Within(() => input.ToEntity().Evaled, input);
var complex = Assert.IsAssignableFrom<Complex>(value);
Assert.True(complex.RealPart.EDecimal.IsZero, $"{input} has the real part {complex.RealPart}");
Assert.True(complex.ImaginaryPart.EDecimal.IsPositiveInfinity(), $"{input} has the imaginary part {complex.ImaginaryPart}");
}

[Fact]
public void APowerOfOnePlusIPastTheRangeIsPositiveInfinity()
=> Assert.Equal(Real.PositiveInfinity, Within(() => "(1 + i)^(10^6)".ToEntity().Evaled, "(1 + i)^(10^6)"));

[Theory]
[InlineData(3, 4, 2000)]
[InlineData(3, 4, 2001)]
[InlineData(1, 2, 3000)]
[InlineData(-5, 7, 2500)]
public void AGaussianPowerPastTheRangeHasTheSignsOfItsParts(int real, int imaginary, int power)
{
var input = $"({real} + {imaginary} * i)^{power}";
var value = Assert.IsAssignableFrom<Complex>(Within(() => input.ToEntity().Evaled, input));
var (exactReal, exactImaginary) = (EInteger.One, EInteger.Zero);
for (var k = 0; k < power; k++)
(exactReal, exactImaginary) = (exactReal * real - exactImaginary * imaginary, exactReal * imaginary + exactImaginary * real);
AssertRoundedInto(exactReal, value.RealPart.EDecimal, input);
AssertRoundedInto(exactImaginary, value.ImaginaryPart.EDecimal, input);
}

[Theory]
[InlineData("(3 + 4 * i)^1400")]
[InlineData("(3 + 4 * i)^(-2000)")]
public void APowerWithinTheRangeIsFinite(string input)
=> Assert.True(Within(() => input.ToEntity().Evaled, input).IsFinite, $"{input} is not finite");

[Theory]
[InlineData("(3 + 4 * i)^2000")]
[InlineData("(1 + i)^(10^6)")]
[InlineData("(3 + 4 * i)^2000 + x")]
public void AnExactSimplificationLeavesItAsWritten(string input)
{
Assert.True(Within(() => input.ToEntity().InnerSimplified, input) is not Number { IsFinite: false }, $"{input} was simplified to a number that is not finite");
Assert.True(Within(() => input.ToEntity().Simplify(), input) is not Number { IsFinite: false }, $"{input} was simplified to a number that is not finite");
}

/// <summary>The exact part rounded into the context: the same infinity past it, and the same sign within.</summary>
private static void AssertRoundedInto(EInteger exact, EDecimal evaluated, string input)
{
var rounded = EDecimal.FromEInteger(exact).RoundToPrecision(MathS.Settings.DecimalPrecisionContext);
Assert.True(rounded.IsInfinity() == evaluated.IsInfinity() && rounded.Sign == evaluated.Sign,
$"{input} has the part {evaluated}, where the exact part is {rounded}");
}

private static Entity Within(Func<Entity> evaluation, string input)
{
var task = Task.Factory.StartNew(evaluation, TaskCreationOptions.LongRunning);
Assert.True(task.Wait(TimeSpan.FromSeconds(30)), $"{input} did not finish in thirty seconds");
return task.Result;
}
}
}
Loading