Linear Algebra vs Element-wise Operations

The * and / operators on Vector/Matrix are always element-wise — they are not matrix multiplication or linear system solving. This is the #1 source of confusion.

Several method names are overridden on TMtx/Matrix so that on a two-dimensional container they perform the real linear-algebra operation instead of the element-wise one their base-class signature implies. The key overrides are:

  • TMtx.Mul(A, B) with two TMtx arguments → matrix multiplication (self := A * B), not element-wise.
  • TMtx.Invmatrix inverse (self := self^-1), not per-element 1/x. On TVec/Vector the inherited Inv is per-element.

So whether .Mul/.Inv is element-wise or linear-algebra depends on the receiver type: element-wise on TVec/Vector, linear-algebra on TMtx/Matrix — the polymorphism is deliberate.

The Critical Distinction

OperationElement-wise formLinear-algebra form
Multiply — * operatorc := a * b on Vector/Matrix (always per-element)— (operators never do linear algebra)
Multiply — methodc.Mul(a, b) on TVec/Vector (per-element)C.Mul(A, B) on TMtx/Matrix (matrix product) — overridden, see above
Dividec := a / b on Vector/Matrix (per-element)MtxExpr.Divide(b, A) — solves A^T · x = b
Back-divideMtxExpr.LDivide(A, b) — solves A · x = b
Inversev.Inv on TVec/Vector (per-element 1/x)M.Inv on TMtx/Matrix (matrix inverse) — overridden

Two API Layers

Vector/Matrix — value types with operator overloading, return new objects:

c := a + b;     // allocates result from object cache
c := a * b;     // ELEMENT-WISE multiply (not matrix product!)
c := a * 2.0;   // scalar multiply
c = a + b;     // result object taken from the object cache
c = a * b;     // ELEMENT-WISE multiply (not matrix product!)
c = a * 2.0;   // scalar multiply

TVec/TMtx — pool-managed types, write result into calling object (self-as-destination):

// On TVec (lowercase v, a, b are TVec — vectors):
v.Add(a, b);       // v := a + b       (no allocation)
v.Mul(a, b);       // v := a .* b      (element-wise — the only meaning on 1D)
v.Mul(2.0);        // v := v * 2.0     (scalar multiply)
v.Inv;             // v := 1 ./ v      (element-wise 1/x)

// On TMtx (uppercase M, A, B are TMtx — matrices) the same method
// names are OVERRIDDEN to perform real linear algebra:
M.Add(A, B);       // M := A + B       (element-wise; addition is the same)
M.Mul(A, B);       // M := A * B       (matrix product — not element-wise)
M.MulElem(A, B);   // M := A .* B      (explicit element-wise product on TMtx)
M.Inv;             // M := M^-1        (matrix inverse — not per-element)
// On Vector/TVec (lowercase v, a, b are vectors):
v.Add(a, b);       // v = a + b       (no allocation)
v.Mul(a, b);       // v = a .* b      (element-wise — the only meaning on 1D)
v.Mul(2.0);        // v = v * 2.0     (scalar multiply)
v.Inv();           // v = 1 ./ v      (element-wise 1/x)

// On Matrix/TMtx (uppercase M, A, B are matrices) the same method
// names are OVERRIDDEN to perform real linear algebra:
M.Add(A, B);       // M = A + B       (element-wise; addition is the same)
M.Mul(A, B);       // M = A * B       (matrix product — not element-wise)
M.MulElem(A, B);   // M = A .* B      (explicit element-wise product on TMtx)
M.Inv();           // M = M^-1        (matrix inverse — not per-element)

The receiver type decides the meaning: on a 1D container (TVec/Vector) every .Mul/.Inv is element-wise because there is nothing else it can be. On a 2D container (TMtx/Matrix) .Mul(A, B) and .Inv mean real linear algebra. Use .MulElem on TMtx when you actually want the Hadamard (per-element) product.

Matrix Multiplication

With Vector/Matrix (via MtxExpr static functions)

In C++ they are free functions in the namespace Dew::Math::Units::MtxExpr (header Units.MtxExpr.h); with using namespace Dew::Math::Units::MtxExpr; they are called unqualified, as below.

These are the primary way to do linear algebra with expression-friendly syntax.

// C++: free functions in Dew::Math::Units::MtxExpr
Vector y = Mul(A, x);          // A * x  (matrix × vector)
Vector z = Mul(x, A);          // x * A  (vector × matrix)
Matrix C = Mul(A, B);          // A * B  (matrix × matrix)
Matrix P = Mul(x, z);          // x * z^T (outer product)

// With transpose/conjugate
Matrix T = Mul(A, B, TMtxOperation::opTran);   // A^T * B

// Chain: A * B * C * D
Matrix R = Mul(A, B, C, D);
// or: Mul(DewArray<TMtx*>{A, B, C, D});

With TVec/TMtx (method calls, zero allocation)

