diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a70ca6bd5..19b460d72 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2927,6 +2927,30 @@ sine, cosine, tangent, logarithm, arcsine, arccosine and arctangent of a real ar | `"Si(10^(-60))".EvalNumerical()` | `0` | `1E-60`, to the hundredth digit | | `"sin(pi)".EvalNumerical()` | `0` | `0` | +### The norm of a vector is the root of its components' moduli squared + +**Silent: wrong answers.** `|v|` of a row vector was taken entry by entry, where a column's was its norm; both are +now the norm. `|v|` of a column vector was `sqrt(sum v_k^2)`, which is its norm only where every +component is real. A complex component made it `NaN`, a different number, or 0, and a symbol, which may take a complex +value, was squared as though it could not. The norm is now `sqrt(sum |v_k|^2)`. A component whose codomain is real, +such as `domain(x, RR)`, is still squared as it is, since that is the same value; a real vector's norm is unchanged. +The compiled norm over `Complex` changes with it, since it compiles the simplified form; over `double` it was right +([#2423](https://github.com/asc-community/AngouriMath/issues/2423)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"abs([1 + i, 2])".ToEntity().Evaled` | `NaN` | `2.449489742783178…`, which is `sqrt(6)` | +| `"abs([1 + i, 2])".Simplify()` | `NaN` | `sqrt(6)` | +| `"abs([3 * i, 4])".Simplify()` | `sqrt(7)` | `5` | +| `"abs([1 + i, 1 - i])".Simplify()` | `0` | `2` | +| `"abs([1 + i, 2]T)".Simplify()`, a row vector | `[[abs(1 + i), 2]]`, entry by entry | `sqrt(6)` | +| `"abs([3, 4]T)".Simplify()` | `[[3, 4]]` | `5` | +| `"abs([x, y])".Simplify()` | `sqrt(x ^ 2 + y ^ 2)` | `sqrt(abs(x) ^ 2 + abs(y) ^ 2)` | +| `"abs([x, 3 * i])".Simplify()` | `sqrt(x ^ 2 - 9)` | `sqrt(9 + abs(x) ^ 2)` | +| `"abs([x, y])".Compile("x", "y")(i, 0)` | `6.1E-17 + i` | `1` | +| `"abs([x, y])".Compile("x", "y")(1 + i, 2)` | `2.058 + 0.486i` | `2.449489742783178` | +| `"abs([3, 4])".Simplify()` | `5` | `5` | + --- ## 2.6.0 — since 2.5.0 diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index d051593ba..bf778e869 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -1619,19 +1619,43 @@ public partial record Absf /// has no exact simplification, so it survived as a and /// then rejected it as "not a simple /// number" -- it only worked when the norm was a perfect square. + /// + /// The norm over the complex numbers is sqrt(sum |v_k|^2). Squaring the + /// component itself is that only on the real line: [1 + i, 2] gave + /// sqrt(4 + 2i), [1 + i, 1 - i] gave 0, and [x, y] gave + /// sqrt(x^2 + y^2), which is 0 at x = i, y = 0 where the norm is 1. + /// https://github.com/asc-community/AngouriMath/issues/2423 /// private static Entity VectorNorm(Matrix vector, bool isExact) { - var norm = Sumf.Sum(vector.Select(component => component.Pow(2))).Pow(0.5); + var norm = Sumf.Sum(vector.Select(ModulusSquared)).Pow(0.5); return isExact ? norm.InnerSimplified : norm.Evaled; } + /// + /// |z|^2, written as z^2 where is known to be + /// real -- the same value, and the form a real vector's norm has always had -- and as + /// re^2 + im^2 for a number, so that an exact one stays exact. Realness is read off + /// the node's codomain, as does: a bare symbol is + /// , so its modulus is kept. + /// + private static Entity ModulusSquared(Entity component) => component switch + { + Complex number => number.RealPart * number.RealPart + number.ImaginaryPart * number.ImaginaryPart, + _ when Conjugatef.IsKnownReal(component) => component.Pow(2), + _ => new Absf(component).Pow(2) + }; + /// protected override Entity InnerSimplify(bool isExact) => ExpandOnOneArgument(Argument, a => a switch { + // A row is a vector as much as a column is: [1 + i, 2]T was taken entry by + // entry, [[abs(1 + i), 2]], where its column is sqrt(6). It is taken through its + // transpose, because a matrix enumerates its rows, and a row's only row is itself. Matrix m when m.IsVector => VectorNorm(m, isExact), + Matrix m when m.IsRowVector => VectorNorm(m.T, isExact), Complex n when !isExact => Number.Abs(n), Absf abs => abs, // The interval itself from zero on, its reflection up to zero, and diff --git a/Sources/Tests/UnitTests/Common/TheNormOfAComplexVectorTest.cs b/Sources/Tests/UnitTests/Common/TheNormOfAComplexVectorTest.cs new file mode 100644 index 000000000..a1b0300f6 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/TheNormOfAComplexVectorTest.cs @@ -0,0 +1,113 @@ +// +// 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 AngouriMath; +using AngouriMath.Extensions; +using Xunit; +using NumericsComplex = System.Numerics.Complex; + +namespace AngouriMath.Tests.Common +{ + /// + /// The norm of a vector is sqrt(sum |v_k|^2). Squaring the components is that only on + /// the real line: |[1 + i, 2]| was NaN, |[1 + i, 1 - i]| was 0, and + /// |[x, y]| simplified to sqrt(x^2 + y^2), which is 0 at x = i, y = 0. + /// https://github.com/asc-community/AngouriMath/issues/2423 + /// + [Trait("Area", "Common")] + public sealed class TheNormOfAComplexVectorTest + { + private static double Value(Entity expression) + { + var value = expression.EvalNumerical(); + Assert.True(value.ImaginaryPart.IsZero, $"{expression} gave {value}, which is not real"); + return value.RealPart.EDecimal.ToDouble(); + } + + // Norms worked out by hand: |a + bi|^2 is a^2 + b^2, summed and rooted. + [Theory] + [InlineData("abs([3, 4])", 5.0)] + [InlineData("abs([-3, 4])", 5.0)] + [InlineData("abs([-2, -3, -6])", 7.0)] + [InlineData("abs([1 + i, 2])", 2.449489742783178)] // sqrt(2 + 4) + [InlineData("abs([3 * i, 4])", 5.0)] // sqrt(9 + 16) + [InlineData("abs([1 + i, 1 - i])", 2.0)] // sqrt(2 + 2) + [InlineData("abs([-1 - 2 * i, 2 * i])", 3.0)] // sqrt(5 + 4) + [InlineData("abs([-i])", 1.0)] + // A row vector is a vector too. + [InlineData("abs([3, 4]T)", 5.0)] + [InlineData("abs([1 + i, 2]T)", 2.449489742783178)] + public void TheNormIsTheRootOfTheSumOfTheModuliSquared(string vector, double norm) + { + Assert.Equal(norm, Value(vector.ToEntity()), 12); + Assert.Equal(norm, Value(vector.ToEntity().Simplify()), 12); + } + + [Fact] + public void AnExactNormStaysExact() => + Assert.Equal(MathS.Sqrt(6), "abs([1 + i, 2])".ToEntity().Simplify()); + + // Every point here has a component off the real line except the last, which is the + // real case that was right before and has to stay right. + public static TheoryData Points => new() + { + { new(0, 1), new(0, 0) }, + { new(1, 1), new(2, 0) }, + { new(1, 2), new(-3, -1) }, + { new(-2, -5), new(0, 3) }, + { new(-3, 0), new(4, 0) }, + }; + + private static Entity At(Entity expression, NumericsComplex x, NumericsComplex y) + => expression + .Substitute("x", Entity.Number.Complex.Create(x.Real, x.Imaginary)) + .Substitute("y", Entity.Number.Complex.Create(y.Real, y.Imaginary)); + + private static double Norm(NumericsComplex x, NumericsComplex y) + => Math.Sqrt(x.Magnitude * x.Magnitude + y.Magnitude * y.Magnitude); + + [Theory] + [MemberData(nameof(Points))] + public void TheSimplifiedNormKeepsItsValueAtComplexPoints(NumericsComplex x, NumericsComplex y) + { + Entity norm = "abs([x, y])"; + Assert.Equal(Norm(x, y), Value(At(norm, x, y)), 10); + Assert.Equal(Norm(x, y), Value(At(norm.Simplify(), x, y)), 10); + Assert.Equal(Norm(x, y), Value(At(norm.Evaled, x, y)), 10); + } + + [Theory] + [MemberData(nameof(Points))] + public void TheNormCompiledOverComplexIsTheNorm(NumericsComplex x, NumericsComplex y) + { + var compiled = "abs([x, y])".Compile("x", "y")(x, y); + Assert.Equal(Norm(x, y), compiled.Real, 10); + Assert.Equal(0, compiled.Imaginary, 10); + } + + [Theory] + [InlineData(-3.0, 4.0)] + [InlineData(1.5, -2.0)] + [InlineData(0.0, -7.0)] + public void TheNormCompiledOverDoubleIsTheNorm(double x, double y) => + Assert.Equal(Math.Sqrt(x * x + y * y), "abs([x, y])".Compile("x", "y")(x, y), 12); + + // Declared real, a component's square is its modulus squared, so the norm is written + // without a modulus, and is the norm at real points. + [Theory] + [InlineData(-3.0, 4.0)] + [InlineData(1.5, -2.0)] + public void ANormOfRealComponentsIsTheNormAtRealPoints(double x, double y) + { + Entity realX = "domain(x, RR)", realY = "domain(y, RR)"; + var norm = MathS.Abs(MathS.Vector(realX, realY)).Simplify(); + Assert.DoesNotContain(norm.Nodes, node => node is Entity.Absf); + Assert.Equal(Math.Sqrt(x * x + y * y), Value(norm.Substitute(realX, x).Substitute(realY, y)), 12); + } + } +}