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..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
@@ -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) => 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
@@ -279,11 +288,30 @@ 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 || 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));
@@ -293,6 +321,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());
+ }
}
}