// Matrix × matrix: C.Mul(A, B)
C.Mul(A, B);                                              // C = A * B
C.Mul(A, B, TMtxOperation::opTran);                       // C = A^T * B
C.Mul(A, B, TMtxType::mtSymmetric, TMtxType::mtGeneral);  // exploit symmetry

// Matrix × vector: y.TensorProd(A, x) or y.TensorProd(x, A)
y.TensorProd(A, x);                                       // y = A * x
y.TensorProd(x, A);                                       // y = x * A

// Outer product: C.TensorProd(colVec, rowVec)
C.TensorProd(x, y);                                       // C = x * y^T

// Element-wise matrix multiply (Hadamard product):
C.MulElem(A, B);                                          // C = A .* B

Chain multiplication — one call, any number of matrices

TMtx.Mul has overloads for three- and four-factor chains, and TMtx.MulArray accepts an arbitrary TMtxArray so the whole chain is computed in a single call. These are preferred over manual chaining (tmp.Mul(A,B); C.Mul(tmp,D);) because the implementation picks the most efficient evaluation order and avoids redundant temporaries.

// Three and four matrices in one call
R.Mul(A, B, C);                              // R = A * B * C
R.Mul(A, B, C, D);                           // R = A * B * C * D

// Arbitrary chain via DewArray<TMtx*> (MulArray is a TMtx method: use ->)
DewArray<TMtx*> chain = {M0, M1, M2, M3, M4};
R->MulArray(chain);                          // R = M0*M1*M2*M3*M4

DewArray<int> transp = {0, 1, 0, 0, 1};      // transpose M1 and M4
R->MulArray(chain, transp);                  // R = M0 * M1^T * M2 * M3 * M4^T

Chain factors must all be matrices. The element type of TMtxArray is TMtx, and the fixed-arity Mul(A, B, C[, D]) overloads take TMtx arguments. There is no heterogeneous [TMtx, TVec, TMtx, ...] overload. For vector weights and vectors-as-operands see the next two subsections.

Folding a vector weight into a chain — `MulDiagLeft` / `MulDiagRight`

When one of the factors in a chain is a diagonal matrix whose diagonal is known as a TVec, you don't need to materialise the full diagonal matrix. TMtx exposes four overloads that multiply in a vector treated as the main diagonal — equivalent to row or column scaling:

// Two-arg: operate on the calling matrix A
A.MulDiagLeft(v);          // A = diag(v) * A   (row-scale)
A.MulDiagRight(w);         // A = A * diag(w)   (column-scale)

// Three-arg: explicit source matrix M, store into A
A.MulDiagLeft(v, M);       // A = diag(v) * M
A.MulDiagRight(M, w);      // A = M * diag(w)

From the summaries: "DiagMtx contains the values of the main diagonal of the diagonal matrix. … This operation is the same as calling TMtx.ScaleRows" (left form) or "TMtx.ScaleCols" (right form). The vector length must match self.Cols for MulDiagLeft and self.Rows for MulDiagRight.

Example — A · diag(w) · B in two calls, no diagonal matrix built:

// Compute R = A * diag(w) * B  without allocating a diagonal matrix.
tmp.MulDiagRight(A, w);       // tmp = A * diag(w)  (column-scale A)
R.Mul(tmp, B);                // R = tmp * B

// Or fold into a longer chain:
R->MulArray(DewArray<TMtx*>{A, B, C, D});   // R = A*B*C*D
R.MulDiagRight(w);                          // R = R * diag(w)   (post-scale columns)

This is strictly faster than materialising diag(w) as a square TMtx and including it in a MulArray — the scaling step is O(n²) instead of the O(n³) a full matrix multiply would cost.

Sparse matrix multiplication — `TSparseMtx.MulLeft` / `MulRight`

TSparseMtx (sparse container) does not share TMtx's Mul/MulArray API. Instead it exposes MulLeft / MulRight, each with a matrix-matrix and a matrix-vector overload. These are the only dense-API methods that take a TVec operand directly as a vector (not as a diagonal):

// Matrix times matrix
S.MulLeft(X, Y);              // Y = X * S       (X multiplies S from the left)
S.MulRight(X, Y);             // Y = S * X       (X multiplies S from the right)

// Matrix times vector
S.MulLeft(x, y);              // y = x * S
S.MulRight(x, y);             // y = S * x

Summaries from the DB: "Multiply the sparse matrix from left" / "Multiply sparse matrix from left with vector X and place the result in vector Y" — and the symmetrical right-hand forms. Use these when your matrix is stored as a TSparseMtx; for dense matrix-vector products use TVec.TensorProd(Mtx, Vec) as shown above.

Note: TSparseMtx has no chain overload — MulLeft/MulRight are always binary. For a sparse chain you apply them one factor at a time.

Solving Linear Systems (A·x = b)

With MtxExpr (returns Vector/Matrix)

