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
24 changes: 24 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Complex, Complex, Complex>("x", "y")(i, 0)` | `6.1E-17 + i` | `1` |
| `"abs([x, y])".Compile<Complex, Complex, Complex>("x", "y")(1 + i, 2)` | `2.058 + 0.486i` | `2.449489742783178` |
| `"abs([3, 4])".Simplify()` | `5` | `5` |

---

## 2.6.0 — since 2.5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1619,19 +1619,43 @@ public partial record Absf
/// has no exact simplification, so it survived as a <see cref="Powf"/> and
/// <see cref="Entity.EvalNumerical"/> 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 <c>sqrt(sum |v_k|^2)</c>. Squaring the
/// component itself is that only on the real line: <c>[1 + i, 2]</c> gave
/// <c>sqrt(4 + 2i)</c>, <c>[1 + i, 1 - i]</c> gave 0, and <c>[x, y]</c> gave
/// <c>sqrt(x^2 + y^2)</c>, which is 0 at <c>x = i, y = 0</c> where the norm is 1.
/// https://github.com/asc-community/AngouriMath/issues/2423
/// </remarks>
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;
}

/// <summary>
/// <c>|z|^2</c>, written as <c>z^2</c> where <paramref name="component"/> is known to be
/// real -- the same value, and the form a real vector's norm has always had -- and as
/// <c>re^2 + im^2</c> for a number, so that an exact one stays exact. Realness is read off
/// the node's codomain, as <see cref="Conjugatef.IsKnownReal"/> does: a bare symbol is
/// <see cref="Domain.Any"/>, so its modulus is kept.
/// </summary>
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)
};

/// <inheritdoc/>
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
Expand Down
113 changes: 113 additions & 0 deletions Sources/Tests/UnitTests/Common/TheNormOfAComplexVectorTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The norm of a vector is <c>sqrt(sum |v_k|^2)</c>. Squaring the components is that only on
/// the real line: <c>|[1 + i, 2]|</c> was <c>NaN</c>, <c>|[1 + i, 1 - i]|</c> was 0, and
/// <c>|[x, y]|</c> simplified to <c>sqrt(x^2 + y^2)</c>, which is 0 at <c>x = i, y = 0</c>.
/// https://github.com/asc-community/AngouriMath/issues/2423
/// </summary>
[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<NumericsComplex, NumericsComplex> 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<NumericsComplex, NumericsComplex, NumericsComplex>("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<double, double, double>("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);
}
}
}
Loading