From 416075c9f4caa94941460082fdd9b2cc544adb7c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 20:58:05 +0000 Subject: [PATCH 1/2] The row echelon forms divide exactly The reduced row echelon form of an invertible 4x4 of small rationals had NaN entries, and its Rank threw InvalidCastException. Both forms were GenericTensor's SafeDivision elimination, which keeps every entry as a numerator and a denominator that are never reduced against each other. That 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: the issue's matrix ended with a denominator near 1e-246, and 0 over it is NaN (#2277). A 7x7 took nine seconds and an 8x8 did not return. Both forms now use the Simple elimination, which divides as it goes. MatrixTest takes the issue's matrix and two more of a random sweep to the identity with rank 4, and an 8x8 with determinant -5598, bounded at a minute. All four fail on master, the 8x8 by not returning. The suite, the trio, F#, AOT and the eight harnesses pass. Symbolic forms are written otherwise, [[a, b], [c, d]]'s row echelon form among them. Traced, with GenericTensor's elimination written out step by step, by a subagent. Fixes #2270. Co-Authored-By: Claude Opus 5.5 (1M context) --- BREAKING-CHANGES.md | 14 ++++++ .../Core/Entity/Omni/Entity.Matrix.cs | 12 ++++- Sources/Tests/UnitTests/Algebra/MatrixTest.cs | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index e2d1048ee..7ccab31fd 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -139,6 +139,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 df6a70e63..367c909ab 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -569,9 +569,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: a 7x7 of small entries took nine seconds and an 8x8 did not return in ten minutes, 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 => - (Matrix)new Matrix(@this.InnerMatrix.RowEchelonFormSafeDivision()).InnerSimplified, + (Matrix)new Matrix(@this.InnerMatrix.RowEchelonFormSimple()).InnerSimplified, this); private LazyPropertyA @ref; @@ -580,7 +588,7 @@ public IEnumerator GetEnumerator() /// public Matrix ReducedRowEchelonForm => rref.GetValue(static @this => - (Matrix)new Matrix(@this.InnerMatrix.ReducedRowEchelonFormSafeDivision()).InnerSimplified, + (Matrix)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] ]")] From 22066c213f762c1e80b4fe449d771085ad638566 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 30 Oct 2026 21:01:56 +0000 Subject: [PATCH 2/2] Quote the 7x7 time that was measured here One 7x7 of small entries took 112 seconds through the SafeDivision elimination, with 42 NaN entries. Nine seconds was another matrix's. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs index 367c909ab..a81670a3b 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -573,7 +573,7 @@ public IEnumerator GetEnumerator() /// 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: a 7x7 of small entries took nine seconds and an 8x8 did not return in ten minutes, and the 4x4 + /// 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. ///