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()); + } } }