diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 0e168e8cc..c784b052f 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -154,6 +154,20 @@ and a number that passes every round is left open, since passing is not a proof | `"(10^30 + 1) in PP".ToEntity().Evaled` | `1000000000000000000000000000001 in PP` | `False` | | `"(10^30 + 57) in PP".ToEntity().Evaled` | `1000000000000000000000000000057 in PP` | `1000000000000000000000000000057 in PP` | +### The row echelon forms divide exactly + +**Not silent for numbers: what threw or did not return now answers. Silent for symbols: the forms are written +otherwise.** Both forms were computed by GenericTensor's SafeDivision elimination, which keeps every entry as a numerator +and a denominator that are never reduced against each other, so their sizes grew with every row operation. For a 4x4 of +small rationals, the reduced form and `Rank` could throw `InvalidCastException`, and an 8x8 did not return. Both now divide as they go +([#2270](https://github.com/asc-community/AngouriMath/issues/2270)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"[[1/2, -2/3, 0, -2/3], [3, -2, 0, -2/3], [2, 2, 1, -3], [-2/3, 0, -2/3, 1]]"`, `.Rank` | throws `InvalidCastException` | `4` | +| the same, `.ReducedRowEchelonForm` | throws `InvalidCastException` | the identity | +| `"[[a, b], [c, d]]"`, `.RowEchelonForm` | `[[a, b], [(c * a + -c * a) / a, (d * a + -c * b) / a]]` | `[[a, b], [c + -c / a * a, d + -c / a * b]]` | + ### `arcsin` and `arccos` of a real number past 1 have their value **Silent: `arcsin(x)` and `arccos(x)` of a real `x` past 1 are `sign(x) pi/2 - i arccosh|x|` and `pi/2 - arcsin(x)`, diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs index a5f07c3b6..fcd071646 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -582,9 +582,17 @@ public IEnumerator GetEnumerator() /// /// Matrix's form, transformed via Gaussian elimination. /// + /// + /// Both forms divide as they go. GenericTensor's SafeDivision variants keep every entry as a numerator and a + /// denominator that are never reduced against each other, which is for element types whose division truncates, + /// and an Entity divides exactly. Unreduced, the pairs grew with every row operation and the back substitution + /// squared them: one 7x7 of small entries took two minutes, and an 8x8 did not return in ten, and the 4x4 + /// of https://github.com/asc-community/AngouriMath/issues/2270 ended with a denominator near 1e-246, which + /// https://github.com/asc-community/AngouriMath/issues/2277 made a NaN. + /// public Matrix RowEchelonForm => @ref.GetValue(static @this => - OfTheShapeOf(@this, new Matrix(@this.InnerMatrix.RowEchelonFormSafeDivision()).InnerSimplified), + OfTheShapeOf(@this, new Matrix(@this.InnerMatrix.RowEchelonFormSimple()).InnerSimplified), this); private LazyPropertyA @ref; @@ -593,7 +601,7 @@ public IEnumerator GetEnumerator() /// public Matrix ReducedRowEchelonForm => rref.GetValue(static @this => - OfTheShapeOf(@this, new Matrix(@this.InnerMatrix.ReducedRowEchelonFormSafeDivision()).InnerSimplified), + OfTheShapeOf(@this, new Matrix(@this.InnerMatrix.ReducedRowEchelonFormSimple()).InnerSimplified), this); private LazyPropertyA rref; diff --git a/Sources/Tests/UnitTests/Algebra/MatrixTest.cs b/Sources/Tests/UnitTests/Algebra/MatrixTest.cs index fea90eb82..462dada8d 100644 --- a/Sources/Tests/UnitTests/Algebra/MatrixTest.cs +++ b/Sources/Tests/UnitTests/Algebra/MatrixTest.cs @@ -556,6 +556,50 @@ [Fact] public void Rank2() Assert.Equal(6, MathS.IdentityMatrix(6).Rank); } + // The elimination kept each entry as an unreduced numerator and denominator, the + // denominator of row 0 reached 1e-246, and 0 over it was NaN: the form reduced to NaN and + // Rank threw InvalidCastException. https://github.com/asc-community/AngouriMath/issues/2270 + [Theory] + [InlineData("[[1/2, -2/3, 0, -2/3], [3, -2, 0, -2/3], [2, 2, 1, -3], [-2/3, 0, -2/3, 1]]")] + [InlineData("[[-2/3, 1, 1, -2], [1/2, -2/3, -2/3, -2/3], [-2/3, -2/3, 1, -1], [1/2, -2, -3, -2/3]]")] + [InlineData("[[-2/3, 1/2, -2/3, 1/2], [1, -2/3, -2/3, -2], [1, -2/3, 1, -3], [-2, -2/3, -1, -2/3]]")] + public void AnInvertibleMatrixReducesToTheIdentity(string matrix) + { + var m = (Matrix)matrix.ToEntity(); + Assert.Equal(MathS.IdentityMatrix(4), m.ReducedRowEchelonForm); + Assert.Equal(4, m.Rank); + } + + // Unreduced, the back substitution squared the pairs' sizes at every row, and this did not + // return in two minutes; bounded, so that it fails rather than hangs the run. + // https://github.com/asc-community/AngouriMath/issues/2270 + [Fact] public void AnEightByEightReduces() + { + var reduced = System.Threading.Tasks.Task.Factory.StartNew(Reduce, + System.Threading.CancellationToken.None, System.Threading.Tasks.TaskCreationOptions.LongRunning, + System.Threading.Tasks.TaskScheduler.Default); + Assert.True(reduced.Wait(System.TimeSpan.FromSeconds(60)), "the reduced row echelon form of an 8x8 did not return"); + Assert.Equal(MathS.IdentityMatrix(8), reduced.Result.Reduced); + Assert.Equal(8, reduced.Result.Rank); + } + + private static (Matrix Reduced, int Rank) Reduce() + { + var m = MathS.Matrix(new Entity[,] + { + { 2, -1, 3, 1, -2, 1, 3, -3 }, + { 1, 2, -1, 3, 1, -2, 1, 2 }, + { -3, 1, 2, -1, 3, 2, -2, 1 }, + { 1, -2, 1, 2, -1, 3, 2, -1 }, + { 2, 3, -2, 1, 2, -1, -3, 1 }, + { -1, 1, 3, -2, 1, 2, 1, 3 }, + { 3, -1, 1, 2, -3, 1, 2, -2 }, + { 1, 2, 2, -1, 1, -3, 1, 1 }, + }); + Assert.NotEqual(Number.Integer.Zero, m.Determinant); + return (m.ReducedRowEchelonForm, m.Rank); + } + [Theory] [InlineData("[a, b]", "[c, d]", "[a c, a d, b c, b d]")] [InlineData("[[a, b], [c, d]]", "[[e, f], [g, h]]", "[ [a e, a f, b e, b f], [a g, a h, b g, b h], [c e, c f, d e, d f], [c g, c h, d g, d h] ]")]