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
13 changes: 13 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,19 @@ evaluation found it.
| `"(5/2)^(10^7)".EvalNumerical()` | does not return | `+oo` |
| `"2^(10^10)".Simplify()` | does not return | `2 ^ 10000000000` |

### The inverse of a symbolic matrix carries no condition of a pivot

**Silent: a different answer.** The inverse took its determinant by elimination, which divides by the first pivot and
multiplies it back, and simplifying that wrote the pivot's condition on every entry: `a ≠ 0` for `[[a, b], [c, d]]`,
`cos t ≠ 0` for a rotation. Each inverse exists wherever the determinant is not 0, and at `t = π/2` the rotation's is
`[[0, 1], [-1, 0]]`. The inverse is the adjugate over the determinant now, neither of which divides.
https://github.com/asc-community/AngouriMath/issues/2346

| Input | Was (2.6.0) | Now |
|---|---|---|
| `[[a, b], [c, d]].Inverse.Simplify()` | each entry `… provided not a = 0` | `[[d / (a * d - b * c), …]]`, with no condition |
| `[[cos(t), -sin(t)], [sin(t), cos(t)]].Inverse.Simplify()` | `[[cos(t), sin(t) provided not cos(t) = 0], …]` | `[[cos(t), sin(t)], [-sin(t), cos(t)]]` |

### A remainder of two ratios is exact

**Silent: a remainder that came back as the divisor, or as NaN, is the remainder.** `%` on `Real`, `mod` and evaluation
Expand Down
20 changes: 13 additions & 7 deletions Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,19 +443,25 @@ private static Entity Laplace(Matrix matrix, int row, int[] columns)
private LazyPropertyA<Set?> eigenvalues;

/// <summary>Returns an inverse matrix if it exists</summary>
/// <remarks>
/// The <see cref="Adjugate"/> over the <see cref="Determinant"/>, entry by entry. GenericTensor's inversion
/// took the determinant by elimination, which divides by the first pivot and multiplies it back: the
/// determinant of <c>[[a, b], [c, d]]</c> came to <c>a * (d * a + -c * b) / a</c>, and simplifying its
/// <c>a / a</c> wrote <c>provided not a = 0</c> on every entry of the inverse, which exists wherever the
/// determinant is not 0, <c>a = 0</c> included; the rotation's inverse carried <c>cos t ≠ 0</c>. The
/// determinant here is a polynomial in the entries and the adjugate's are cofactors, so neither divides.
/// https://github.com/asc-community/AngouriMath/issues/2346
/// </remarks>
public Matrix? Inverse => inverse.GetValue(static @this =>
{
var cp = @this.InnerMatrix.Copy(false);
if (!@this.IsSquare)
return null;
if (@this.Determinant is null)
if (@this.Determinant is not { } determinant || determinant == 0)
return null;
if (@this.Determinant == 0)
if (@this.Adjugate is not { } adjugate)
return null;
// Inverting goes through the adjugate, which takes its minors in the same
// process-wide scratch matrix the determinant does. See Functions.GenTensorGuard.
Functions.GenTensorGuard.InvertMatrix(cp);
return ToMatrix(new Matrix(cp).InnerSimplified);
return OfTheShapeOf(@this, MathS.Matrix(@this.RowCount, @this.ColumnCount,
(row, column) => adjugate[row, column] / determinant).InnerSimplified);
}, this);
private LazyPropertyA<Matrix?> inverse;

