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
21 changes: 21 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,27 @@ value is a number compiles. One whose value is a matrix is refused as before, an
| `"A * B".ToEntity().Compile<double[,], double[,], double[,]>("A", "B")` | throws `UncompilableNodeException` | the matrix product |
| `"[1, 0]T * [[a, b], [c, d]] * [0, 1]".ToEntity().Compile("a", "b", "c", "d")` | throws `UncompilableNodeException` | `b` |

### A function of a compiled matrix argument is the function of each entry

**Addition, not silent.** Under the LINQ compiler a function of a matrix argument compiles to what evaluation makes of
it once the matrix is put in: the function of each entry. That holds for a function of one argument, `sin(A)`, `exp(A)`,
`ln(A)`, `abs(A)`, and of two where evaluation maps it over a matrix and a number or two matrices of one shape: a number
to the power of a matrix, `log`, `max`, `min`, `mod`, `binomial` and the rest. It is not the matrix function: `exp(A)` is
not the matrix exponential, which evaluation does not compute either. A power with a matrix for its base stays the
matrix power. `abs` of a vector, a column or a row known when compiling to be one, is its norm, `sqrt(sum |v_k|^2)`, a
number ([#2423](https://github.com/asc-community/AngouriMath/issues/2423)); of a two-dimensional array whose shape is known
only at the call it is the modulus of each entry, and a column or a row of more than one entry there throws
`InvalidMatrixOperationException`. A matrix
of statements, as `A < 0` is, has no compiled form. A one-dimensional array, `double[]` or `Complex[]`, is a column
vector, as an argument and as the result.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"sin(A)"`, compiled over `GenTensor<double>` and called at `A = [[1, 2], [3, -4]]` | throws `UncompilableNodeException` | `[[0.841, 0.909], [0.141, 0.757]]` |
| `"exp(A)"`, `"2 ^ A"`, `"max(A, 0)"`, the same | throws `UncompilableNodeException` | the function of each entry |
| `"abs(v)"`, compiled from `double[]` to `double` and called at `[3, -4]` | throws `AngouriBugException` | `5` |
| `"sin(v)"`, compiled from `double[]` to `double[]` | throws `KeyNotFoundException` | the sine of each entry |

### A matrix argument is not simplified as a number before compiling

**Silent: what compiled to a wrong value now gives the right one.** The LINQ compiler simplifies an expression with a
Expand Down
6 changes: 6 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.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

[AngouriMath/Functions/Compilation/IntoLinq/TensorCompilation.Elementwise.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

[AngouriMath/Functions/Compilation/IntoLinq/ThreeValuedLogic.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

Expand Down Expand Up @@ -495,6 +498,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/CompileMatrixTest.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/CompileAFunctionOfAMatrixTest.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

[AngouriMath/Functions/IntervalArithmetic.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

Expand Down
18 changes: 12 additions & 6 deletions Sources/AngouriMath/Docs/Usage/Exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ take.

`Compile` was asked for a function it cannot build. Two distinct causes, and the message says which:

- a node with no compiled form — a derivative, an integral, a limit, a function of a matrix;
- a node with no compiled form — a derivative, an integral, a limit, a comparison of a matrix;
- a variable the expression mentions that the compilation was not given. The message names the
variable and lists the ones it *was* given, since `Compile("x")` on `a * x` failing used to
surface as a bare `KeyNotFoundException` about some dictionary.
Expand All @@ -170,10 +170,13 @@ to and to pass a matrix in as: `"A * B".ToEntity().Compile<GenTensor<double, Dou
GenTensor<double, DoubleOperations>, GenTensor<double, DoubleOperations>>("A", "B")` is the matrix
product, `2 A + B - 1` scales, adds and shifts element by element, `A ^ 3` is the matrix power
([#526](https://github.com/asc-community/AngouriMath/issues/526)). A two-dimensional array of the
element type -- `double[,]`, `Complex[,]` -- is a matrix too, as an argument and as the result.
Compiling a matrix as a number, or a number as a matrix, or `sin(A)`, is still this exception, and
so is dividing by a matrix of `int`, `long` or `BigInteger`, whose inverse is not one of whole
numbers.
element type -- `double[,]`, `Complex[,]` -- is a matrix too, as an argument and as the result,
and a one-dimensional array -- `double[]` -- is a column vector. A function of a matrix is the
function of each entry, as evaluation has it: `sin(A)`, `exp(A)`, `2 ^ A`, `log(2, A)`,
`max(A, B)`; `abs` of a vector, a column or a row, is its norm. Compiling a matrix as a number, unless it is known
to be 1 x 1, or a number as a matrix, is still this exception, as are a comparison or equality
of matrices, a matrix to the power of a matrix, and dividing by a matrix of `int`, `long` or
`BigInteger`, whose inverse is not one of whole numbers.

### `CannotEvalException`

Expand Down Expand Up @@ -237,7 +240,10 @@ involved.

A compiled matrix raises it as well: `Compile` where the shapes of matrices written into the
expression do not fit, and the compiled delegate where the shapes of matrix arguments do not, or
where a matrix power's exponent is not a whole number.
where a matrix power's exponent is not a whole number. So does a delegate given an empty array, one
returning a vector that is given a matrix of more than one column, and one taking `abs` of a
two-dimensional array that turns out to be a column or a row, whose modulus is a norm rather than a
matrix: pass a column as a one-dimensional array for its norm.

### `BadMatrixShapeException`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,10 @@ public virtual Expression ConvertType(Expression expr, Type type)
public virtual Expression ConvertUnaryNode(Expression e, Entity typeHolder)
{
e = NumericOperand(e);
// A function of a matrix has no compiled form here: the tensor operations are the
// arithmetic ones, and sin(A) is not an element-by-element sine of a matrix.
// A function of a matrix is the function of each entry, which is what evaluation makes of it, and not a
// matrix function: sin(A) is not the power series of A. https://github.com/asc-community/AngouriMath/issues/526
if (TensorCompilation.IsTensor(e.Type))
throw new UncompilableNodeException(
$"There is no compiled form for {typeHolder.GetType().Name} of a matrix.");
return TensorCompilation.ElementwiseUnary(e, typeHolder, entry => ConvertUnaryNode(entry, typeHolder), ConvertType);
return typeHolder switch
{
Sinf when ShouldBeAtLeastDouble(e) is var newE => Expression.Call(GetDef("Sin", 1, newE.Type), newE),
Expand Down Expand Up @@ -238,9 +237,15 @@ public virtual Expression ConvertBinaryNode(Expression left, Expression right, E
// A matrix in either place: the tensor operation of the same name, the scalar side
// converted to the element type. https://github.com/asc-community/AngouriMath/issues/526
if (TensorCompilation.IsTensor(left.Type) || TensorCompilation.IsTensor(right.Type))
{
// A matrix of whole numbers written beside a matrix argument over double is a matrix over double.
(left, right) = TensorCompilation.Widened(left, right, ConvertType);
return TensorCompilation.Binary(typeHolder, left, right, ConvertType)
// A function of two arguments one of which is a matrix, where evaluation applies it entry by entry.
?? TensorCompilation.ElementwiseBinary(typeHolder, left, right, (l, r) => ConvertBinaryNode(l, r, typeHolder), ConvertType)
?? throw new UncompilableNodeException(
$"The node of type {typeHolder.GetType()} has no compiled form over a matrix.");
}

(left, right) = EqualizeTypesIfAble(NumericOperand(left), NumericOperand(right));
return typeHolder switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal static TDelegate Compile<TDelegate>(
) where TDelegate : Delegate
{
bool IsMatrixArgument(Variable variable) => typesAndNames.Any(c => c.variable == variable && TensorCompilation.IsMatrixType(c.type));
Type? TypeOf(Variable variable) => typesAndNames.FirstOrDefault(c => c.variable == variable).type;
// A matrix has no compiled form, but an expression built out of matrices often
// has a value that is an ordinary number -- [0, 1]T * [[a, b], [c, d]] * [1, 0]
// is c -- and there is nothing to stop that being compiled. Nothing simplified
Expand All @@ -36,7 +37,9 @@ internal static TDelegate Compile<TDelegate>(
StatementPositions.Check(expr);
// Two matrices whose shapes are written into the expression and do not fit are refused here, rather than by
// a delegate that throws at every point it is called at. https://github.com/asc-community/AngouriMath/issues/526
TensorCompilation.CheckShapes(expr, IsMatrixArgument);
Func<Variable, (int Rows, int Columns)?> argumentShape = variable => TensorCompilation.ShapeOfArgument(TypeOf(variable));
TensorCompilation.CheckShapes(expr, argumentShape);
CheckResultShape(expr, returnType, argumentShape);
if (MathS.Settings.RefuseNonRealCompilation && returnType is { } real && IsRealType(real)
&& typesAndNames.All(c => IsRealType(c.type)) && !IsRealWhereverDefined(expr, typesAndNames))
throw new UncompilableNodeException(
Expand Down Expand Up @@ -77,11 +80,15 @@ static bool IsComplex(Type? type)
var constantType = typesAndNames.Any(c => IsComplex(c.type)) || IsComplex(returnType)
? typeof(System.Numerics.Complex)
: matrixElements.Count == 1 ? matrixElements[0] : null;
// Only a matrix argument can be a vector whose shape is not written out: abs of a written one is its norm
// after simplification already.
if (typesAndNames.Any(c => TensorCompilation.IsMatrixType(c.type)))
BuildNorms(expr, argumentShape, subexpressionsCache, variableAssignments, localVars, protocol, constantType);
var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol, constantType);
// A matrix known to be 1 x 1 asked for as a number is its entry, as evaluation has it. Any other matrix
// is refused as a number by ConvertType.
if (TensorCompilation.IsTensor(tree.Type) && returnType is not null && !TensorCompilation.IsMatrixType(returnType)
&& TensorCompilation.IsOneByOne(expr, IsMatrixArgument))
&& TensorCompilation.IsOneByOne(expr, argumentShape))
tree = TensorCompilation.ToScalar(tree);
var treeWithLocals = Expression.Block(localVars, variableAssignments.Append(tree));
Expression entireExpression = returnType is not null ? protocol.ConvertType(treeWithLocals, returnType) : treeWithLocals;
Expand All @@ -98,6 +105,43 @@ or NotSupportedException
}
}

/// <summary>
/// Every <c>abs</c> of a column vector, compiled to its norm ahead of the rest and cached, so that the tree
/// built after it reads the number. Of a vector, <c>abs</c> is a number, and of any other matrix a matrix,
/// and only the shape known when compiling can tell which.
/// https://github.com/asc-community/AngouriMath/issues/2423
/// </summary>
private static void BuildNorms(Entity expr, Func<Variable, (int Rows, int Columns)?> argumentShape,
Dictionary<Entity, ParameterExpression> cachedSubexpressions, List<Expression> variableAssignments,
List<ParameterExpression> newLocalVars, CompilationProtocol protocol, Type? constantType)
{
foreach (var norm in TensorCompilation.Norms(expr, argumentShape))
{
if (cachedSubexpressions.ContainsKey(norm))
continue;
var vector = BuildTree(norm.Argument, cachedSubexpressions, variableAssignments, newLocalVars, protocol, constantType);
if (!TensorCompilation.IsTensor(vector.Type))
continue;
var value = Expression.Variable(typeof(double));
variableAssignments.Add(Expression.Assign(value, TensorCompilation.Norm(vector)));
newLocalVars.Add(value);
cachedSubexpressions[norm] = value;
}
}

/// <summary>
/// A matrix known when compiling not to be a column, asked for as a vector, is refused then rather than by a
/// delegate that throws at every call.
/// </summary>
private static void CheckResultShape(Entity expr, Type? returnType, Func<Variable, (int Rows, int Columns)?> argumentShape)
{
if (returnType is null || !TensorCompilation.IsVectorArray(returnType) || TensorCompilation.Shape(expr, argumentShape) is not { } shape)
return;
if (shape.Columns > 1)
throw new InvalidMatrixOperationException(
$"`{expr.Stringize()}` is a matrix of {shape.Columns} columns, not a column vector, and cannot be compiled as {returnType}");
}

private static bool IsRealType(Type type)
=> type == typeof(double) || type == typeof(float) || type == typeof(int) || type == typeof(long)
|| type == typeof(System.Numerics.BigInteger);
Expand Down
Loading
Loading