From a41dfeb0b525469779801f9529b302b1677a5a91 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 27 Oct 2026 02:15:39 +0000 Subject: [PATCH 1/2] A matrix is divided, raised to a power and compared as a matrix Where an operation had no answer of its own for two matrices, or a matrix and a number, ExpandOnTwoArguments applied it entry by entry. That is what addition and subtraction mean, and a matrix over a number, but not division, powers or equality: [[1, 2], [3, 4]] divided by itself was [[1, 1], [1, 1]], [1, 2]^2 was [1, 4], and [1, 2] = [1, 3] was the vector [True, False] while [1, 2] = [1, 2] was True. Simplify writes x * x as x^2, so the undefined product [1, 2] * [1, 2] became [1, 4] too. Dividing by a matrix now multiplies by its inverse, as the wiki says it does, and a divisor with no inverse leaves the quotient written. A number over a matrix is that number times the inverse as well: x / y and x * y^(-1) are rewritten into each other, and y^(-1) of a matrix is its inverse, so reading 1 / M entry by entry disagreed with M^(-1). A version that spread the number over the entries instead turned a singular M^(-1), which Simplify writes as 1 / M, into the reciprocals of its entries. A power of anything but a square matrix stays written, and so does a negative power of a singular one, which threw InvalidDeterminantException from Evaled and Simplify. Two matrices are equal when they have one shape and are equal entry by entry, as one statement. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 32 +++++++++++++ ...aluation.Continuous.Arithmetics.Classes.cs | 25 +++++++++-- .../Evaluation.Discrete.Classes.cs | 19 ++++++++ Sources/Tests/UnitTests/Algebra/MatrixTest.cs | 45 +++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index a3ed4d206..f0d93485d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -60,6 +60,38 @@ mentions answers it as a parameter, as a short system does, where it was `null` | `MathS.Equations("x - 1", "x^2 - 1").Solve("x", "y")` | `null` | `[[1, t_1]]` | | `MathS.Equations("a - 1", "b * 0").Solve("a", "b")` | `null` | `[[1, t_1]]` | +### A matrix is divided, raised to a power and compared as a matrix + +**Silent: wrong answers about matrices become right, or stay written.** Where an operation had no answer of its own +for two matrices, or a matrix and a number, it fell through to a branch every operation shares, which applies the +operation entry by entry. Addition and subtraction are meant to be that, and are unchanged, as is a matrix over a +number. But the wiki's Matrices page says division multiplies by the divisor's inverse, and that a product of shapes +that do not fit does not happen. Now: + +- **Dividing by a matrix** multiplies by its inverse. A number over a matrix does too: `x / y` and `x * y^(-1)` are + rewritten into each other, and `y^(-1)` of a matrix is its inverse. A divisor with no inverse leaves the quotient + written. +- **A power** of anything but a square matrix stays written, and so does a negative power of a singular one, which + threw `InvalidDeterminantException`. `Simplify` writes `x * x` as `x^2`, so the undefined product `[1, 2] * [1, 2]` + had come out as `[1, 4]`. +- **Two matrices are equal** when they have one shape and are equal entry by entry, and the equality is one + statement. + +([#2141](https://github.com/asc-community/AngouriMath/issues/2141)). `Evaled` and `Simplify()` give the same answers, +except where shown. + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `[[1, 2], [3, 4]] / [[1, 2], [3, 4]]` | `[[1, 1], [1, 1]]` | `[[1, 0], [0, 1]]` | +| `2 / [[1, 2], [3, 4]]` | `[[2, 1], [2/3, 1/2]]` | `[[-4, 2], [3, -1]]` | +| `2 / [1, 2]` | `[2, 1]` | `2 / [1, 2]` | +| `[1, 2] ^ 2` | `[1, 4]` | `[1, 2] ^ 2` | +| `([1, 2] * [1, 2]).Simplify()` | `[1, 4]` | `[1, 2] ^ 2` | +| `[1, 2] = [1, 3]` | `[True, False]` | `False` | +| `[a, b] = [c, d]` | `[a = c, b = d]` | `a = c and b = d` | +| `([[1, 2], [2, 4]] ^ (-1)).Evaled` | throws `InvalidDeterminantException` | `[[1, 2], [2, 4]] ^ (-1)` | +| `([[1, 2], [2, 4]] ^ (-1)).Simplify()` | throws `InvalidDeterminantException` | `1 / [[1, 2], [2, 4]]` | + ### A polynomial system is not answered with some of its solutions missing **Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index 72a0609ce..ea4a2a726 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -169,6 +169,15 @@ protected override Entity InnerSimplify(bool isExact) => ExpandOnTwoArguments(Dividend, Divisor, (a, b) => (a, b) switch { + // Dividing by a matrix multiplies by its inverse, as the wiki's Matrices page + // says division does. Left to the elementwise branch every operation shares, + // [[1, 2], [3, 4]] / [[1, 2], [3, 4]] was [[1, 1], [1, 1]]. A number over a + // matrix too: x / y is x * y^(-1), which Simplify rewrites freely, and y^(-1) + // of a matrix is its inverse, so 1 / M read elementwise disagreed with M^(-1). + // A divisor with no inverse, singular or not square, leaves the quotient + // written. A matrix over a number is still spread over the entries. + // https://github.com/asc-community/AngouriMath/issues/2141 + (_, Matrix divisor) => divisor.Inverse is { } inverse ? (a * inverse).InnerSimplified(isExact) : a / b, (Integer(0), var n0) => n0.Evaled is Complex c ? c.IsZero ? MathS.NaN : 0 @@ -279,9 +288,13 @@ private bool IsEvenRoot private static bool TryPower(Matrix m, int exp, out Entity res) { - // Only a square matrix has powers; Pow throws for any other. - res = m.IsSquare ? m.Pow(exp) : 0; - return m.IsSquare; + // Only a square matrix has powers; Pow throws for any other. And only one with + // an inverse has negative powers: Pow threw for a singular one, out of eval and + // Simplify alike, and so did dividing by one once division used the inverse. + // https://github.com/asc-community/AngouriMath/issues/2141 + var defined = m.IsSquare && (exp >= 0 || m.Inverse is not null); + res = defined ? m.Pow(exp) : 0; + return defined; } // Re(x) = x/2 * (1 + 1/sgn(x)^2) @@ -293,6 +306,12 @@ protected override Entity InnerSimplify(bool isExact) => (a, b) => (a, b) switch { (Matrix m, Integer(var exp)) when exp is { } expNotNull && TryPower(m, expNotNull, out var res) => res.InnerSimplified(isExact), + // Only a square matrix has powers, and only whole ones. Anything else fell + // through to the elementwise branch: [1, 2]^2 was [1, 4], which is no power + // of a column, and Simplify, which writes x * x as x^2, made the undefined + // product [1, 2] * [1, 2] into the same. It stays written. + // https://github.com/asc-community/AngouriMath/issues/2141 + (Matrix, _) => MathS.Pow(a, b), // An interval raised to a number, and a number raised to an interval, are // the images of the interval under a monotone function where the function // is one. https://github.com/asc-community/AngouriMath/issues/322 diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs index 0fc2e7b4c..f717f659d 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Discrete/Evaluation.Discrete.Classes.cs @@ -298,10 +298,29 @@ private static bool HasAnUnreadSize(Entity side) _ => null, }; + /// + /// Two matrices are equal when they have one shape and are equal entry by entry, so + /// their equality is one statement, the conjunction of the entries' equalities. Left to + /// the elementwise branch every operation shares, [1, 2] = [1, 3] was the vector + /// [True, False], while [1, 2] = [1, 2] was True. + /// https://github.com/asc-community/AngouriMath/issues/2141 + /// + private static Entity MatricesAreEqual(Matrix left, Matrix right, bool isExact) + { + if (left.RowCount != right.RowCount || left.ColumnCount != right.ColumnCount) + return False; + Entity all = True; + for (var row = 0; row < left.RowCount; row++) + for (var column = 0; column < left.ColumnCount; column++) + all &= left[row, column].Equalizes(right[row, column]); + return all.InnerSimplified(isExact); + } + /// protected override Entity InnerSimplify(bool isExact) => ExpandOnTwoArguments(Left, Right, (left, right) => left == right ? true + : left is Matrix matrixLeft && right is Matrix matrixRight ? MatricesAreEqual(matrixLeft, matrixRight, isExact) // A size that is not a number is compared as a size, never by the difference // of two expressions: card(ZZ) - card(QQ) is not zero, and the two are equal. : AngouriMath.Core.Sets.Cardinality.IsSize(left) || AngouriMath.Core.Sets.Cardinality.IsSize(right) ? AngouriMath.Core.Sets.Cardinality.Compare(left, right, static sign => sign == 0, isExact) diff --git a/Sources/Tests/UnitTests/Algebra/MatrixTest.cs b/Sources/Tests/UnitTests/Algebra/MatrixTest.cs index fc4715fc7..fea90eb82 100644 --- a/Sources/Tests/UnitTests/Algebra/MatrixTest.cs +++ b/Sources/Tests/UnitTests/Algebra/MatrixTest.cs @@ -923,5 +923,50 @@ public void SymbolicDeterminantCarriesNoCondition(string matrixRaw, string expec Assert.DoesNotContain(det.Nodes, node => node is Providedf); Assert.Equal(0, ((Entity)expected - det).Simplify().EvalNumerical()); } + + // Division by a matrix, a power of a matrix and the equality of two matrices each fell + // through to the elementwise branch every operation shares wherever the operation itself + // had no answer: A / A was all ones, [1, 2]^2 was [1, 4], and [1, 2] = [1, 3] was the + // vector [True, False]. Division multiplies by the inverse, as the wiki says it does; a + // power of anything but a square matrix stays written; and an equality of matrices is + // one statement. https://github.com/asc-community/AngouriMath/issues/2141 + [Theory] + [InlineData("[[1, 2], [3, 4]] / [[1, 2], [3, 4]]", "[[1, 0], [0, 1]]")] + [InlineData("[[2, 1], [7, 4]] / [[1, 2], [3, 4]]", "[[-5/2, 3/2], [-8, 5]]")] + [InlineData("[1, 2] = [1, 3]", "false")] + [InlineData("[1, 2] = [1, 2]", "true")] + [InlineData("[1, 2] = [1, 2, 3]", "false")] + [InlineData("[a, b] = [c, d]", "a = c and b = d")] + public void AMatrixIsDividedAndComparedAsAMatrix(string expression, string expected) + { + Assert.Equal(expected.ToEntity(), expression.ToEntity().InnerSimplified); + Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify()); + } + + [Theory] + [InlineData("[1, 2] ^ 2")] + [InlineData("[1, 2] * [1, 2]")] + [InlineData("[[1, 2], [2, 4]] / [[1, 2], [2, 4]]")] + [InlineData("[[1, 2], [2, 4]] ^ (-1)")] + [InlineData("1 / [[1, 2], [2, 4]]")] + [InlineData("2 / [1, 2]")] + public void WhatIsNotDefinedOnMatricesStaysWritten(string expression) + { + Assert.IsNotType(expression.ToEntity().Evaled); + Assert.IsNotType(expression.ToEntity().Simplify()); + } + + // A number over a matrix is the number times its inverse, so that it agrees with the + // matrix to the power -1, which Simplify writes it as. A matrix over a number is still + // spread over the entries. + [Theory] + [InlineData("2 / [[1, 2], [3, 4]]", "[[-4, 2], [3, -1]]")] + [InlineData("[[1, 2], [3, 4]] ^ (-1)", "[[-2, 1], [3/2, -1/2]]")] + [InlineData("[[1, 2], [3, 4]] / 2", "[[1/2, 1], [3/2, 2]]")] + public void ANumberOverAMatrixIsItsInverseScaled(string expression, string expected) + { + Assert.Equal(expected.ToEntity(), expression.ToEntity().InnerSimplified); + Assert.Equal(expected.ToEntity(), expression.ToEntity().Simplify()); + } } } From b9132055ba7207fd30a7c7965de173f353ffdaa3 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 27 Oct 2026 02:37:18 +0000 Subject: [PATCH 2/2] Decide whether a matrix has an inverse without GenericTensor Asking Matrix.Inverse from division and from a power made GenericTensor's InvertMatrix, and through Determinant its DeterminantLaplace, reachable from every node. Both are compiled at run time and are not safe in a native build, so every AOT publish failed with IL3053. Whether a matrix may have an inverse is now asked of the exact elimination, which does not touch GenericTensor, and the inverse itself is Pow(-1), which was reachable before. A determinant the elimination cannot write is not known to be zero. The C++ wrapper's native publish and the trimmed and AOT smoke tests pass locally, the latter 45 of 45. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- ...aluation.Continuous.Arithmetics.Classes.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs index ea4a2a726..3e4513f13 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Arithmetics.Classes.cs @@ -177,7 +177,7 @@ protected override Entity InnerSimplify(bool isExact) => // A divisor with no inverse, singular or not square, leaves the quotient // written. A matrix over a number is still spread over the entries. // https://github.com/asc-community/AngouriMath/issues/2141 - (_, Matrix divisor) => divisor.Inverse is { } inverse ? (a * inverse).InnerSimplified(isExact) : a / b, + (_, Matrix divisor) => Powf.MayHaveAnInverse(divisor) ? (a * divisor.Pow(-1)).InnerSimplified(isExact) : a / b, (Integer(0), var n0) => n0.Evaled is Complex c ? c.IsZero ? MathS.NaN : 0 @@ -292,11 +292,26 @@ private static bool TryPower(Matrix m, int exp, out Entity res) // an inverse has negative powers: Pow threw for a singular one, out of eval and // Simplify alike, and so did dividing by one once division used the inverse. // https://github.com/asc-community/AngouriMath/issues/2141 - var defined = m.IsSquare && (exp >= 0 || m.Inverse is not null); + var defined = m.IsSquare && (exp >= 0 || MayHaveAnInverse(m)); res = defined ? m.Pow(exp) : 0; return defined; } + /// + /// Whether a square matrix may have an inverse: not where its determinant is zero. + /// + /// + /// Asked of the exact elimination, not of or + /// , which can go through GenericTensor's compiled + /// operations: those are not safe in a native build, and every node reaches this. + /// Using them here broke the native build of every wrapper. A determinant the + /// elimination cannot write is not known to be zero. + /// + internal static bool MayHaveAnInverse(Matrix m) + => m.IsSquare + && (Functions.PolynomialDeterminant.Of(m.RowCount, (row, column) => m[row, column]) is not { } determinant + || determinant.InnerSimplified != 0); + // Re(x) = x/2 * (1 + 1/sgn(x)^2) internal static Entity Re(Entity x) => x / 2 * (1 + 1 / new Signumf(x).Pow(2));