Expand Down
15 changes: 3 additions & 12 deletions Sources/AngouriMath/Functions/GenTensorGuard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ internal static class GenTensorGuard
{
/// <summary>
/// Held across any call that reaches GenericTensor's shared scratch-matrix pool -- which
/// is <c>DeterminantLaplace</c>, <c>Adjoint</c>, and <c>InvertMatrix</c>, since inverting
/// goes through the adjugate. <c>DeterminantGaussianSafeDivision</c> and
/// is <c>DeterminantLaplace</c> and <c>Adjoint</c>; <c>InvertMatrix</c> goes through the
/// adjugate too, and the inverse is no longer taken from it. <c>DeterminantGaussianSafeDivision</c> and
/// <c>MatrixMultiply</c> do not touch the pool and are deliberately not covered.
/// </summary>
/// <remarks>
/// It is private, and the three operations below are the whole surface, because mutual
/// It is private, and the operations below are the whole surface, because mutual
/// exclusion here is not a property any one caller can hold up: a single call that skips
/// the lock corrupts every call that takes it, and reads corrupted minors back. Exposing
/// the lock lets a caller reach GenericTensor without it; exposing only the operations
Expand Down Expand Up @@ -83,15 +83,6 @@ internal static Entity Adjoint(GenTensor matrix)
return new Entity.Matrix(matrix.Adjoint()).InnerSimplified;
}

/// <summary>
/// Inversion in place, serialised on <see cref="ScratchPool"/> -- it goes through the
/// adjugate, so it takes its minors in the same pool.
/// </summary>
internal static void InvertMatrix(GenTensor matrix)
{
lock (ScratchPool)
matrix.InvertMatrix();
}

[ConstantField] private static readonly Lazy<bool> piecewiseCache =
new Lazy<bool>(WarmPiecewiseCache, LazyThreadSafetyMode.ExecutionAndPublication);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// 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 Xunit;
using static AngouriMath.Entity;

namespace AngouriMath.Tests.Algebra
{
/// <summary>
/// The inverse took its determinant by elimination, which divides by the first pivot and multiplies it back, so
/// every entry of the inverse of <c>[[a, b], [c, d]]</c> simplified with <c>provided not a = 0</c>, and the rotation's
/// with <c>cos t ≠ 0</c>, though each inverse exists wherever the determinant is not 0. It is the adjugate over the
/// determinant now. https://github.com/asc-community/AngouriMath/issues/2346
/// </summary>
[Trait("Area", "Algebra")]
public sealed class AnInverseIsTheAdjugateOverTheDeterminantTest
{
[Theory]
[InlineData("[[a, b], [c, d]]")]
[InlineData("[[cos(t), -sin(t)], [sin(t), cos(t)]]")]
[InlineData("[[x, 1], [1, 0]]")]
public void CarriesNoConditionOfAPivot(string matrix)
{
var inverse = Assert.IsAssignableFrom<Matrix>(((Matrix)matrix.ToEntity()).Inverse);
Assert.DoesNotContain("provided", inverse.Simplify().Stringize());
}

[Fact]
public void IsTheAdjugateOverTheDeterminant()
{
var inverse = Assert.IsAssignableFrom<Matrix>(((Matrix)"[[a, b], [c, d]]".ToEntity()).Inverse);
var expected = (Matrix)"[[d, -b], [-c, a]]".ToEntity();
for (var row = 0; row < 2; row++)
for (var column = 0; column < 2; column++)
{
// Zero where the inverse exists, a d - b c not 0, and the difference says so.
var difference = (inverse[row, column] - expected[row, column] / "a d - b c".ToEntity()).Simplify();
while (difference is Providedf(var inner, _))
difference = inner;
Assert.Equal(Number.Integer.Zero, difference);
}
}

[Fact]
public void TheRotationsInverseHoldsWhereTheCosineIsZero()
{
var inverse = Assert.IsAssignableFrom<Matrix>(((Matrix)"[[cos(t), -sin(t)], [sin(t), cos(t)]]".ToEntity()).Inverse);
Assert.Equal("[[0, 1], [-1, 0]]".ToEntity(), inverse.Substitute("t", MathS.pi / 2).Evaled);
}

[Fact]
public void ANumericInverseIsAsItWas()
=> Assert.Equal("[[-2, 1], [3/2, -1/2]]".ToEntity(), ((Matrix)"[[1, 2], [3, 4]]".ToEntity()).Inverse);

[Fact]
public void ASingularMatrixHasNone()
=> Assert.Null(((Matrix)"[[1, 2], [2, 4]]".ToEntity()).Inverse);
}
}
Loading