Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

/// <summary>
/// Whether a square matrix may have an inverse: not where its determinant is zero.
/// </summary>
/// <remarks>
/// Asked of the exact elimination, not of <see cref="Matrix.Inverse"/> or
/// <see cref="Matrix.Determinant"/>, 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.
/// </remarks>
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));

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,29 @@ private static bool HasAnUnreadSize(Entity side)
_ => null,
};

/// <summary>
/// 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, <c>[1, 2] = [1, 3]</c> was the vector
/// <c>[True, False]</c>, while <c>[1, 2] = [1, 2]</c> was <c>True</c>.
/// https://github.com/asc-community/AngouriMath/issues/2141
/// </summary>
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);
}

/// <inheritdoc/>
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)
Expand Down
45 changes: 45 additions & 0 deletions Sources/Tests/UnitTests/Algebra/MatrixTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Matrix>(expression.ToEntity().Evaled);
Assert.IsNotType<Matrix>(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());
}
}
}
Loading