diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f7c7fe93d..92e6eede8 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -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 diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs index dd713cab7..cb6173868 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -443,19 +443,25 @@ private static Entity Laplace(Matrix matrix, int row, int[] columns) private LazyPropertyA eigenvalues; /// Returns an inverse matrix if it exists + /// + /// The over the , entry by entry. GenericTensor's inversion + /// took the determinant by elimination, which divides by the first pivot and multiplies it back: the + /// determinant of [[a, b], [c, d]] came to a * (d * a + -c * b) / a, and simplifying its + /// a / a wrote provided not a = 0 on every entry of the inverse, which exists wherever the + /// determinant is not 0, a = 0 included; the rotation's inverse carried cos t ≠ 0. 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 + /// 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 inverse; diff --git a/Sources/AngouriMath/Functions/GenTensorGuard.cs b/Sources/AngouriMath/Functions/GenTensorGuard.cs index 810a04587..089d07315 100644 --- a/Sources/AngouriMath/Functions/GenTensorGuard.cs +++ b/Sources/AngouriMath/Functions/GenTensorGuard.cs @@ -43,12 +43,12 @@ internal static class GenTensorGuard { /// /// Held across any call that reaches GenericTensor's shared scratch-matrix pool -- which - /// is DeterminantLaplace, Adjoint, and InvertMatrix, since inverting - /// goes through the adjugate. DeterminantGaussianSafeDivision and + /// is DeterminantLaplace and Adjoint; InvertMatrix goes through the + /// adjugate too, and the inverse is no longer taken from it. DeterminantGaussianSafeDivision and /// MatrixMultiply do not touch the pool and are deliberately not covered. /// /// - /// 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 @@ -83,15 +83,6 @@ internal static Entity Adjoint(GenTensor matrix) return new Entity.Matrix(matrix.Adjoint()).InnerSimplified; } - /// - /// Inversion in place, serialised on -- it goes through the - /// adjugate, so it takes its minors in the same pool. - /// - internal static void InvertMatrix(GenTensor matrix) - { - lock (ScratchPool) - matrix.InvertMatrix(); - } [ConstantField] private static readonly Lazy piecewiseCache = new Lazy(WarmPiecewiseCache, LazyThreadSafetyMode.ExecutionAndPublication); diff --git a/Sources/Tests/UnitTests/Algebra/AnInverseIsTheAdjugateOverTheDeterminantTest.cs b/Sources/Tests/UnitTests/Algebra/AnInverseIsTheAdjugateOverTheDeterminantTest.cs new file mode 100644 index 000000000..9a6918b7c --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/AnInverseIsTheAdjugateOverTheDeterminantTest.cs @@ -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 +{ + /// + /// The inverse took its determinant by elimination, which divides by the first pivot and multiplies it back, so + /// every entry of the inverse of [[a, b], [c, d]] simplified with provided not a = 0, and the rotation's + /// with cos t ≠ 0, 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 + /// + [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.ToEntity()).Inverse); + Assert.DoesNotContain("provided", inverse.Simplify().Stringize()); + } + + [Fact] + public void IsTheAdjugateOverTheDeterminant() + { + var inverse = Assert.IsAssignableFrom(((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)"[[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); + } +}