// Solve A * x = b  (left-divide, like MATLAB's A\b)
Vector x = LDivide(A, b);         // uses LQR internally
Matrix X = LDivide(A, B);         // multi-column RHS

// Solve x * A = b  (right-divide, like MATLAB's b/A)
Vector xr = Divide(b, A);

With TMtx (method calls, more control)

// LU-based (fastest for square, well-conditioned)
A.LUSolve(b, x);                          // A * x = b, general
A.LUSolve(b, x, TMtxType::mtSymmetric);   // exploit symmetry

// QR-based (robust, handles rank deficiency)
A.LQRSolve(b, x);                         // full rank
int rank = A.LQRSolve(b, x, 1e-9);        // rank-deficient, returns effective rank

// SVD-based (minimum-norm solution)
rank = A.SVDSolve(b, x, s, 1e-9);         // s receives singular values

Decompositions

With TMtx (in-place, reuse objects)

// Cholesky (symmetric positive definite)
if (A.Cholesky()) { /* ... */ }          // A is replaced by factor

// LU decomposition
A.LU(LU_result, pivots);

// QR / LQ decomposition
A.LQR(L, Q, R);                          // economy-size by default
A.LQR(L, Q, R, P);                       // with column pivoting

// SVD
A.SVD(U, S, V);                          // singular values and vectors
A.SVD(S);                                // singular values only

// Eigenvalues
A.Eig(D);                                // eigenvalues in D
A.Eig(D, TMtxType::mtSymmetric);         // symmetric -> real eigenvalues
A.Eig(VL, D, VR);                        // with eigenvectors

MtxExpr convenience wrappers

Vector eigenvalues = Eig(A);              // eigenvalues
Vector singularvals = SVD(A);             // singular values
bool ok = Cholesky(A);                    // in-place, returns boolean

Matrix Inverse

// MtxExpr (returns Matrix value type)
Matrix B = Inverse(A);                              // general inverse
Matrix Bs = Inverse(A, TMtxType::mtSymmetric);      // exploit symmetry

// TMtx method (in-place)
A.Inv();                                  // A is replaced by A^(-1)
A.Inv(TMtxType::mtSymmetric);             // symmetric inverse

Important: Prefer LDivide(A, b) over Inverse(A) * b. Solving is faster and more numerically stable than computing the explicit inverse.

Performance: Vector/Matrix vs TVec/TMtx

object cache. C# language design prevents user-defined memory allocation control for value types. Every operator expression allocates new managed objects. Use TVec/TMtx with CreateIt/FreeIt for best performance.

In C++, Vector, Matrix, VectorInt and MatrixInt DO benefit from the object cache: every temporary of an expression is borrowed from the cache and returned by its destructor when the expression ends. Operator expressions are efficient in C++.

// Operators are efficient in C++ (the object cache supplies the temporaries)
c = a + b;           // temporary from cache, returned automatically
d = c * e;           // same

// Methods are still faster for hot loops (zero allocation)
c.Add(a, b);
c.Mul(e);

// Linear algebra — same trade-off:
Vector y = Mul(A, x);   // convenient, result taken from the object cache
y.TensorProd(A, x);     // writes into the existing y

Quick Reference

The * and / operators on Vector/Matrix are always element-wise — never matrix multiplication. Use the method calls or MtxExpr free functions for linear algebra.

TaskVector/Matrix operatorMtxExpr / free functionTMtx/TVec method
Element-wise add / subc := a + b, a - bc.Add(a, b), c.Sub(a, b)
Element-wise multiplyc := a * bc.Mul(a, b) on TVec; C.MulElem(A, B) on TMtx
Element-wise dividec := a / bc.DivideVec(a, b) (per-element); see also DivideBy(N) for N / self
Element-wise 1/xv.Inv on TVec/Vector
Matrix × matrix(operators are element-wise!)Mul(A, B) → MatrixC.Mul(A, B) on TMtx
Matrix × vectorMul(A, x) → Vectory.TensorProd(A, x)
Vector × matrixMul(x, A) → Vectory.TensorProd(x, A)
Outer product x · yᵀMul(x, y) → MatrixC.TensorProd(x, y)
Row-scale (diag(w) · A)A.MulDiagLeft(w)ScaleRows
Column-scale (A · diag(w))A.MulDiagRight(w)ScaleCols
Sparse mat × mat / mat × vecS.MulLeft(X, Y) / MulRight(X, Y) on TSparseMtx
Chain matrix productMul(A, B, C[, D]) → MatrixR.Mul(A, B, C[, D]), R.MulArray([...])
Solve A · x = bLDivide(A, b) → VectorA.LUSolve(b, x) or A.LQRSolve(b, x)
Solve x · A = bDivide(b, A) → Vector
Matrix inverseInverse(A) → MatrixA.Inv (overridden on TMtx)
EigenvaluesEig(A) → VectorA.Eig(D)
SVDSVD(A) → VectorA.SVD(U, S, V)