diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 10896a986..04bb60337 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -300,6 +300,18 @@ are now also found wherever the `1` and the square sit in a sum, under the condi | `"1 + tan(x)^2 - sec(x)^2".Simplify()` | `1 + tan(x) ^ 2 - sec(x) ^ 2` | `0 provided not cos(x) = 0` | | `"a + 1 + tan(y)^2 - sec(y)^2".Simplify()` | `a + 1 + tan(y) ^ 2 - sec(y) ^ 2` | `a provided not cos(y) = 0` | +### A scalar over a matrix argument is not compiled + +**Not silent: what compiled to a wrong value now throws `UncompilableNodeException`.** A scalar over a matrix that is an +argument of the compiled function was compiled as the scalar over every element, where evaluation takes it as the scalar +times the inverse. The inverse is not compiled, as a negative matrix power is not, so the quotient is refused. A matrix +written into the expression is inverted before it is compiled, as before +([#2287](https://github.com/asc-community/AngouriMath/issues/2287)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"x / A"`, compiled over `GenTensor` and called at `A = [[1, 2], [3, 4]]`, `x = 2` | `[[2, 1], [0.667, 0.5]]` | throws `UncompilableNodeException` | + ### A square system is not answered with rows that are no solution **Silent: a row that is no solution is gone, and a system with a row that fails for some values is `null`.** diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 986ac8813..2e7b9a242 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -164,6 +164,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Common/ListArgumentOverloadTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Core/Multithreading/SettingsAcrossAsync.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs index 287671e14..8e8af054e 100644 --- a/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs +++ b/Sources/AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.cs @@ -249,14 +249,13 @@ private sealed class Kit internal MethodInfo SubtractFromScalar { get; } internal MethodInfo MultiplyScalar { get; } internal MethodInfo DivideScalar { get; } - internal MethodInfo DivideFromScalar { get; } internal MethodInfo Power { get; } private Kit(Type tensor, MethodInfo empty, MethodInfo set, MethodInfo add, MethodInfo subtract, MethodInfo multiply, MethodInfo addScalar, MethodInfo subtractScalar, MethodInfo subtractFromScalar, MethodInfo multiplyScalar, - MethodInfo divideScalar, MethodInfo divideFromScalar, MethodInfo power) - => (Tensor, Empty, Set, Add, Subtract, Multiply, AddScalar, SubtractScalar, SubtractFromScalar, MultiplyScalar, DivideScalar, DivideFromScalar, Power) - = (tensor, empty, set, add, subtract, multiply, addScalar, subtractScalar, subtractFromScalar, multiplyScalar, divideScalar, divideFromScalar, power); + MethodInfo divideScalar, MethodInfo power) + => (Tensor, Empty, Set, Add, Subtract, Multiply, AddScalar, SubtractScalar, SubtractFromScalar, MultiplyScalar, DivideScalar, Power) + = (tensor, empty, set, add, subtract, multiply, addScalar, subtractScalar, subtractFromScalar, multiplyScalar, divideScalar, power); // An expression tree is how C# names a method without a string: the call inside it // is resolved at compile time, closed over T and TWrapper, and rooted for the trimmer. @@ -274,7 +273,6 @@ internal static Kit For() where TWrapper : struct, IOperations Named, GenTensor>>((s, a) => TensorCompilation.SubtractFromScalar(s, a)), Named, T, GenTensor>>((a, s) => TensorCompilation.MultiplyScalar(a, s)), Named, T, GenTensor>>((a, s) => TensorCompilation.DivideScalar(a, s)), - Named, GenTensor>>((s, a) => TensorCompilation.DivideFromScalar(s, a)), Named, int, GenTensor>>((a, n) => TensorCompilation.Power(a, n))); } @@ -380,6 +378,10 @@ public static GenTensor MultiplyScalar(GenTensor DivideScalar(GenTensor a, T scalar) where TWrapper : struct, IOperations => Elementwise(a, x => default(TWrapper).Divide(x, scalar)); /// The scalar over every element. + /// + /// Not what a scalar over a matrix means, which is the scalar times the inverse, and so not what the compiler + /// makes of one. https://github.com/asc-community/AngouriMath/issues/2287 + /// public static GenTensor DivideFromScalar(T scalar, GenTensor a) where TWrapper : struct, IOperations => Elementwise(a, x => default(TWrapper).Divide(scalar, x)); /// The matrix power, a non-negative whole exponent, by repeated squaring. @@ -452,7 +454,12 @@ internal static Expression Build(Entity.Matrix matrix, IReadOnlyList Entity.Sumf => Expression.Call(rightKit!.AddScalar, right, value), Entity.Minusf => Expression.Call(rightKit!.SubtractFromScalar, value, right), Entity.Mulf => Expression.Call(rightKit!.MultiplyScalar, right, value), - Entity.Divf => Expression.Call(rightKit!.DivideFromScalar, value, right), + // A scalar over a matrix is the scalar times its inverse, as evaluation has it, and the + // inverse is not compiled, as a negative power is not: dividing by every element + // instead gave 2 / [[1, 2], [3, 4]] as [[2, 1], [2/3, 1/2]]. + // https://github.com/asc-community/AngouriMath/issues/2287 + Entity.Divf => throw new UncompilableNodeException( + "A scalar over a matrix is the scalar times its inverse, which is not compiled: invert the matrix first"), _ => null, }; } diff --git a/Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs b/Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs new file mode 100644 index 000000000..d1c216f5f --- /dev/null +++ b/Sources/Tests/UnitTests/Common/AScalarOverAMatrixArgumentTest.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath; +using AngouriMath.Core.Compilation.IntoLinq; +using AngouriMath.Core.Exceptions; +using AngouriMath.Extensions; +using GenericTensor.Core; +using Xunit; + +namespace AngouriMath.Tests.Common +{ + /// + /// A scalar over a matrix argument is not compiled, where it was compiled as the scalar over every element: evaluation + /// takes it as the scalar times the inverse, and the inverse is not compiled, as a negative matrix power is not. A matrix + /// written into the expression is inverted before it is compiled, and that is unchanged. + /// #2287 + /// + [Trait("Area", "Common")] + public sealed class AScalarOverAMatrixArgumentTest + { + [Fact] + public void ItIsRefused() + => Assert.Throws(() => "x / A".ToEntity() + .Compile, double, GenTensor>("A", "x")); + + [Fact] + public void AMatrixArgumentOverAScalarIsStillCompiled() + { + var matrix = GenTensor.CreateMatrix(new double[,] { { 1, 2 }, { 3, 4 } }); + var divided = "A / x".ToEntity() + .Compile, double, GenTensor>("A", "x")(matrix, 2); + Assert.Equal(new[] { 0.5, 1, 1.5, 2 }, new[] { divided[0, 0], divided[0, 1], divided[1, 0], divided[1, 1] }); + } + + [Fact] + public void AWrittenMatrixIsInvertedAsBefore() + { + var inverse = "x / [[1, 0], [2, 4]]".ToEntity().Compile>("x")(1); + Assert.Equal(new[] { 1, 0, -0.5, 0.25 }, new[] { inverse[0, 0], inverse[0, 1], inverse[1, 0], inverse[1, 1] }); + } + } +}