Generated reference › API — ICoreBlocks/ICoreMath/Foundation
kind: generated#api#icoreblocks-icoremath-foundation

API — ICoreBlocks/ICoreMath/Foundation

The public contract of 12 header(s) under src/ICoreBlocks/ICoreMath/Foundation — 12 class/struct definition(s), 424 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.

ICoreComplexMatrix.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreComplexMatrix.h

ICoreComplexMatrix#

ICoreComplexMatrix.h:32 · class · pImpl · 37 declaration(s)

A 2-D matrix of complex doubles -- the console's Kind::Complex value (the core-MATLAB row C0.2), and the wrapper that replaces the N x 2 [real, imag] convention every complex-producing function...

class ICoreComplexMatrix {
public:
    ICoreComplexMatrix();
    ICoreComplexMatrix(size_t rows, size_t columns);
    explicit ICoreComplexMatrix(const std::complex<double>& scalar);
    ICoreComplexMatrix(const ICoreComplexMatrix& other);
    ICoreComplexMatrix& operator=(const ICoreComplexMatrix& other);
    ~ICoreComplexMatrix();

    // Every real matrix IS a complex one with zero imaginary parts; the two
    // builders below are the only way in, so a half-built value cannot exist.
    static ICoreComplexMatrix fromReal(const ICoreMatrix& real);
    static ICoreComplexMatrix fromParts(const ICoreMatrix& real, const ICoreMatrix& imaginary,
                                        std::string* whyNot = nullptr);
    static ICoreComplexMatrix fromList(const std::vector<ICoreComplexVariable>& values);

    size_t size1() const;
    size_t size2() const;
    size_t numel() const;
    bool   isScalar() const;
    bool   isEmpty() const;
    bool   isVector() const;

    std::complex<double>& operator()(size_t i, size_t j);
    const std::complex<double>& operator()(size_t i, size_t j) const;

    // The four projections MATLAB names (C5.44-C5.46), each answering a REAL
    // matrix of the same shape.
    ICoreMatrix realPart() const;
    ICoreMatrix imagPart() const;
    ICoreMatrix magnitude() const;   // abs(z)
    ICoreMatrix phase() const;       // angle(z), atan2(imag, real)

    // MATLAB's isreal() is a question about the STORAGE, not the values: a
    // value built as complex answers false even when every imaginary part is
    // zero. hasImaginaryPart() is the other question -- whether any imaginary
    // part is actually non-zero -- and it is what decides whether a result
    // narrows back to a real matrix.
    bool hasImaginaryPart() const;
    ICoreMatrix narrowToReal() const;   // the real parts, whatever the imaginary ones are

    ICoreComplexMatrix conjugate() const;
    ICoreComplexMatrix transpose() const;    // .'  -- no conjugation
    ICoreComplexMatrix ctranspose() const;   // '   -- conjugate transpose
    ICoreComplexMatrix negate() const;

    // Element-wise arithmetic, broadcasting a 1x1 operand the way ICoreMatrix
    // does. A size mismatch fills `whyNot` and answers a default matrix.
    ICoreComplexMatrix add(const ICoreComplexMatrix& other, std::string* whyNot = nullptr) const;
    ICoreComplexMatrix subtract(const ICoreComplexMatrix& other, std::string* whyNot = nullptr) const;
    ICoreComplexMatrix elementWiseMultiply(const ICoreComplexMatrix& other, std::string* whyNot = nullptr) const;
    ICoreComplexMatrix elementWiseDivide(const ICoreComplexMatrix& other, std::string* whyNot = nullptr) const;
    ICoreComplexMatrix elementWisePower(const ICoreComplexMatrix& other, std::string* whyNot = nullptr) const;
    ICoreComplexMatrix multiply(const ICoreComplexMatrix& other, std::string* whyNot = nullptr) const;

    // A \ b with complex entries: a square system solved exactly, a tall one
    // in the least-squares sense -- the same two readings ICoreMatrix::solve
    // gives, over a column-pivoting QR.
    ICoreComplexMatrix leftDivide(const ICoreComplexMatrix& rhs, std::string* whyNot = nullptr) const;

    ICoreComplexMatrix elementWiseSqrt() const;
    ICoreComplexMatrix elementWiseExp() const;
    ICoreComplexMatrix elementWiseLog() const;

    double normFrobenius() const;
    bool   isApprox(const ICoreComplexMatrix& other, const double& tolerance) const;

    // The PRINCIPAL matrix square root and the general real matrix power
    // (the core-MATLAB row C7.17), both over the complex field because that
    // is the only field they are total in: [1 2; 3 4] has a negative
    // eigenvalue, so its square root is complex and a real-only routine can
    // only answer NaN -- which is what this console did until this row.
    //
    // Both go through Eigen's Schur-based matrix functions rather than an
    // eigendecomposition, and the difference is not academic: a DEFECTIVE
    // matrix has too few eigenvectors to diagonalise, so an eig-based root
    // divides by a singular V and answers noise. [1 1; 0 1] is the smallest
    // one, its square root is exactly [1 0.5; 0 1], and MATLAB answers that
    // (measured against R2026a, 2026-09-02) because MATLAB's sqrtm is
    // Schur-based too.
    //
    // `matrixPower` is MATLAB's A^p for a non-integer p, and p = 0.5 agrees
    // with matrixSquareRoot to the last few bits on both sides. A singular
    // matrix with a negative exponent has no answer and says so.
    ICoreComplexMatrix matrixSquareRoot(std::string* whyNot = nullptr) const;
    ICoreComplexMatrix matrixPower(const double& exponent, std::string* whyNot = nullptr) const;

    // ---- the rows that PRODUCE a complex value -----------------------------

    // MATLAB's fft/ifft along `dim` (1 = down the columns, 2 = across the
    // rows, 0 = the first non-singleton), padded or truncated to `n` when n is
    // non-zero. C10.1, C10.2; fft2/ifft2 (C10.3) are the two applied in turn,
    // which is what MATLAB's own fft2 is.
    static ICoreComplexMatrix dft(const ICoreComplexMatrix& x, size_t n, int dim,
                                  const bool& inverse, std::string* whyNot = nullptr);

    // Eigenvalues as a COLUMN, and MATLAB's [V, D] and [V, D, W] forms
    // (C7.8). `vectors`/`leftVectors` are filled only when asked for.
    static bool eigen(const ICoreMatrix& a,
                      ICoreComplexMatrix& values,
                      ICoreComplexMatrix* vectors,
                      ICoreComplexMatrix* leftVectors,
                      std::string* whyNot = nullptr);

    // The generalized problem A*v = lambda*B*v (C7.8's two-argument form).
    static bool eigenGeneralized(const ICoreMatrix& a, const ICoreMatrix& b,
                                 ICoreComplexMatrix& values,
                                 ICoreComplexMatrix* vectors,
                                 std::string* whyNot = nullptr);

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

ICoreComplexVariable.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreComplexVariable.h

////////////////////////// / Scalar operations /////////////////////////

ICoreComplexVariable#

ICoreComplexVariable.h:7 · class · pImpl · 32 declaration(s)

class ICoreComplexVariable {
public:
    ICoreComplexVariable();
    ICoreComplexVariable(const double& real, const double& imaginary);
    static ICoreComplexVariable fromPolar(const double& magnitude, const double& angle);

    ////////////////////////////
    ///     Scalar operations
    ///////////////////////////
    ICoreComplexVariable operator*(double scalar) const;
    ICoreComplexVariable operator/(double scalar) const;
    ////////////////////////////
    ///     Basic arithmetic operators
    ///////////////////////////
    ICoreComplexVariable operator+(const ICoreComplexVariable &other) const;
    ICoreComplexVariable operator-(const ICoreComplexVariable &other) const;
    ICoreComplexVariable operator*(const ICoreComplexVariable &other) const;
    ICoreComplexVariable operator/(const ICoreComplexVariable &other) const;

    ////////////////////////////
    ///     Conjugate
    ///////////////////////////
    ICoreComplexVariable conjugate() const;

    ////////////////////////////
    ///     Other operations
    ///////////////////////////
    double magnitude() const;
    double magnitudeSquared() const;
    double phase() const;

    double complexDistance(const ICoreComplexVariable &other) const;

    ICoreComplexVariable normalized() const;
    ICoreComplexVariable exp() const;
    ICoreComplexVariable log() const;
    ICoreComplexVariable pow(const double& exponent) const;
    bool isApprox(const ICoreComplexVariable &other, const double& tol = 1e-12) const;
    bool isZero(double tol) const;
    bool isReal(double tol) const;
    ICoreComplexVariable rotate(const double &angle) const;

    ////////////////////////////
    ///     Setters/Getters
    ///////////////////////////
    void setReal(const double& newReal);
    void setImaginary(const double& newImaginary);
    double getReal() const;
    double getImaginary() const;
    std::string getVariableAsString() const;

    friend std::ostream& operator<<(std::ostream& os, const ICoreComplexVariable& mat);

    // Complex values live in std::vector (eigenvalues, root loci), so the type
    // stays copyable; the unique_ptr residue below deletes the implicit copy
    // operations, so all four are written out in the .cpp.
    ICoreComplexVariable(const ICoreComplexVariable& other);
    ICoreComplexVariable& operator=(const ICoreComplexVariable& other);
    ICoreComplexVariable(ICoreComplexVariable&& other) noexcept;
    ICoreComplexVariable& operator=(ICoreComplexVariable&& other) noexcept;

    ~ICoreComplexVariable();

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

ICoreFactorUpdates.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreFactorUpdates.h

ICoreFactorUpdates#

ICoreFactorUpdates.h:35 · class · 1 declaration(s)

Core MATLAB's matfun/ factorization UPDATES and the two norm estimators (the core-MATLAB board, C7.30): given a factorization you already have, the factorization of a slightly different matrix, c...

class ICoreFactorUpdates {
public:

    // ---- planerot ------------------------------------------------------
    // MATLAB's own Givens rotation, and it is here in the header's contract
    // because the four QR updates below are DEFINED by which rotations they
    // apply in which order: G = [x'; -x(2) x(1)]/r with r = norm(x), and the
    // IDENTITY (not a sign-flipped rotation) when x(2) is already zero. That
    // last case is why a zero row does not change the factors at all.
    static void planeRotation(double a, double b, double& c, double& s, double& r);

    // ---- cholupdate ----------------------------------------------------
    // R is the UPPER Cholesky factor of A (MATLAB's `chol`), and this answers
    // the upper factor of A + x*x' (update) or A - x*x' (downdate).
    //
    // LINPACK's `dchud`/`dchdd` rather than a transcription: MATLAB's
    // cholupdate is a built-in with no `.m` to read, so this is the published
    // algorithm those built-ins are named for and NOT a line-for-line copy of
    // what MATLAB runs. It agrees with MATLAB to the factorization band, and
    // that is the claim -- not the bit-for-bit agreement the transcribed rows
    // on this board make.
    //
    // `failedRank` is MATLAB's second output `p`: 0 when the answer is a full
    // Cholesky factor, and otherwise the 1-based step at which A - x*x' stopped
    // being positive definite. A downdate that fails leaves `out` untouched.
    static bool choleskyUpdate(const ICoreMatrix& r, const ICoreMatrix& x, bool downdate,
                               ICoreMatrix& out, size_t* failedRank = nullptr,
                               std::string* whyNot = nullptr);

    // ---- qrupdate ------------------------------------------------------
    // A = Q*R and this answers the factors of A + u*v'. Same footing as
    // cholupdate: the published rank-1 QR update, not a transcription.
    static bool qrUpdate(const ICoreMatrix& q, const ICoreMatrix& r,
                         const ICoreMatrix& u, const ICoreMatrix& v,
                         ICoreMatrix& qOut, ICoreMatrix& rOut,
                         std::string* whyNot = nullptr);

    // ---- qrinsert / qrdelete -------------------------------------------
    // The factors of A with one column or row inserted before index `j`
    // (1-based, MATLAB's own indexing), or deleted at it. `byRow` picks
    // MATLAB's 'row' orientation over its default 'col'.
    //
    // These two ARE transcriptions -- `qrinsert.m` and `qrdelete.m` carry a
    // readable path beside their built-in call, and it is the rotations in
    // that path, in that order, that are written out here.
    static bool qrInsert(const ICoreMatrix& q, const ICoreMatrix& r,
                         size_t j, const ICoreMatrix& x, bool byRow,
                         ICoreMatrix& qOut, ICoreMatrix& rOut,
                         std::string* whyNot = nullptr);
    static bool qrDelete(const ICoreMatrix& q, const ICoreMatrix& r,
                         size_t j, bool byRow,
                         ICoreMatrix& qOut, ICoreMatrix& rOut,
                         std::string* whyNot = nullptr);

    // ---- normest -------------------------------------------------------
    // `normest.m` transcribed: the 2-norm by power iteration to a RELATIVE
    // tolerance, default 1e-6. It is an ESTIMATE and not a value -- MATLAB's
    // own answer differs from `norm(A, 2)` in the sixth or seventh digit by
    // design -- so a parity case over it is a BAND and never a comparison, and
    // the console's row says so. `iterations` is MATLAB's second output.
    //
    // The first iterate is the vector of COLUMN 1-norms, which is what makes
    // the iteration reproducible: nothing here is random unless a product
    // comes out exactly zero, and MATLAB reaches for `rand` only there.
    static bool norm2Estimate(const ICoreMatrix& a, double tol,
                              double& estimate, size_t* iterations = nullptr,
                              std::string* whyNot = nullptr);

    // ---- cdf2rdf / rsf2csf ---------------------------------------------
    // The two conversions between the eigen-decomposition's REAL and COMPLEX
    // spellings, and they run in opposite directions:
    //
    //   cdf2rdf takes `[V, D] = eig(A)` for a real A -- complex conjugate
    //   eigenvalues down D's diagonal -- and answers a REAL pair with those
    //   eigenvalues in 2 x 2 blocks. `cdf2rdf.m` transcribed.
    //
    //   rsf2csf takes `[U, T] = schur(A)`, the REAL Schur form with 2 x 2
    //   blocks on T's diagonal, and answers the COMPLEX Schur form with the
    //   eigenvalues on the diagonal. `rsf2csf.m` transcribed.
    //
    // cdf2rdf assumes conjugate pairs are ADJACENT, which is what eig answers,
    // and says so rather than producing nonsense when they are not.
    static bool complexToRealBlockDiagonal(const ICoreComplexMatrix& v,
                                           const ICoreComplexMatrix& d,
                                           ICoreMatrix& vOut, ICoreMatrix& dOut,
                                           std::string* whyNot = nullptr);
    static bool realToComplexSchur(const ICoreMatrix& u, const ICoreMatrix& t,
                                   ICoreComplexMatrix& uOut, ICoreComplexMatrix& tOut,
                                   std::string* whyNot = nullptr);

    // ---- condest -------------------------------------------------------
    // An estimate of `cond(A, 1)` -- `norm(A, 1) * norm(inv(A), 1)` with the
    // second factor ESTIMATED rather than computed, so no inverse is formed.
    //
    // **This is Hager's algorithm, and MATLAB's condest is not.** MATLAB calls
    // `normest1`, the Higham-Tisseur BLOCK generalisation of Hager, whose
    // default block size is 2 and whose starting matrix has a first column of
    // 1/n and the rest **random** (`normest1.m`). So MATLAB's own condest is
    // not reproducible run to run on a matrix large enough for the estimate to
    // matter, which is exactly what C0.9 keeps off this console. The single-
    // vector Hager iteration here is deterministic and finds the EXACT 1-norm
    // on small matrices -- which is where the two agree, and the row says so
    // rather than claiming agreement it cannot have at size.
    //
    // `witness` is MATLAB's second output `v`: a vector with `norm(A*v, 1)`
    // small relative to `norm(v, 1)`, normalised so `norm(v, 1) == 1`.
    static bool conditionEstimate(const ICoreMatrix& a, double& estimate,
                                  ICoreMatrix* witness = nullptr,
                                  std::string* whyNot = nullptr);

    // ---- polyeig -------------------------------------------------------
    // The polynomial eigenvalue problem
    //     (A0 + lambda*A1 + ... + lambda^p * Ap) * x = 0
    // by MATLAB's own linearization (`polyeig.m`): build the n*p by n*p pair
    // (A, B) whose generalized eigenvalues are the answer, and hand it to the
    // QZ algorithm C7.8 already wired up. `vectors`, when asked for, is filled
    // with MATLAB's own choice -- for each eigenvalue, the block of the big
    // eigenvector with the smallest normalized residual, scaled to unit
    // 2-norm.
    static bool polynomialEigenvalues(const std::vector<ICoreMatrix>& coefficients,
                                      ICoreComplexMatrix& values,
                                      ICoreComplexMatrix* vectors,
                                      std::string* whyNot = nullptr);
};
};

ICoreGeneralizedSVD.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreGeneralizedSVD.h

ICoreGeneralizedSVD#

ICoreGeneralizedSVD.h:43 · class · 0 declaration(s)

MATLAB's gsvd -- the generalized singular value decomposition of a pair of matrices with the same number of columns (the core-MATLAB board, C7.30's last name, and its own module because it is the...

class ICoreGeneralizedSVD {
public:
    // [U, V, X, C, S] = gsvd(A, B), or gsvd(A, B, "econ") for `economy`,
    // which needs m >= p or n >= p and answers U and V with at most p
    // columns (MATLAB's own restriction and shape). B must have A's column
    // count; anything else says so and answers false.
    static bool decompose(const ICoreMatrix& a, const ICoreMatrix& b, bool economy,
                          ICoreMatrix& u, ICoreMatrix& v, ICoreMatrix& x,
                          ICoreMatrix& c, ICoreMatrix& s,
                          std::string* whyNot = nullptr);

    // sigma = gsvd(A, B): the generalized singular values as a column, which
    // is sqrt(diag(C'*C) ./ diag(S'*S)) read off the same decomposition --
    // MATLAB's one-output form, computed by the same path so the two
    // spellings cannot disagree.
    // MATLAB accepts the "econ" word here too and reads the values off the
    // economy decomposition; `economy` is that.
    static bool values(const ICoreMatrix& a, const ICoreMatrix& b, bool economy,
                       ICoreMatrix& sigma, std::string* whyNot = nullptr);
};
};

ICoreLogic.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreLogic.h

ICoreLogic#

ICoreLogic.h:32 · class · 2 declaration(s)

Core MATLAB's yes/no answers (the core-MATLAB board, C9.1-C9.3, C9.7) and the three non-finite tests that answer the same shape (C5.48).

class ICoreLogic {
public:

    // isnan / isinf / isfinite, element-wise (C5.48).
    enum class FiniteTest { IsNaN, IsInf, IsFinite };
    static ICoreMatrix finiteTest(const FiniteTest& which, const ICoreMatrix& a,
                                  std::string* whyNot = nullptr);

    // any(A), any(A, dim), any(A, "all") -- and all(...) in the same forms
    // (C9.1). `dim` is 1 (down columns), 2 (across rows) or 0 for MATLAB's
    // default, the first non-singleton dimension; `everyElement` is the "all"
    // word, which reduces the whole array to one answer.
    static ICoreMatrix anyOf(const ICoreMatrix& a, int dim, bool everyElement,
                             std::string* whyNot = nullptr);
    static ICoreMatrix allOf(const ICoreMatrix& a, int dim, bool everyElement,
                             std::string* whyNot = nullptr);

    // The six comparisons, element-wise, with MATLAB's rules (C1.5): a scalar
    // operand broadcasts, any other size mismatch is refused the way MATLAB
    // refuses it, and `==` is EXACT -- the console's eq() compared within
    // 1e-12 until this landed, so `0.1 + 0.2 == 0.3` answered 1 where MATLAB
    // answers 0 (C0.1 chose MATLAB's meaning for a core name). A NaN is
    // unequal to everything including itself, which falls out of IEEE and is
    // the one place these differ from the connectives below: `<` and `==`
    // take a NaN, `and`/`or` refuse it.
    enum class Comparison { Eq, Ne, Lt, Le, Gt, Ge };
    static ICoreMatrix compare(const Comparison& which, const ICoreMatrix& a,
                               const ICoreMatrix& b, std::string* whyNot = nullptr);

    // and / or / xor, and not (C9.2). A scalar operand broadcasts; any other
    // size mismatch is refused, as MATLAB refuses it. `a & b`, `a | b` and
    // `~a` are the same three operations under MATLAB's operator spelling
    // since C1.6 retired `|`/`&` as concatenation, and they reach exactly
    // this code -- the function form and the operator cannot drift.
    enum class Connective { And, Or, Xor };
    static ICoreMatrix connective(const Connective& which, const ICoreMatrix& a,
                                  const ICoreMatrix& b, std::string* whyNot = nullptr);
    static ICoreMatrix negate(const ICoreMatrix& a, std::string* whyNot = nullptr);

    // isequal(A, B, ...) and isequaln (C9.3): every value must have the same
    // shape AND the same entries. They differ on one point only -- isequal
    // says NaN is not equal to NaN (it follows ==), isequaln says it is.
    static bool equalValues(const std::vector<ICoreMatrix>& values, bool nanEqualsNan,
                            std::string* whyNot = nullptr);

    // issorted(v), issorted(A), issorted(A, "rows") (C9.7). Without "rows" a
    // matrix is sorted when each COLUMN is non-decreasing and a row vector when
    // it is non-decreasing along its length (the first non-singleton rule
    // again); with it, the rows must be non-decreasing lexicographically.
    static bool isSorted(const ICoreMatrix& a, bool byRows, std::string* whyNot = nullptr);

};
};

ICoreMatrix.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreMatrix.h

Two members below (toEigen2DMatrixXd / fromEigen) name Eigen::MatrixXd in their signatures, so this header cannot be parsed without Eigen. It used to arrive for free from the force-included SDK-root pch.h; that Eigen block now lives in ICoreMath's own umbrella and is named here explicitly.

ICoreMatrix#

ICoreMatrix.h:49 · class · pImpl · 263 declaration(s)

class ICoreMatrix {
public:

    ////////////////////////////////////////////////////
    ///
    ///     Constructors and Base Methods
    ///
    ////////////////////////////////////////////////////
    ICoreMatrix();
    ICoreMatrix(size_t d1, size_t d2, size_t d3 = 1);
    explicit ICoreMatrix(const double& scalar);
    explicit ICoreMatrix(const std::vector<double>& vec1d);
    explicit ICoreMatrix(const std::vector<std::vector<double>>& vec2d);
    explicit ICoreMatrix(const std::vector<std::vector<std::vector<double>>>& vec3d);
    static ICoreMatrix identity(const size_t& n);
    static ICoreMatrix zeros(const size_t &size1, const size_t &size2);
    static ICoreMatrix zerosVector(const size_t &size1);
    static ICoreMatrix ones(const size_t &size1, const size_t &size2);
    static ICoreMatrix onesVector(const size_t &size1);
    // Both are ROW vectors, MATLAB's shape (the core-MATLAB board, C4.9 and
    // C4.10); they were columns until 2026-09-02. n = 1 is the END point, and
    // logspace sweeps to pi itself, not to 10^pi, when the upper exponent is
    // exactly pi -- MATLAB's documented special case.
    static ICoreMatrix linspace(const double& a, const double& b, const size_t& n);
    static ICoreMatrix logspace(const double& a, const double& b, const size_t& n);
    static ICoreMatrix rand(const size_t& size1, const size_t& size2);   // uniform [0,1)
    static ICoreMatrix randn(const size_t& size1, const size_t& size2);  // standard normal

    // This is the setter method
    double& operator()(size_t i, size_t j, size_t k = 0);

    // This is the getter method
    double operator()(size_t i, size_t j, size_t k = 0) const;

    size_t size1() const;
    size_t size2() const;
    size_t size3() const;
    std::vector<double> rawData() const;
    void assignRawDataDirectly(const size_t& size1, const size_t& size2, const size_t& size3, const std::vector<double>& newRawDataVector);
    size_t numberOfRows() const;
    size_t numberOfColumns() const;

    void resize(size_t d1, size_t d2 = 1, size_t d3 = 1);

    void overwriteIndex(const double& newVal, const size_t i, const size_t j = 0, const size_t k = 0);

    void cloneValueFromMatrix(const ICoreMatrix* other);
    void cloneValueFromMatrix(const ICoreMatrix& other);

    void clear();

    ////////////////////////////////////////////////////
    ///
    ///     Verifications
    ///
    ////////////////////////////////////////////////////
    bool verifySize(const size_t &d1, const size_t &d2 = 1, const size_t &d3 = 1) const;
    bool verifySquareMatrix() const;
    bool verifyElementWiseInteger() const;
    bool verifyElementWiseNotNegative() const;
    bool verifyElementWiseStrictlyPositive() const;
    bool verifyZerosMatrix(double tol = 1e-12) const;

    bool verifySalarZeroMatrix(double tol) const;

    bool verifyOnesMatrix(double tol = 1e-12) const;
    bool verifyValidVectorSize() const;

    bool isScalar() const;

    // Shape and structure predicates (the core-MATLAB board, C4.18 and C7.24).
    // Each answers about the LANGUAGE-level shape, which for this console is
    // always 2-D: isMatrix() is therefore true for everything it can hold, and
    // isEmpty() is false for everything it can hold -- both are answers, not
    // stubs, and both stop being trivial the day C3.9 gives the console an
    // empty matrix.
    // MATLAB's LOGICAL class (decision C0.3, rows C3.6 and C9.4). A flag rather
    // than a second value kind, because that is the shape of MATLAB's own rule:
    // a logical array holds ones and zeros like any other, and what differs is
    // the CLASS it reports and what the class-reading operations do with it.
    //
    //   isLogical()  is this matrix a logical array
    //   asLogical()  the same numbers, said to be (or not to be) logical --
    //                for the operations that ANSWER one (a comparison, a
    //                connective, any/all, every is* question) and for the few
    //                that carry one through unchanged
    //   logicalOf()  MATLAB's logical(A): non-zero becomes 1, and a NaN is
    //                REFUSED rather than converted ("NaN values cannot be
    //                converted to logicals"), which is MATLAB's own sentence
    //
    // Arithmetic needs no rule here: every operator builds a fresh matrix and
    // a fresh matrix is not logical, which is exactly MATLAB's behaviour
    // (`true + 1` is the double 2).
    bool isLogical() const;
    ICoreMatrix asLogical(bool yes = true) const;
    static ICoreMatrix logicalOf(const ICoreMatrix& a, std::string* whyNot = nullptr);

    bool isEmpty() const;
    bool isVector() const;      // 1 x n or n x 1
    bool isRow() const;         // 1 x n
    bool isColumn() const;      // n x 1
    bool isMatrix() const;      // 2-D, i.e. anything this class holds today
    bool isDiagonal() const;    // every off-diagonal entry exactly 0
    bool isUpperTriangular() const;
    bool isLowerTriangular() const;
    // Every non-zero entry within `lower` bands below and `upper` above the
    // main diagonal. isDiagonal() is isBanded(0, 0).
    bool isBanded(const size_t& lower, const size_t& upper) const;
    bool isSymmetric(double tol = 1e-9) const;
    bool isPositiveDefinite() const;

    ////////////////////////////////////////////////////
    ///
    ///     Conversions to std vectors
    ///
    ////////////////////////////////////////////////////

    // Convert full 3D matrix to nested vectors
    std::vector<std::vector<std::vector<double>>> toVector3D() const;
    std::vector<std::vector<double>> toVector2D() const;

    // Get a 2D slice by fixing dim1 index (like a "matrix" at i)
    std::vector<std::vector<double>> getSlice2D(const size_t& i) const;

    // // Get a 1D column vector fixing dim1 and dim3 (returns all j for fixed i,k)
    // std::vector<double> getColumn(const size_t& i, const size_t& k = 0) const;

    // // Get a 1D row vector fixing dim1 and dim2 (returns all k for fixed i,j)
    // std::vector<double> getRow(const size_t& i, const size_t& j) const;

    ICoreMatrix getColumn(const size_t& j) const;
    ICoreMatrix getRow(const size_t& i) const;

    // Get a 1D vector of the entire entries for the entire matrix
    std::vector<double>& getRawDataVector();
    std::vector<double> getRawDataVector_Copy() const;

    std::pair<bool, ICoreMatrix> sliceMatrix2D(const size_t &row_start, const size_t &row_end, const size_t &col_start,
                              const size_t &col_end) const;

    ////////////////////////////////////////////////////
    ///
    ///     Algebra
    ///
    ////////////////////////////////////////////////////

    // To Apply operators
    void operator+=(const ICoreMatrix& other);
    ICoreMatrix operator+(const ICoreMatrix& other) const;
    void operator-=(const ICoreMatrix& other);
    ICoreMatrix operator-(const ICoreMatrix& other) const;
    ICoreMatrix operator*(const double& scale) const;
    friend ICoreMatrix operator*(double scale, const ICoreMatrix& m);
    ICoreMatrix operator*(const ICoreMatrix& other) const;
    ICoreMatrix operator|(const ICoreMatrix& other) const;
    ICoreMatrix operator&(const ICoreMatrix& other) const;

    // Element-wise addition: this + other
    ICoreMatrix& add(const ICoreMatrix& other);

    // Element-wise subtraction: this - other
    ICoreMatrix& subtract(const ICoreMatrix& other);

    // Product
    double dot(const ICoreMatrix& other) const;
    ICoreMatrix cross(const ICoreMatrix& other) const;
    ICoreMatrix multiply(const ICoreMatrix& other) const;
    ICoreMatrix& multiplyElementWiseByScalar(const double& factor);
    ICoreMatrix multiplyElementWiseByMatrix(ICoreMatrix& other) const;

    ICoreMatrix transpose() const;
    size_t rank(const double& tol = 1e-12) const;

    // Inverse
    ICoreMatrix inverse() const;
    ICoreMatrix syntraInverse() const;

    // Eigen-backed scalar quantities
    double determinant() const;        // square 2D only
    double trace() const;              // square 2D only
    double conditionNumber() const;    // ratio of largest/smallest singular value

    // Eigen-backed factorizations / generalized inverses
    ICoreMatrix pseudoInverse(double tol = -1.0) const;  // Moore-Penrose (SVD); tol<0 => auto
    ICoreMatrix sqrtm() const;                           // principal matrix square root (square)
    ICoreMatrix choleskyL() const;                       // lower factor L, A = L*L^T (square SPD)
    ICoreMatrix nullSpace() const;                       // basis for ker(A) as columns

    // LDLT decomposition (symmetric, possibly indefinite): P^T*L*diag(D)*L^T*P = A
    ICoreMatrix ldltL() const;
    ICoreMatrix ldltD() const;
    ICoreMatrix ldltP() const;

    // Matrix exponential
    ICoreMatrix expm() const;
    ICoreMatrix logm() const;

    // Norms
    double norm_Frobenius() const;
    double norm_RMS() const;
    double norm_1() const;
    // The largest singular value, from the SVD. It was a power iteration with
    // an iteration count and a 1e-6 tolerance until 2026-09-02: MATLAB's
    // norm(A, 2) is exact, so a parity case over it could only ever be run in
    // the iterative band (the core-MATLAB board, C7.5).
    double norm_2() const;
    double norm_inf() const;
    double norm_nuclear() const;       // sum of singular values, 2D only

    ICoreMatrix concatHorizontal(const ICoreMatrix& other) const;
    ICoreMatrix concatVertical(const ICoreMatrix& other) const;

    std::vector<ICoreComplexVariable> eigenvalues() const;
    ICoreMatrix eigenvectors() const;  // real part; exact for symmetric matrices

    // The SELF-ADJOINT eigendecomposition, and the only pair of accessors here
    // whose two halves are in one order: entry k of symmetricEigenvalues() is
    // the eigenvalue of column k of symmetricEigenvectors(), both ASCENDING,
    // which is the order MATLAB's `eig` answers a symmetric matrix in.
    //
    // ⚠ eigenvalues() AND eigenvectors() CANNOT BE PAIRED. The first runs
    // Eigen's general EigenSolver and the second, for a symmetric matrix, the
    // self-adjoint one; the two order their answers by different rules, so
    // `eigenvalues()[k]` is not in general the eigenvalue of column k of
    // `eigenvectors()`. Anything that needs the pairing -- cmdscale's B, a
    // covariance's principal axes -- asks for it here.
    ICoreMatrix symmetricEigenvalues() const;    // n x 1, ascending
    ICoreMatrix symmetricEigenvectors() const;   // n x n, column k for value k

    // A*x = rhs. Square: the exact solution. Tall: the least-squares one
    // (column-pivoting QR, MATLAB's backslash). Wide: refused.
    ICoreMatrix solve(const ICoreMatrix &rhs) const;

    // QR decomposition (thin): A = Q*R
    ICoreMatrix qrQ() const;
    ICoreMatrix qrR() const;

    // LU decomposition with partial pivoting (square only): P*A = L*U
    ICoreMatrix luL() const;
    ICoreMatrix luU() const;
    ICoreMatrix luP() const;

    // SVD (thin): A = U*diag(S)*V^T; svdS is a column vector of singular values
    ICoreMatrix svdU() const;
    ICoreMatrix svdS() const;
    ICoreMatrix svdV() const;

    // ---- MATLAB's factorizations, in the shapes MATLAB answers (C7.9-C7.13) ----
    //
    // The six thin factors above are the console's own extras (qrq, svdu, ...)
    // and keep their meaning. MATLAB's `[Q, R] = qr(A)` is the FULL
    // factorization -- Q is m x m and R is m x n -- so these are separate
    // methods rather than a change of shape under the old names, and MATLAB's
    // economy form (`qr(A, 0)`, `svd(A, 'econ')`) is the flag.
    ICoreMatrix qrFactorQ(const bool& economy = false) const;
    ICoreMatrix qrFactorR(const bool& economy = false) const;

    // The COLUMN-PIVOTED QR, A*P = Q*R: MATLAB's three-output form. A
    // different factorization, not a rearrangement of the one above.
    ICoreMatrix qrPivotedQ() const;
    ICoreMatrix qrPivotedR() const;
    ICoreMatrix qrPivotedP() const;

    // A = U*S*V', with S the m x n MATRIX MATLAB answers rather than the
    // vector svdS() gives.
    ICoreMatrix svdFactorU(const bool& economy = false) const;
    ICoreMatrix svdFactorS(const bool& economy = false) const;
    ICoreMatrix svdFactorV(const bool& economy = false) const;

    // The UPPER Cholesky factor, A = R'*R -- MATLAB's default, where
    // choleskyL() is the lower one. `failedPivot` answers MATLAB's `p`: 0 when
    // A is positive definite, otherwise the 1-based pivot at which the
    // factorization failed, and the result is then the leading (p-1) x (p-1)
    // block that did succeed. Hand-written rather than Eigen's LLT for exactly
    // that reason: a decomposition that only says "it failed" cannot answer p.
    ICoreMatrix choleskyUpper(size_t* failedPivot = nullptr) const;

    // MATLAB's two null-space bases. nullSpaceOrthonormal() is null(A): the
    // columns of V whose singular values fall at or below
    // max(size(A)) * eps(largest) -- an ORTHONORMAL basis, where nullSpace()
    // above answers Eigen's kernel, which is not orthonormal. Both answer a
    // matrix with ZERO columns when A has full column rank.
    // nullSpaceRational() is null(A, 'r'): read off the reduced row echelon
    // form, so its entries are the small rationals a hand calculation gives.
    ICoreMatrix nullSpaceOrthonormal() const;
    ICoreMatrix nullSpaceRational() const;

    // ---- the three similarity transforms MATLAB names (C7.21-C7.23) --------
    //
    // Each answers BOTH halves of its transform, because the one-output form
    // is not checkable without the other half:
    //   hess:    A = P*H*P'   P orthogonal,  H upper Hessenberg
    //   schur:   A = U*T*U'   U orthogonal,  T the REAL Schur form (quasi-
    //                         upper-triangular: a 1x1 block per real
    //                         eigenvalue, a 2x2 per complex pair)
    //   balance: B = T\A*T    T a permutation times a diagonal of exact
    //                         powers of two, so no rounding is introduced
    // A non-square argument reports through ICoreRunDiagnosis and answers the
    // default matrix, the way every factorization here does.
    ICoreMatrix hessenbergH() const;
    ICoreMatrix hessenbergP() const;
    ICoreMatrix schurT() const;
    ICoreMatrix schurU() const;

    // balanced() is MATLAB's B and balancingSimilarity() its T. `permute` is
    // MATLAB's default; false is balance(A, "noperm"), which scales without
    // reordering.
    ICoreMatrix balanced(const bool& permute = true) const;
    ICoreMatrix balancingSimilarity(const bool& permute = true) const;

    // diag(): vector -> diagonal matrix; 2D matrix -> its diagonal as a column vector
    ICoreMatrix diag() const;

    // Kronecker product, 2D only
    ICoreMatrix kron(const ICoreMatrix& other) const;

    // Element-wise math (operates over all entries, including 3D)
    ICoreMatrix elementWiseAbs() const;
    ICoreMatrix elementWiseExp() const;
    ICoreMatrix elementWiseLog() const;
    ICoreMatrix elementWiseSqrt() const;
    ICoreMatrix elementWiseSin() const;
    ICoreMatrix elementWiseCos() const;
    ICoreMatrix elementWiseTan() const;
    ICoreMatrix elementWisePow(const double& exponent) const;
    ICoreMatrix elementWiseMin(const ICoreMatrix& other) const;   // broadcasts a scalar operand
    ICoreMatrix elementWiseMax(const ICoreMatrix& other) const;   // broadcasts a scalar operand
    ICoreMatrix clamp(const double& lo, const double& hi) const;

    // Core MATLAB's elementary element-wise math, over all entries including
    // 3D. The binary forms broadcast a scalar
    // operand exactly as elementWiseMin/Max do; a size mismatch reports
    // through ICoreRunDiagnosis and returns the default matrix.
    ICoreMatrix elementWiseSign() const;
    ICoreMatrix elementWiseFloor() const;
    ICoreMatrix elementWiseCeil() const;
    ICoreMatrix elementWiseFix() const;                        // round toward zero
    ICoreMatrix elementWiseRound(const int& digits = 0) const;  // half AWAY from zero, not banker's
    ICoreMatrix elementWiseRoundSignificant(const int& digits) const;
    ICoreMatrix elementWiseSec() const;
    ICoreMatrix elementWiseCsc() const;
    ICoreMatrix elementWiseCot() const;
    ICoreMatrix elementWiseSinh() const;
    ICoreMatrix elementWiseCosh() const;
    ICoreMatrix elementWiseTanh() const;
    ICoreMatrix elementWiseDeg2Rad() const;
    ICoreMatrix elementWiseSpacing() const;  // eps(x): distance to the next double
    ICoreMatrix elementWiseRad2Deg() const;
    ICoreMatrix elementWiseHypot(const ICoreMatrix& other) const;
    ICoreMatrix elementWiseAtan2(const ICoreMatrix& other) const;  // atan2(this, other) = atan2(y, x)
    ICoreMatrix elementWiseMod(const ICoreMatrix& other) const;    // sign follows the DIVISOR
    ICoreMatrix elementWiseRem(const ICoreMatrix& other) const;    // sign follows the DIVIDEND

    // Integer-domain element-wise math. Each reports a non-integer (or, for
    // factorial, a negative) entry through whyNot and returns the default
    // matrix, the way the Sylvester family reports a singular operator --
    // MATLAB raises an error on the same inputs, so returning a plausible
    // number would be the one answer that cannot be right.
    ICoreMatrix elementWiseFactorial(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseGcd(const ICoreMatrix& other, std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseLcm(const ICoreMatrix& other, std::string* whyNot = nullptr) const;

    // The prime trio of the core-MATLAB board (C5.31), which belongs with the
    // integer-domain family above because it refuses on the same grounds.
    // primes(n) is a ROW vector of every prime up to n, by the sieve;
    // isprime tests each entry (and MATLAB accepts only non-negative whole
    // numbers, so -3 and 2.5 are refused rather than answered false);
    // primeFactors(n) is MATLAB's factor(), ascending with repeats, a SCALAR
    // argument only. factor(1) is 1 and factor(0) is 0, both as MATLAB has it.
    static ICoreMatrix primes(const double& upTo, std::string* whyNot = nullptr);
    ICoreMatrix elementWiseIsPrime(std::string* whyNot = nullptr) const;
    static ICoreMatrix primeFactors(const double& n, std::string* whyNot = nullptr);
    // divisors(n) -- every POSITIVE divisor of n, ascending, as a row.
    // MATLAB's is a Symbolic Math Toolbox name that answers a double for a
    // double argument, which is why it sits here beside the prime trio rather
    // than only on the symbolic side (the toolbox board's T4.18).
    // divisors(0) is 0 and the sign of n is ignored, both measured.
    static ICoreMatrix divisors(const double& n, std::string* whyNot = nullptr);

    // The real-only family: MATLAB leaves the reals for these inputs and the
    // real* names exist precisely to refuse instead. nthroot is real by
    // definition and needs an odd integer root for a negative base.
    ICoreMatrix elementWiseRealSqrt(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseRealLog(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseRealPow(const ICoreMatrix& other, std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseNthRoot(const ICoreMatrix& other, std::string* whyNot = nullptr) const;

    // expm1/log1p keep the digits exp(x)-1 and log(1+x) lose near zero.
    // log1p refuses below -1 for the reason the real* family exists.
    ICoreMatrix elementWiseExpm1() const;
    ICoreMatrix elementWiseLog1p(std::string* whyNot = nullptr) const;

    // log2/log10, the two remaining bases (the core-MATLAB board, C5.7, C5.8).
    // Total maps like elementWiseLog(): a negative entry is NaN here, and the
    // caller that means MATLAB's log2/log10 asks hasNegativeEntry() first --
    // MATLAB answers a COMPLEX number there and this console has no complex
    // kind yet (C0.2), so sqrt/log/log2/log10 refuse rather than answer NaN.
    ICoreMatrix elementWiseLog2() const;
    ICoreMatrix elementWiseLog10() const;
    bool hasNegativeEntry() const;   // a negative zero is NOT negative, as IEEE has it

    // [f, e] = log2(x), MATLAB's two-output form (C5.7) -- and it is std::frexp
    // entry by entry: f in [0.5, 1) with x = f * 2^e. It is TOTAL on the reals
    // where one-output log2 is not, negatives included (log2(-6) is complex,
    // but [f, e] = log2(-6) is -0.75 and 3), so neither answer refuses.
    // Zero, the infinities and NaN all report e = 0 and f = x, MATLAB's own.
    ICoreMatrix elementWiseLog2Mantissa() const;
    ICoreMatrix elementWiseLog2Exponent() const;

    // meshgrid/ndgrid (C4.15). One builder for both, because ndgrid IS
    // meshgrid transposed: meshgrid(x, y) is numel(y) BY numel(x) with x
    // running along the row, ndgrid(x, y) is numel(x) by numel(y) with x
    // running down the column. `second` picks the output (X/Y, or the ndgrid
    // pair); the orientation of x and y themselves is ignored, as MATLAB
    // ignores it.
    static ICoreMatrix grid(const ICoreMatrix& x, const ICoreMatrix& y,
                            const bool& second, const bool& transposed,
                            std::string* whyNot = nullptr);

    // Trigonometry in DEGREES (the core-MATLAB board, C5.21). These are not
    // sin(deg2rad(x)): the angle never becomes radians at a multiple of 90, so
    // sind(180) is exactly +0, cosd(90) is exactly +0 and tand(90) is Inf --
    // which is what makes secd/cscd/cotd answer Inf there rather than 1e16.
    // The inverse three refuse outside [-1, 1], where MATLAB goes complex.
    ICoreMatrix elementWiseSinDeg() const;
    ICoreMatrix elementWiseCosDeg() const;
    ICoreMatrix elementWiseTanDeg() const;
    ICoreMatrix elementWiseSecDeg() const;
    ICoreMatrix elementWiseCscDeg() const;
    ICoreMatrix elementWiseCotDeg() const;
    ICoreMatrix elementWiseAsinDeg(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAcosDeg(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAtanDeg() const;

    // The same three, one number at a time. The Aerospace conversions
    // (the toolbox board's T7.10 and T7.11) work in degrees and are
    // written scalar-at-a-time over a caller's rows, and MATLAB's own
    // geodesy calls sind/cosd/atan2d rather than a radian route -- so the
    // exactness above is part of THEIR answers too, and a second copy of the
    // octant reduction beside them would be a copy that eventually disagrees.
    // atan2d is the pair's inverse and is exact on the axes: atan2Degrees(1, 0)
    // is exactly 90, which atan2(1, 0)*180/pi is not.
    static double sinDegrees(const double& degrees);
    static double cosDegrees(const double& degrees);
    static double atan2Degrees(const double& y, const double& x);

    // The inverse trigonometry and the whole hyperbolic family (C5.15, C5.17,
    // C5.19, C5.20). Each reciprocal is 1/f and each inverse-reciprocal is
    // f(1/x) -- MATLAB's own composition, measured bitwise, and the one that
    // carries the signed zero out to the pole: acot(-0) is -pi/2 and coth(-0)
    // is -Inf. The seven that take a whyNot refuse where MATLAB goes complex,
    // with the boundary itself inside the domain: atanh(1), acoth(1) and
    // asech(0) are all Inf, and asech(-0) is NaN.
    ICoreMatrix elementWiseAsin(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAcos(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAtan() const;
    ICoreMatrix elementWiseAsec(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAcsc(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAcot() const;
    ICoreMatrix elementWiseSech() const;
    ICoreMatrix elementWiseCsch() const;
    ICoreMatrix elementWiseCoth() const;
    ICoreMatrix elementWiseAsinh() const;
    ICoreMatrix elementWiseAcosh(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAtanh(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAsech(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseAcsch() const;
    ICoreMatrix elementWiseAcoth(std::string* whyNot = nullptr) const;

    // The error function and its inverses (C5.40). erfinv/erfcinv answer NaN
    // outside their domain rather than refusing, because MATLAB does.
    ICoreMatrix elementWiseErf() const;
    ICoreMatrix elementWiseErfc() const;
    ICoreMatrix elementWiseErfInv() const;
    ICoreMatrix elementWiseErfcInv() const;

    // The gamma and beta families (C5.41, C5.42). gamma() is Inf at every
    // non-positive integer; gammaln/beta/betaln refuse a negative entry and
    // betainc refuses an x outside [0, 1], each exactly where MATLAB errors.
    // The `upper` flag is MATLAB's 'upper' tail option.
    ICoreMatrix elementWiseGamma() const;
    ICoreMatrix elementWiseGammaLn(std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseGammaInc(const ICoreMatrix& a, const bool& upper,
                                    std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseBeta(const ICoreMatrix& other, std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseBetaLn(const ICoreMatrix& other, std::string* whyNot = nullptr) const;
    ICoreMatrix elementWiseBetaInc(const ICoreMatrix& a, const ICoreMatrix& b,
                                   const bool& upper, std::string* whyNot = nullptr) const;

    // n x n / m x n filled with one value -- what Inf(m, n) and NaN(m, n) are
    // built from.
    static ICoreMatrix constant(const size_t& size1, const size_t& size2, const double& value);

    // nchoosek(n, k) for a SCALAR n: the binomial coefficient -- MATLAB's other
    // reading of the same name.
    static double binomialCoefficient(const double& n, const double& k,
                                      std::string* whyNot = nullptr);

    // nchoosek(v, k): every k-element combination of v's entries, one per row,
    // in MATLAB's lexicographic order. An empty result is 0 x k.
    static ICoreMatrix combinations(const ICoreMatrix& vector, const size_t& k,
                                    std::string* whyNot = nullptr);

    // Element-wise comparisons (broadcast a scalar operand); result is 0.0/1.0 per entry
    ICoreMatrix greaterThan(const ICoreMatrix& other) const;
    ICoreMatrix lessThan(const ICoreMatrix& other) const;
    ICoreMatrix greaterEqual(const ICoreMatrix& other) const;
    ICoreMatrix lessEqual(const ICoreMatrix& other) const;
    ICoreMatrix equalTo(const ICoreMatrix& other, double tol = 1e-12) const;
    ICoreMatrix notEqualTo(const ICoreMatrix& other, double tol = 1e-12) const;

    // Matrix power: integer exponent via exponentiation by squaring (negative uses inverse())
    ICoreMatrix mpower(const long long& n) const;

    // Reductions over all entries
    double sum() const;
    double mean() const;
    double maxElement() const;
    double minElement() const;

    // Axis-wise reductions, 2D only. *Rows collapses each row to one value
    // (result is a column vector); *Cols collapses each column (row vector).
    ICoreMatrix sumRows() const;
    ICoreMatrix sumCols() const;
    ICoreMatrix meanRows() const;
    ICoreMatrix meanCols() const;
    ICoreMatrix maxRows() const;
    ICoreMatrix maxCols() const;
    ICoreMatrix minRows() const;
    ICoreMatrix minCols() const;

    // Cumulative sum/product. Vectors accumulate along their length; general
    // matrices accumulate down each column (Matlab convention).
    ICoreMatrix cumsum() const;
    ICoreMatrix cumprod() const;

    // Shape manipulation, 2D only. reshape reads and writes COLUMN-major,
    // MATLAB's element order (C0.8) -- it was row-major until 2026-09-02.
    ICoreMatrix reshape(const size_t& newDim1, const size_t& newDim2) const;
    ICoreMatrix flipud() const;
    ICoreMatrix fliplr() const;
    ICoreMatrix repmat(const size_t& rowTimes, const size_t& colTimes) const;

    // ---- Core MATLAB's array structure and shape (the core-MATLAB board, C4) --
    //
    // MATLAB semantics at the language level, whatever this class stores
    // internally (the board's C0.8): 1-based counting is the evaluator's business, and the
    // one ordering decision that reaches here is `nonzeros`, which is
    // COLUMN-major because MATLAB's is.
    //
    // An operation whose MATLAB answer is an EMPTY matrix refuses instead,
    // through whyNot, naming C3.9: the console has no empty-matrix semantics
    // yet (`[] + 1` is `[]` in MATLAB and nothing here), so returning a 0 x n
    // would hand the rest of the console a value it cannot compute with. Same
    // contract the real* family uses -- a plausible answer would be the one
    // answer that cannot be right.

    // k-th diagonal, MATLAB's diag(v, k) / diag(A, k): a VECTOR builds an
    // (n + |k|) square carrying v on that diagonal, a MATRIX returns that
    // diagonal as a column. k = 0 is diag() above.
    ICoreMatrix diag(const int& k, std::string* whyNot = nullptr) const;

    // Block diagonal (MATLAB blkdiag): the two operands down the diagonal,
    // zeros elsewhere. Neither need be square and their sizes need not match.
    static ICoreMatrix blkdiag(const ICoreMatrix& first, const ICoreMatrix& second);

    // Size, in MATLAB's reading: numel() counts every entry, length() is the
    // longest dimension (0 when the matrix is empty), ndims() is 2 for every
    // 2-D matrix -- MATLAB never reports fewer than 2, so a scalar is 2, not 0.
    size_t numel() const;
    size_t length() const;
    size_t ndims() const;

    // Upper/lower triangle at and beyond the k-th diagonal (MATLAB triu/tril).
    ICoreMatrix triu(const int& k) const;
    ICoreMatrix tril(const int& k) const;

    // Counter-clockwise quarter turns (MATLAB rot90). `times` is taken mod 4
    // and may be negative.
    ICoreMatrix rot90(const int& times) const;

    // Circular shift along `dim` (1 = down the columns, 2 = along the rows);
    // `by` may be negative. MATLAB's no-dim form shifts along the first
    // non-singleton dimension -- firstNonSingletonDim() below is that rule.
    ICoreMatrix circshift(const long long& by, const int& dim) const;

    // Reverse the order along `dim` (MATLAB flip); flipud and fliplr are its
    // two fixed-dim spellings.
    ICoreMatrix flip(const int& dim) const;

    // n-th order difference along `dim` (MATLAB diff): each pass drops one
    // entry from that dimension. An order that would consume the dimension
    // refuses (C3.9) rather than returning an empty.
    ICoreMatrix diff(const size_t& order, const int& dim,
                     std::string* whyNot = nullptr) const;

    // The dimension MATLAB reduces along when none is named: the first whose
    // extent is not 1, or 1 when there is none (a scalar).
    int firstNonSingletonDim() const;

    // Non-zero entries: nnz() counts them; nonzeros() returns them as a COLUMN
    // in COLUMN-major order, MATLAB's order (C0.8). All-zero refuses (C3.9).
    size_t nnz() const;
    ICoreMatrix nonzeros(std::string* whyNot = nullptr) const;

    // ---- Ordering, searching and sets (the core-MATLAB board, C4.24-C4.28,
    //      C9.5, C9.6) ------------------------------------------------------
    //
    // Three of MATLAB's rules here are the opposite of the obvious one, all
    // measured against R2026a rather than reasoned about, and every one falls
    // out of using plain `==` and an is-NaN test rather than a tolerance:
    //
    //   * A NaN is never equal to itself, so `unique([NaN 1 NaN])` keeps BOTH
    //     NaNs (`[1 NaN NaN]`) and `ismember(NaN, NaN)` is FALSE.
    //   * A NaN sorts LAST ascending and FIRST descending -- it is not simply
    //     "largest", it moves with the direction.
    //   * Negative zero is zero: `unique([-0 0])` is one element, and
    //     `find([0 -0 1])` is 3.
    //
    // Ordering is column-major wherever an index or a flattening is involved
    // (the board's C0.8), and an empty result REFUSES naming C3.9 rather than
    // returning a 0 x n, as the C4.34 family does.
    //
    // Orientation follows MATLAB exactly and is not the same rule everywhere:
    // `unique`/`find` give a ROW only for a row-vector input, while the set
    // operations give a row only when BOTH inputs are rows.

    // sort along `dim` (1 = down the columns, 2 = along the rows).
    ICoreMatrix sorted(const int& dim, const bool& descending) const;

    // sortrows: order whole ROWS lexicographically by the given 1-based
    // columns, a negative column meaning descending on it. An empty `columns`
    // means every column, left to right. Ties keep their original order.
    ICoreMatrix sortedRows(const std::vector<long long>& columns,
                           std::string* whyNot = nullptr) const;

    // unique: sorted-and-deduplicated (or first-appearance order when
    // `stable`). `byRows` deduplicates whole rows instead of elements.
    ICoreMatrix uniqueValues(const bool& stable, const bool& byRows,
                             std::string* whyNot = nullptr) const;

    // find: the 1-based COLUMN-MAJOR indices of the non-zero entries, at most
    // `limit` of them (0 = all), taken from the end when `fromLast`.
    ICoreMatrix findIndices(const size_t& limit, const bool& fromLast,
                            std::string* whyNot = nullptr) const;

    // sub2ind over a rows x cols shape: 1-based, column-major, element-wise
    // over the two subscript matrices.
    static ICoreMatrix subToInd(const size_t& rows, const size_t& cols,
                                const ICoreMatrix& rowSubs, const ICoreMatrix& colSubs,
                                std::string* whyNot = nullptr);

    // ismember: 1.0 where the entry appears in `set`, shaped like *this.
    // `byRows` asks the question of whole rows instead, one answer per row.
    ICoreMatrix memberOf(const ICoreMatrix& set) const;
    ICoreMatrix memberOfRows(const ICoreMatrix& set, std::string* whyNot = nullptr) const;

    // The four set operations, over elements or over whole rows.
    enum class SetOp { Union, Intersect, Difference, SymmetricDifference };
    static ICoreMatrix setOperation(const ICoreMatrix& first, const ICoreMatrix& second,
                                    const SetOp& op, const bool& stable,
                                    std::string* whyNot = nullptr);
    static ICoreMatrix setOperationRows(const ICoreMatrix& first, const ICoreMatrix& second,
                                        const SetOp& op, std::string* whyNot = nullptr);

    // ---- Core MATLAB's construction and scalar linear algebra (C4, C7) -----
    //
    // Same contract as the shape family above: MATLAB semantics at the
    // language level, and an operation whose MATLAB answer is EMPTY refuses
    // through whyNot rather than handing back a 0 x n the rest of the console
    // cannot compute with (C3.9).

    // Rectangular identity (MATLAB eye(m, n)): ones on the main diagonal.
    static ICoreMatrix eye(const size_t& size1, const size_t& size2);

    // MATLAB's magic(n) square, algorithm for algorithm: the Siamese method
    // for odd n, the complement pattern when 4 divides n, and the LUX row
    // exchange for the singly even n -- which is why magic(2) is MATLAB's
    // degenerate [1 3; 4 2] rather than a refusal. n = 0 refuses (C3.9).
    static ICoreMatrix magic(const size_t& n, std::string* whyNot = nullptr);

    // MATLAB's colon(a, s, b), a ROW vector. The element COUNT is MATLAB's:
    // an all-integer triple counts by truncation, anything else rounds and
    // then steps back when a + n*s overshoots b by more than the tolerance.
    // The last element is set to b EXACTLY when it lands inside that
    // tolerance, which is why 0:0.1:1 ends at 1 and not at 0.9999999999999999.
    // An empty range (s = 0, or b out of reach) refuses (C3.9).
    static ICoreMatrix colon(const double& from, const double& step, const double& to,
                             std::string* whyNot = nullptr);

    // Products, the mirror of sum()/sumRows()/sumCols(): over every entry,
    // along each row (a column vector), down each column (a row vector).
    double prod() const;
    ICoreMatrix prodRows() const;
    ICoreMatrix prodCols() const;

    // MATLAB's norm(A, p). A VECTOR takes any p > 0 and both infinities
    // (-Inf is the smallest magnitude); a MATRIX takes 1, 2 and Inf, since
    // 'fro' is norm_Frobenius() above. Any other p refuses, as MATLAB errors.
    double norm_p(const double& p, std::string* whyNot = nullptr) const;

    // MATLAB's vecnorm: the p-norm of each column (dim = 1, a row vector) or
    // of each row (dim = 2, a column vector).
    ICoreMatrix vecnorm(const double& p, const int& dim, std::string* whyNot = nullptr) const;

    // MATLAB's rcond, which is LAPACK's one-norm condition ESTIMATE and not
    // the quantity it estimates -- so this is the estimator (DLACN2 driven the
    // way DGECON drives it), not 1/(norm(A,1)*norm(inv(A),1)). The two differ
    // by 15% on the parity suite's own 4x4 fixture, measured against R2026a
    // 2026-09-02 (C7.7). A singular A answers 0, as MATLAB's does.
    double rcond() const;

    // MATLAB's cond(A, p) = norm(A, p) * norm(inv(A), p). p = 2 comes from the
    // SVD (conditionNumber() above); p = -1 is this file's spelling of
    // MATLAB's 'fro', the one p that is not a number.
    double conditionNumber(const double& p, std::string* whyNot = nullptr) const;

    // MATLAB's DEFAULT rank tolerance, max(m, n) * eps(largest singular
    // value). rank(tol) above already counts the singular values above tol --
    // MATLAB's rule; only the default differed, and this is that default.
    double rankDefaultTolerance() const;

    // MATLAB's inv(): a singular operand answers an all-Inf matrix and says so
    // through wasSingular, where inverse() above returns the default matrix
    // and logs an error. Both exist because this tree's own callers (the
    // state-space discretizations, the descriptor block) test that empty.
    ICoreMatrix inverseOrInf(bool* wasSingular = nullptr) const;

    // ---- Cumulative reductions, integration and binning (C4.32, C4.37,
    // ---- C4.38, C7.15, C8.16, C8.17) --------------------------------------
    //
    // Same contract as the two families above: MATLAB semantics at the
    // language level, and an operation whose MATLAB answer is EMPTY refuses
    // through whyNot rather than handing back a 0 x n (C3.9).

    // Cumulative reductions along a dimension (MATLAB cumsum/cumprod/cummax/
    // cummin, C4.32). `dim` is 1 (down the columns) or 2 (along the rows); the
    // no-argument cumsum()/cumprod() above are the dim = firstNonSingletonDim()
    // spellings, which is MATLAB's own default. Unlike the collapsing
    // reductions these keep the shape of the input.
    ICoreMatrix cumsum(const int& dim) const;
    ICoreMatrix cumprod(const int& dim) const;
    ICoreMatrix cummax(const int& dim) const;
    ICoreMatrix cummin(const int& dim) const;

    // Trapezoidal integration (MATLAB trapz/cumtrapz, C8.16/C8.17). trapz
    // collapses `dim` to a single entry; cumtrapz keeps the shape and its
    // first entry along `dim` is 0. The x-taking forms integrate over a
    // non-uniform grid and need one x per sample along `dim` -- MATLAB does
    // not require x to be sorted or increasing, and neither do these; the
    // others use unit spacing.
    ICoreMatrix trapz(const int& dim) const;
    ICoreMatrix trapz(const ICoreMatrix& x, const int& dim,
                      std::string* whyNot = nullptr) const;
    ICoreMatrix cumtrapz(const int& dim) const;
    ICoreMatrix cumtrapz(const ICoreMatrix& x, const int& dim,
                         std::string* whyNot = nullptr) const;

    // MATLAB's numeric gradient (C8.18): a CENTRAL difference at every
    // interior sample and a ONE-SIDED difference at each end, so the answer
    // has the shape of the input rather than one entry fewer (that is diff()).
    // `dim` is 1 down the rows -- MATLAB's FY -- and 2 across the columns,
    // MATLAB's FX and the one a single-output gradient(A) answers.
    // `coords` is NULL for unit spacing, a SCALAR for uniform spacing, or one
    // coordinate per sample along `dim`, in which case the interior difference
    // is divided by the distance its two neighbours actually span. A lane of
    // one sample is 0, as MATLAB's gradient(5) is.
    ICoreMatrix gradient(const int& dim, const ICoreMatrix* coords,
                         std::string* whyNot = nullptr) const;

    // Orthonormal basis for the range of A (MATLAB orth, C7.15): the first
    // rank(A) left singular vectors, as columns. `tol` below zero asks for
    // MATLAB's automatic threshold, max(m, n) * eps * sigma_max -- the same
    // rule rankDefaultTolerance() above states. The basis is NOT unique (each
    // column's sign, and the order inside a repeated singular value, are
    // conventions), so a test pins the projector Q*Q', which is. A zero matrix
    // has an empty range and refuses (C3.9).
    ICoreMatrix orth(const double& tol = -1.0, std::string* whyNot = nullptr) const;

    // accumarray(subs, val), MATLAB's basic two-argument form (C4.37): subs is
    // an n x 1 column of 1-based row subscripts or an n x 2 of [row, col], and
    // val is one value per subscript row or a scalar broadcast over them all.
    // The result is max(subs) tall (and wide, for the two-column form), each
    // entry the SUM of the values that landed on it and zero where none did.
    static ICoreMatrix accumarray(const ICoreMatrix& subs, const ICoreMatrix& values,
                                  std::string* whyNot = nullptr);

    // histcounts (C4.38), in two halves so the binning rule can be tested on
    // its own. histogramEdges reproduces MATLAB's OWN bin chooser -- the
    // integer and Scott rules of toolbox/matlab/datafun/histcounts.m over
    // .../+matlab/+internal/+math/binpicker.m -- so histcounts(x) and
    // histcounts(x, nbins) are value-comparable against MATLAB rather than
    // merely plausible; `bins` 0 asks for the automatic rule, anything else
    // for that many bins. histcounts then counts x into `edges`: bin i is
    // [e(i), e(i+1)), the LAST bin is closed, values outside every bin and NaN
    // are not counted, and the result is a ROW one shorter than `edges`.
    static ICoreMatrix histogramEdges(const ICoreMatrix& x, const size_t& bins,
                                      std::string* whyNot = nullptr);
    static ICoreMatrix histcounts(const ICoreMatrix& x, const ICoreMatrix& edges,
                                  std::string* whyNot = nullptr);

    // ---- Concatenation, dot products, correlation, spectral shift and the
    // ---- two least-squares variants (C4.12, C5.38, C6.6, C7.19, C10.4) ----

    // MATLAB's cat(dim, A, B): dim 1 stacks, dim 2 joins side by side -- which
    // are concatVertical and concatHorizontal above, under MATLAB's spelling.
    ICoreMatrix cat(const int& dim, const ICoreMatrix& other,
                    std::string* whyNot = nullptr) const;

    // MATLAB's dot: the scalar product of two VECTORS, and column by column
    // (dim 1) or row by row (dim 2) for two matrices. The dot() above is the
    // console's older "over every element" reading and is NOT this one -- the
    // two agree on vectors and disagree on everything else (the board's C0.1).
    ICoreMatrix dotProduct(const ICoreMatrix& other, const int& dim,
                           std::string* whyNot = nullptr) const;

    // MATLAB's corrcoef(x, y): the 2 x 2 correlation of two vectors read as
    // two variables. correlation() above is corrcoef(A) for a data matrix,
    // and this is the pair form, which is corrcoef([x(:) y(:)]).
    static ICoreMatrix correlationOfPair(const ICoreMatrix& x, const ICoreMatrix& y,
                                         std::string* whyNot = nullptr);

    // MATLAB's fftshift/ifftshift: swap the halves of `dim` (0 = every
    // dimension) so a zero-frequency term moves to the centre and back. Both
    // are a circshift of floor(n/2) -- forward one way, backward the other --
    // and that is the whole of it. They differ only for an ODD length, where
    // the split is off centre and shifting the same way twice does not return.
    ICoreMatrix fftshift(const int& dim, const bool& inverse) const;

    // MATLAB's lsqminnorm(A, b): of all the least-squares solutions, the one
    // of smallest norm. Equal to A\b whenever A has full column rank, and the
    // pseudo-inverse solution when it does not -- which is exactly the case
    // the backslash operator refuses to guess at (C1.3).
    ICoreMatrix lsqminnorm(const ICoreMatrix& b, std::string* whyNot = nullptr) const;

    // MATLAB's lscov(A, b) and lscov(A, b, w): ordinary and weighted least
    // squares, the weighted form scaling every row of A and b by sqrt(w)
    // first -- MATLAB's own reduction, from optimfun-adjacent lscov.m. Pass a
    // 1 x 1 of 1.0 for the unweighted form. A rank-deficient design refuses:
    // MATLAB warns and returns a BASIC solution, which is a different vector
    // from the minimum-norm one and nothing in the call says which was meant.
    ICoreMatrix lscov(const ICoreMatrix& b, const ICoreMatrix& weights,
                      std::string* whyNot = nullptr) const;

    // ---- The one random generator, and MATLAB's rng (C4.4-C4.7) -----------
    //
    // rand, randn, randi and randperm all draw from ONE Mersenne Twister,
    // because rng(seed) has to mean something for all four at once. It starts
    // seeded 0, which is MATLAB's own startup state (rng('default')), so a
    // fresh console replays a fresh MATLAB's SEQUENCE OF DRAWS -- not its
    // values: MATLAB's stream and mt19937's never agree sample for sample,
    // which is why every parity row in this family is Skip (C0.9). What the
    // seed buys, and what the regress cases assert, is that the same seed
    // replays the same stream.
    static unsigned int randomSeed();                          // the seed now in force
    static unsigned int seedRandom(const unsigned int& seed);  // set it; returns the PREVIOUS seed
    static unsigned int shuffleRandom();                       // seed unpredictably; returns the PREVIOUS seed

    // MATLAB's randi: whole numbers drawn uniformly from the CLOSED range
    // [low, high]. Both ends must be whole numbers and low must not exceed
    // high, as MATLAB requires.
    static ICoreMatrix randi(const double& low, const double& high,
                             const size_t& size1, const size_t& size2,
                             std::string* whyNot = nullptr);

    // MATLAB's randperm(n) / randperm(n, k): k values drawn from 1..n WITHOUT
    // replacement, as a ROW -- MATLAB's shape, and a new name takes it from
    // birth. k = n is the full permutation. k > n refuses, as MATLAB errors.
    static ICoreMatrix randperm(const size_t& n, const size_t& k,
                                std::string* whyNot = nullptr);

    // MATLAB's lsqnonneg(C, d): the least-squares solution of C*x = d subject
    // to x >= 0, by the Lawson-Hanson active-set algorithm transcribed from
    // optimfun/lsqnonneg.m -- its convergence tolerance included,
    // 10 * eps * norm(C, 1) * max(size(C)), because that tolerance is what
    // decides which variables come back exactly zero (C7.20).
    ICoreMatrix lsqnonneg(const ICoreMatrix& d, std::string* whyNot = nullptr) const;

    // Convolution (MATLAB conv and conv2). `shape` is "full" (the whole
    // result), "same" (the central part, the size of the FIRST operand) or
    // "valid" (only the positions where the operands fully overlap). A
    // "valid" result that would be empty refuses (C3.9), as does a 1-D
    // convolution of anything but two vectors, which MATLAB refuses too.
    //
    // MATLAB's orientation rule for the 1-D form: the answer is a column only
    // when BOTH operands are column-shaped; a row anywhere makes it a row.
    static ICoreMatrix convolve(const ICoreMatrix& first, const ICoreMatrix& second,
                                const std::string& shape, std::string* whyNot = nullptr);
    static ICoreMatrix convolve2D(const ICoreMatrix& first, const ICoreMatrix& second,
                                  const std::string& shape, std::string* whyNot = nullptr);

    // MATLAB's filter (the core-MATLAB row C10.6): the rational difference
    // equation
    //
    //     a(1)y(n) = b(1)x(n) + ... + b(N)x(n-N+1) - a(2)y(n-1) - ... - a(N)y(n-N+1)
    //
    // run in the DIRECT FORM II TRANSPOSED structure MATLAB itself uses. The
    // structure is not an implementation detail here: it is what defines the
    // state vector, so zi and zf mean what MATLAB's mean only under this form.
    // b and a are vectors, zero-padded to the longer of the two; a(1) must be
    // non-zero and both are divided through by it, as MATLAB does.
    //
    // `dim` is 1 (down each column) or 2 (along each row); 0 asks for MATLAB's
    // default, the first non-singleton dimension -- 2 for a row vector, 1 for
    // everything else.
    //
    // `initial` is MATLAB's zi (nullptr for none) and `finalState` its zf
    // (nullptr when the caller does not want it). Both carry the state along
    // the LEADING dimension whatever `dim` is: with N-1 states and S signals
    // they are (N-1) BY S, even when dim is 2 and the signals run across the
    // rows -- so filtering a 3x4 array along dim 2 answers a 2x3 zf, not 3x2.
    // Measured against MATLAB R2026a on 2026-09-02; MATLAB's own error message
    // for a mis-shaped zi says it in as many words ("an array with the leading
    // dimension of size max(length(a),length(b))-1 and with remaining
    // dimensions matching those of x").
    //
    // A single signal takes any vector of N-1 initial states, whichever way it
    // is turned; several signals need the (N-1) BY S array exactly.
    static ICoreMatrix filter(const ICoreMatrix& numerator, const ICoreMatrix& denominator,
                              const ICoreMatrix& x, const ICoreMatrix* initial,
                              const int& dim, ICoreMatrix* finalState,
                              std::string* whyNot = nullptr);

    // ---- The discrete field operators (the core board's C8.27) ------------
    //
    // All three are core MATLAB -- `which del2` is datafun/, `which curl` and
    // `which divergence` are graphics/specgraph/ -- so the board's "🚫 N-D
    // fields" was a statement about their THREE-dimensional forms only. The
    // 2-D forms are these, and the N-D ones stay out under Z6.

    // MATLAB's del2: the discrete Laplacian, DIVIDED BY 2*ndims. It is
    // transcribed from `toolbox/matlab/datafun/del2.m` rather than derived,
    // because two of its rules are not what a derivation would produce:
    //
    //   * the divisor is `ndims(f)` AFTER MATLAB has made a vector into a
    //     column, and MATLAB's ndims is never below 2 -- so a vector is
    //     divided by 2 as a matrix is, and del2([1 4 9 16]) is 0.5 and not 1
    //     even though only one dimension was differenced.
    //   * the end samples are a LINEAR EXTRAPOLATION of the interior second
    //     differences when there are more than three samples along the
    //     dimension, a copy of the single interior value when there are
    //     exactly three, and 0 when there are two or fewer.
    //
    // `hx`/`hy` are NULL for unit spacing, a scalar step, or one coordinate
    // per sample; del2(f, h) applies the one step to BOTH dimensions, which is
    // what MATLAB's scalar form does. A row vector answers a row.
    static ICoreMatrix laplacian(const ICoreMatrix& f, const ICoreMatrix* hx,
                                 const ICoreMatrix* hy, std::string* whyNot = nullptr);

    // MATLAB's 2-D divergence: d(u)/dx + d(v)/dy, each term the numeric
    // gradient above. `x`/`y` are the meshgrid pair (NULL for unit spacing);
    // only x's first ROW and y's first COLUMN are read, which is what
    // MATLAB's own divergence.m does with them.
    static ICoreMatrix divergence2D(const ICoreMatrix& u, const ICoreMatrix& v,
                                    const ICoreMatrix* x, const ICoreMatrix* y,
                                    std::string* whyNot = nullptr);

    // MATLAB's 2-D curl: d(v)/dx - d(u)/dy.
    //
    // **This function answers a DIFFERENT number depending on how many
    // outputs are asked for**, and it is MATLAB's rule rather than a
    // convenience: `curl(X, Y, U, V)` with ONE output is the ANGULAR VELOCITY
    // -- half the curl -- while `[curlz, cav] = curl(X, Y, U, V)` answers the
    // curl first and the angular velocity second. Read off curl.m and
    // measured against R2026a on 2026-09-02: for U = X.*Y, V = X.^2 on
    // meshgrid(1:4, 1:4) the one-output form is 1, 1, 1.5, 1.5 across each
    // row where the curl itself is 2, 2, 3, 3. `angularVelocityOnly` says
    // which the caller is asking for.
    static ICoreMatrix curl2D(const ICoreMatrix& u, const ICoreMatrix& v,
                              const ICoreMatrix* x, const ICoreMatrix* y,
                              const bool& angularVelocityOnly,
                              std::string* whyNot = nullptr);

    // ---- The two signal functions that are core MATLAB (C10.10) -----------
    //
    // `which detrend` and `which xcorr` are BOTH toolbox/matlab/datafun/ in
    // R2026a -- base MATLAB, not the Signal Processing Toolbox -- which is
    // the measurement the board's row asked for before marking itself 🚫.
    // The other seven names on that row are signal/signal/ and stay refused.

    // MATLAB's detrend: subtract the least-squares polynomial of degree
    // `degree` from each COLUMN (from the vector, whichever way it is
    // turned). Degree 0 is MATLAB's 'constant' and is the column mean
    // subtracted exactly; degree 1 is the default and MATLAB's 'linear'.
    //
    // The fit is over the SCALED sample points detrend.m uses -- s./s(end),
    // so the last one is 1 -- rather than over 1..N. It is the same fit in
    // exact arithmetic and a better-conditioned one in this one, and matching
    // MATLAB's conditioning is the difference between agreeing to the last
    // bit and agreeing to the tenth digit on a long ramp.
    static ICoreMatrix detrend(const ICoreMatrix& x, const size_t& degree,
                               std::string* whyNot = nullptr);

    // MATLAB's xcorr, for VECTORS: the raw cross-correlation
    //
    //     c(lag) = sum_n x(n + lag) * y(n)
    //
    // at every lag from -maxlag to +maxlag, so the answer has 2*maxlag+1
    // entries and c's middle entry is lag 0. `y` empty asks for the
    // AUTOcorrelation. `maxlag` below zero asks for MATLAB's default,
    // max(numel(x), numel(y)) - 1; a larger one zero-pads, as MATLAB's does.
    //
    // `scale` is "none" (the default), "biased" (divided by N), "unbiased"
    // (divided by N - |lag|, with a non-positive divisor clamped to 1, which
    // is MATLAB's own guard) or "coeff"/"normalized" -- the autocorrelation
    // divided by its own zero-lag value, or the cross-correlation divided by
    // sqrt(sum(x.^2) * sum(y.^2)). Every scale but "none" needs the two
    // vectors the same length, which is MATLAB's NoScale error.
    //
    // The orientation of the ANSWER follows x: a row answers a row, a column a
    // column. The LAGS are a row either way, which is MATLAB's shape and the
    // one asymmetry here -- a lag axis is not a second signal.
    //
    // This is the direct O(N * lags) sum where MATLAB's is an FFT and an
    // inverse FFT, so the two agree to rounding rather than exactly -- and it
    // is MATLAB that carries the error: xcorr([1 2 3 4]) answers
    // 3.9999999999999982 at its end lags where the sum of products is 4.
    // `y` is NULL for the autocorrelation. It is a pointer rather than an
    // empty matrix because a default-constructed ICoreMatrix is a 1 x 1
    // holding ZERO, not an empty one -- the same trap that made chol() answer
    // a silent 0 before C7.12.
    static ICoreMatrix xcorr(const ICoreMatrix& x, const ICoreMatrix* y,
                             const int& maxlag, const std::string& scale,
                             ICoreMatrix* lags, std::string* whyNot = nullptr);

    // Statistics: columns are variables, rows are observations
    ICoreMatrix covariance() const;
    ICoreMatrix correlation() const;

    // Vector-only helpers
    ICoreMatrix normalize() const;                    // unit vector (v / ||v||)
    double angleTo(const ICoreMatrix& other) const;    // angle between two vectors, radians

    // Sylvester-family linear matrix equations, solved via vec()/Kronecker
    // reduction. A unique solution needs the operator nonsingular — lyap:
    // eig(A)+eig(A) never 0; dlyap: eig(A)*eig(A) never 1; sylvester:
    // eig(A)+eig(B) never 0. A singular operator makes the QR solve return a
    // NON-solution with no diagnostic, so each solver verifies its residual
    // afterwards; on failure it reports (into whyNot when given, the run
    // diagnosis otherwise) and returns the default matrix. Found by the
    // command-parity suite: MATLAB returns NaN on the same inputs.
    ICoreMatrix lyap(const ICoreMatrix& Q, std::string* whyNot = nullptr) const;              // A*X + X*A^T + Q = 0
    ICoreMatrix dlyap(const ICoreMatrix& Q, std::string* whyNot = nullptr) const;             // A*X*A^T - X + Q = 0
    ICoreMatrix sylvester(const ICoreMatrix& B, const ICoreMatrix& C,
                          std::string* whyNot = nullptr) const;                               // A*X + X*B = C

    // The GENERALIZED (descriptor) members of the same family, and the
    // discrete Sylvester form -- MATLAB's lyap(A, Q, [], E), dlyap(A, Q, [], E)
    // and dlyap(A, B, C) (the toolbox board's T1.28). Same vec()/
    // Kronecker reduction, same rank-and-residual verification, so a singular
    // operator is reported rather than answered:
    //   lyapGeneralized   A*X*E' + E*X*A' + Q = 0   (E⊗A + A⊗E) vec(X) = -vec(Q)
    //   dlyapGeneralized  A*X*A' - E*X*E' + Q = 0   (A⊗A - E⊗E) vec(X) = -vec(Q)
    //   dlyapSylvester    A*X*B - X + C = 0         (B'⊗A - I)  vec(X) = -vec(C)
    //
    // Note the SIGNS, which are MATLAB's and not this family's: the two
    // Lyapunov forms take Q on the left-hand side and answer the X that makes
    // the sum ZERO, while sylvester() above takes C on the RIGHT and answers
    // A*X + X*B = C. That is why MATLAB's lyap(A, B, C) is this console's
    // sylvester(A, B, -C) and not a fourth solver.
    ICoreMatrix lyapGeneralized(const ICoreMatrix& Q, const ICoreMatrix& E,
                                std::string* whyNot = nullptr) const;
    ICoreMatrix dlyapGeneralized(const ICoreMatrix& Q, const ICoreMatrix& E,
                                 std::string* whyNot = nullptr) const;
    ICoreMatrix dlyapSylvester(const ICoreMatrix& B, const ICoreMatrix& C,
                               std::string* whyNot = nullptr) const;

    // Algebraic Riccati equations (LQR/Kalman-style design). Solved via the stable
    // invariant subspace of the Hamiltonian (care) / symplectic pencil (dare).
    // Assumes (A,B) stabilizable and (A,Q) detectable, with no eigenvalues exactly
    // on the stability boundary (imaginary axis / unit circle).
    ICoreMatrix care(const ICoreMatrix& B, const ICoreMatrix& Q, const ICoreMatrix& R) const;  // A^T X + X A - X B R^-1 B^T X + Q = 0
    ICoreMatrix dare(const ICoreMatrix& B, const ICoreMatrix& Q, const ICoreMatrix& R) const;  // A^T X A - X - A^T X B (R+B^T X B)^-1 B^T X A + Q = 0

    Eigen::MatrixXd toEigen2DMatrixXd() const;

    ////////////////////////////////////////////////////
    ///
    ///     Matrix print-out operator
    ///
    ////////////////////////////////////////////////////

    // Declare friend operator<< for output streaming
    friend std::ostream& operator<<(std::ostream& os, const ICoreMatrix& mat);

    // A matrix is copied constantly -- by value into and out of nearly every
    // numeric call in the tree -- so the type stays copyable and nothrow-movable.
    // The unique_ptr residue below deletes the implicit copy, so all four are
    // written out in the .cpp.
    //
    // THE COST, recorded rather than hidden: an ICoreMatrix now costs TWO heap
    // allocations to build instead of one (the Impl, then its data vector) and
    // one more indirection per element access. It already allocated for `data`
    // on every copy, so this is a constant factor on an allocating path, not a
    // new allocation on a free one. See the SH2 notes in HEADER_SURFACE.md.
    ICoreMatrix(const ICoreMatrix& other);
    ICoreMatrix& operator=(const ICoreMatrix& other);
    ICoreMatrix(ICoreMatrix&& other) noexcept;
    ICoreMatrix& operator=(ICoreMatrix&& other) noexcept;

    // Declared, defined in the .cpp: unique_ptr cannot destroy an incomplete Impl.
    ~ICoreMatrix();

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

ICorePolynomial.h#

src/ICoreBlocks/ICoreMath/Foundation/ICorePolynomial.h

MATLAB's polyval and polyvalm (the core-MATLAB board, C8.2 and C8.10). polyval evaluates at EVERY entry of x and keeps its shape; polyvalm evaluates the polynomial IN the matrix -- x^2 is A*A, not A .* A, and the constant term is that multiple of the identity, so A must be square.

ICorePolynomial#

ICorePolynomial.h:9 · class · 25 declaration(s)

class ICorePolynomial {
public:
    ICorePolynomial();
    explicit ICorePolynomial(const double& coefficient);
    explicit ICorePolynomial(const std::vector<double>& coefficients);
    explicit ICorePolynomial(const ICoreMatrix& coefficients);

    double evaluate(const double& x) const;

    // MATLAB's polyval and polyvalm (the core-MATLAB board, C8.2 and C8.10).
    // polyval evaluates at EVERY entry of x and keeps its shape; polyvalm
    // evaluates the polynomial IN the matrix -- x^2 is A*A, not A .* A, and
    // the constant term is that multiple of the identity, so A must be square.
    ICoreMatrix evaluateElementWise(const ICoreMatrix& x) const;
    ICoreMatrix evaluateMatrix(const ICoreMatrix& matrix, std::string* whyNot = nullptr) const;
    ICoreComplexVariable evaluateComplex(const double &real, const double &imag) const;
    ICorePolynomial derivative() const;
    ICorePolynomial integral(const double &constant) const;

    void normalize();
    void trimTrailingZeros(double tol = 1e-14);

    ICorePolynomial operator+(const ICorePolynomial &other) const;
    ICorePolynomial operator-(const ICorePolynomial &other) const;
    ICorePolynomial operator*(const ICorePolynomial &other) const;
    std::pair<ICorePolynomial, ICorePolynomial> divide(const ICorePolynomial &divisor) const;

    // MATLAB's polyfit (the core-MATLAB board, C8.5): the degree-n polynomial
    // whose coefficients least-squares fit the samples, answered as a ROW of
    // n+1 coefficients highest power first -- MATLAB has no polynomial object
    // to hand back, and a column x and y still answer a row.
    //
    // The fit is the backslash solution of the Vandermonde system, which is
    // what MATLAB's own polyfit.m computes (a column-pivoting QR).
    //
    // `degree` must be strictly below the number of samples. MATLAB warns and
    // answers a rank-deficient particular solution instead; that solution is
    // not the fit anyone meant, so this refuses with MATLAB's own condition.
    static ICoreMatrix fit(const ICoreMatrix& x, const ICoreMatrix& y, const size_t& degree,
                           std::string* whyNot = nullptr);

    // MATLAB's residue (the core-MATLAB row C8.11): the partial fraction
    // expansion of b(s)/a(s),
    //
    //     b(s)/a(s) = r(1)/(s - p(1)) + ... + r(n)/(s - p(n)) + k(s)
    //
    // with a repeated pole taking consecutive terms of RISING power --
    // r(j)/(s - p)^1, r(j+1)/(s - p)^2, ... -- which is MATLAB's own layout
    // and is why r and p are the same length even when p repeats.
    //
    // The residues are solved as a LINEAR SYSTEM rather than through the
    // derivative formula: multiplying the expansion by a(s) turns every term
    // into the polynomial a(s)/(s - p(j))^e(j), so the coefficients of b give
    // n equations in the n unknown residues. That is what MATLAB's own
    // residue.m does, and it is the reason a repeated pole and a complex one
    // need no special case here -- the derivative formula needs both.
    //
    // `poles` is roots(a) with equal poles adjacent; `residues` follows it
    // term for term; `direct` is the polynomial part, EMPTY when b is of lower
    // degree than a (MATLAB answers a 0x0 there, and so does this since C3.9).
    // All three come back as columns, MATLAB's shape.
    //
    // Poles are complex in general, so r and p are complex too and narrow to
    // real at the console boundary when nothing imaginary survives (C0.2).
    static bool residue(const ICorePolynomial& numerator, const ICorePolynomial& denominator,
                        ICoreComplexMatrix& residues, ICoreComplexMatrix& poles,
                        ICoreMatrix& direct, std::string* whyNot = nullptr);

    static ICorePolynomial fromRoots(const std::vector<ICoreComplexVariable> &roots);
    std::vector<ICoreComplexVariable> roots() const;

    ICoreMatrix companionMatrix() const;
    bool isStableContinuous() const;
    bool isStableDiscrete() const;

    ICoreMatrix getCoefficients() const;
    int getOrder() const;

    void print(const std::string& varName = "x") const;

    ~ICorePolynomial();

};

ICoreRecord.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreRecord.h

ICoreRecord#

ICoreRecord.h:45 · class · pImpl · 23 declaration(s)

MATLAB's scalar struct, as much of one as this console admits: an ORDERED, NAMED list of numeric fields, read by .name and nothing else (the toolbox board's T0.6 (A) and X3).

class ICoreRecord {
public:
    // NOT `= default`: the residue below is a unique_ptr, and a defaulted
    // default constructor leaves it null -- which compiles, links, and
    // dereferences null on first use.
    ICoreRecord();

    // Records are stored in the variables space and handed around by value, so
    // all four copy/move operations are written out in the .cpp.
    ICoreRecord(const ICoreRecord& other);
    ICoreRecord& operator=(const ICoreRecord& other);
    ICoreRecord(ICoreRecord&& other) noexcept;
    ICoreRecord& operator=(ICoreRecord&& other) noexcept;
    ~ICoreRecord();

    void clear();

    // Appends in the order MATLAB answers the fields. A name already present is
    // REPLACED in place rather than appended twice, so a caller that builds a
    // record in a loop cannot produce two fields with one name -- MATLAB's
    // struct cannot hold that and neither can the storage string.
    void append(const std::string& name, const ICoreMatrix& value);
    void appendScalar(const std::string& name, const double& value);

    // Records a field MATLAB answers that this console does not hold, so that
    // reading it refuses BY NAME. `why` is the whole diagnostic the reader
    // prints; it should name the rule and the row (see the class note).
    void withhold(const std::string& name, const std::string& why);

    [[nodiscard]] bool   isEmpty() const;
    [[nodiscard]] size_t fieldCount() const;
    [[nodiscard]] const std::string& fieldName(const size_t& index) const;
    [[nodiscard]] const ICoreMatrix& fieldValue(const size_t& index) const;
    [[nodiscard]] std::vector<std::string> fieldNames() const;

    [[nodiscard]] bool hasField(const std::string& name) const;
    [[nodiscard]] bool getField(const std::string& name, ICoreMatrix& out) const;

    // True when `name` is a field MATLAB answers here but this console withheld;
    // `why` comes back with the diagnostic to print. Kept separate from
    // hasField() so a reader cannot confuse "absent" with "refused".
    [[nodiscard]] bool isWithheld(const std::string& name, std::string& why) const;
    [[nodiscard]] std::vector<std::string> withheldNames() const;

    // MATLAB's scalar-struct display: names right-aligned to the longest, one
    // field per line, a non-scalar shown as its SIZE and not its values --
    // `lower: [2x1 double]` -- exactly as MATLAB does. The numbers are
    // MATLAB's `format short`, which this console already reproduces wherever
    // a display is MATLAB's (a tf prints 0.3333, not 0.333333).
    //
    // ⚠ This display shows the numeric fields ONLY, so for a solver record
    // carrying withheld character fields it is SHORTER than MATLAB's. That is
    // deliberate and it is why the parity cases for these rows compare
    // `out.iterations` and not `disp(out)` -- printing `algorithm: '...'` for
    // a field the console refuses to hand back would be a display promising a
    // value that no read can reach.
    [[nodiscard]] std::string matlabDisplay() const;

    // The lossless storage/bridge form: `struct('a', 1, 'b', [1, 2])`, with
    // every number written at full precision. This is what the variables space
    // stores and what crosses to MATLAB (X3); the import already admits a
    // struct(...) call for the time-series shape, so the spelling is not new.
    [[nodiscard]] std::string toStructCall() const;
    static bool fromStructCall(const std::string& text, ICoreRecord& out, std::string& error);

    // MATLAB's `format short` for ONE scalar, which is the display rule for a
    // struct field and is NOT the four-significant-digit rule a tf's
    // coefficients use. Public because the record is not the only caller that
    // will want it, and two implementations of one rule is how they drift.
    static std::string formatShort(const double& v);

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};

ICoreSpline.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreSpline.h

ICoreSpline#

ICoreSpline.h:12 · class · nested PiecewiseCubic · 0 declaration(s)

Interpolation through (x,y) sample points, backed by Eigen::Spline / Eigen::SplineFitting.

class ICoreSpline {
public:
    // The B-spline fit behind the Studio Spline Fitting tool -- a spline of a
    // CHOSEN degree through the samples, which is the one thing on this class
    // MATLAB has no name for. It was called interp1() until C8.12 gave that
    // name MATLAB's meaning; the degree argument it reads is what made the two
    // impossible to keep under one name, since MATLAB's fourth argument is a
    // method WORD.
    //
    // x, y: vectors of the same length (>= 2), x need not be evenly spaced but
    // must be strictly increasing. xq: query points. degree: spline degree
    // (default 3 = cubic); reduced automatically if there are too few points.
    // Returns yq, one value per entry of xq. Queries outside [x(1), x(end)]
    // are CLAMPED to the ends rather than extrapolated.
    static ICoreMatrix bSplineFit(const ICoreMatrix& x, const ICoreMatrix& y,
                                  const ICoreMatrix& xq, size_t degree = 3);

    // MATLAB's interp1 (the core-MATLAB board, C8.12), whose default is
    // LINEAR -- the divergence C0.1 decided in MATLAB's favour.
    //
    // The six methods are the ones with a single meaning. MATLAB's 'cubic' is
    // deliberately absent: measured against R2026a 2026-09-02 it is a THIRD
    // curve, neither Spline nor Pchip (on the uniform grid x = 1:4,
    // v = [1 3 2 5] the three answer 2.375, 2.8125 and 2.4375 at xq = 1.5),
    // and on a non-uniform grid it warns and silently becomes 'spline' while
    // still refusing to extrapolate. Mapping it onto either of these would be
    // a wrong number rather than a missing feature, so it is refused by name.
    //
    // `extrapolate` is MATLAB's 'extrap': false fills every query outside
    // [min(x), max(x)] with `outsideValue` (MATLAB's extrapval, NaN when the
    // caller passes none). MATLAB's DEFAULT differs by method -- Spline and
    // Pchip extrapolate, the other four do not -- and that choice belongs to
    // the caller, which is why it is a parameter here rather than a rule.
    // Two measured asymmetries survive `extrapolate`: Previous has no previous
    // sample BELOW the range and Next has no next one above, so those stay NaN
    // however the flag is set.
    //
    // x and v are vectors of the same length (>= 2) with distinct entries;
    // neither needs to be sorted (this sorts on x, as MATLAB does) and a
    // decreasing x is legal. The answer has the SHAPE of xq.
    enum class Method { Linear, Nearest, Next, Previous, Spline, Pchip };
    static ICoreMatrix interp1(const ICoreMatrix& x, const ICoreMatrix& v,
                               const ICoreMatrix& xq, const Method& method,
                               const bool& extrapolate, const double& outsideValue,
                               std::string* whyNot = nullptr);

    // MATLAB's spline() and pchip() (the core-MATLAB board, C8.14). Neither
    // goes through Eigen: both are the piecewise cubic MATLAB itself builds,
    // and they differ only in how the slope at each sample point is chosen.
    //
    //   spline  the NOT-A-KNOT cubic: the third derivative is continuous
    //           across the second and the second-to-last points, which is what
    //           makes two points a straight line and three a single parabola.
    //   pchip   the shape-preserving cubic: the slope at a sample is a
    //           weighted harmonic mean of its neighbouring secants, and ZERO
    //           wherever they change sign, so the result never overshoots.
    //
    // Both EXTRAPOLATE outside [x(1), x(end)] by continuing the end cubic,
    // as MATLAB does. x need not be sorted (both sort it, as MATLAB does) but
    // must hold no repeats; x and y must be the same length.
    static ICoreMatrix splineNotAKnot(const ICoreMatrix& x, const ICoreMatrix& y,
                                      const ICoreMatrix& xq, std::string* whyNot = nullptr);
    static ICoreMatrix pchip(const ICoreMatrix& x, const ICoreMatrix& y,
                             const ICoreMatrix& xq, std::string* whyNot = nullptr);

    // The same two curves handed over as a FORM rather than evaluated: the
    // sorted samples and the slope at each of them, which with the Hermite
    // basis is the piecewise cubic itself (toolbox row T8.4). `spline` and
    // `pchip` above are these two plus an evaluation, so there is one
    // implementation of each curve and not two -- a caller that needs the
    // COEFFICIENTS (to differentiate a fit exactly, or to integrate one)
    // builds them from here rather than re-deriving the curve.
    struct PiecewiseCubic {
        std::vector<double> x;        // sorted, no repeats
        std::vector<double> y;
        std::vector<double> slopes;   // one per sample
    };
    static bool notAKnotForm(const ICoreMatrix& x, const ICoreMatrix& y,
                             PiecewiseCubic& out, std::string* whyNot = nullptr);
    static bool pchipForm(const ICoreMatrix& x, const ICoreMatrix& y,
                          PiecewiseCubic& out, std::string* whyNot = nullptr);

    // MATLAB's interp2 (the core-MATLAB board, C8.13) on a plaid grid.
    //
    // It is the SEPARABLE application of interp1 above, not a second
    // algorithm: interpolate every grid column over y at the query rows, then
    // interpolate those across x at the query columns. For each of the four
    // methods that is exactly what MATLAB's own 2-D interpolant is -- a
    // tensor product factors that way by construction -- so the two spellings
    // cannot drift, the way conv2 and filter2 share one convolution.
    //
    // X and Y are either the meshgrid arrays (every row of X the same, every
    // column of Y the same -- MATLAB refuses a grid that is not plaid, and so
    // does this) or the two vectors they were made from. V is size(Y) BY
    // size(X). Xq and Yq broadcast against each other the way MATLAB's do: the
    // same shape is element-wise, a row against a column is the grid of pairs.
    //
    // The four methods are the ones with a single meaning in TWO dimensions,
    // and the list is shorter than interp1's for reasons measured against
    // R2026a on 2026-09-02 rather than assumed:
    //
    //   previous, pchip   MATLAB accepts the word, WARNS that it is 1-D only,
    //                     and silently interpolates linearly instead. A word
    //                     that changes method behind the caller's back is the
    //                     one answer that cannot be right, so it is refused.
    //   cubic, v5cubic    a THIRD curve again (C8.12): on the uniform grid
    //                     x = y = 1:4 the three answer 1.265625 (cubic),
    //                     0.785156 (spline) and 1.334248 (makima) at
    //                     (1.5, 1.5). On a non-uniform grid it also warns and
    //                     becomes spline.
    //   makima            a fourth curve, which interp1 does not have either.
    //
    // `extrapolate` and `outsideValue` mean what they mean on interp1, and
    // MATLAB's default is per method there too: Spline extrapolates, the other
    // three answer `outsideValue` (NaN unless the caller passes one) at any
    // query outside the grid in EITHER coordinate.
    enum class Method2D { Linear, Nearest, Next, Spline };
    static ICoreMatrix interp2(const ICoreMatrix& gridX, const ICoreMatrix& gridY,
                               const ICoreMatrix& values,
                               const ICoreMatrix& queryX, const ICoreMatrix& queryY,
                               const Method2D& method, const bool& extrapolate,
                               const double& outsideValue, std::string* whyNot = nullptr);

};
};

ICoreStatistics.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreStatistics.h

ICoreStatistics#

ICoreStatistics.h:24 · class · 1 declaration(s)

Core MATLAB's descriptive statistics (the core-MATLAB board, C6): median, var/std with MATLAB's weight argument, cov's three readings, rescale, and the mov* sliding-window family.

class ICoreStatistics {
public:

    // The four reductions MATLAB spells with one name and an axis (C5.32-C5.35):
    // sum, mean, max and min. `dim` is 1, 2 or 0 for the default (the first
    // non-singleton dimension), `everyElement` is MATLAB's "all", and
    // `omitNaN` is its "omitnan".
    //
    // **The NaN rules differ by function and they are MATLAB's, not a choice
    // made here**: sum and mean PROPAGATE a NaN (one NaN makes the whole slice
    // NaN) unless "omitnan" is asked for, while max and min IGNORE one always
    // -- `max([1 NaN 3])` is 3, and "omitnan" only says out loud what max
    // already does. A slice that is ALL NaN answers NaN whichever way.
    //
    // `indices`, when non-null, receives MATLAB's second output for max/min:
    // the 1-based position of the chosen entry WITHIN its slice, or the linear
    // (column-major) position over the whole array when `everyElement` is set.
    // It is left untouched for sum and mean, which have no second output.
    enum class Reduction { Sum, Mean, Max, Min };
    static ICoreMatrix reduce(const Reduction& which, const ICoreMatrix& a, int dim,
                              bool everyElement, bool omitNaN,
                              ICoreMatrix* indices = nullptr, std::string* whyNot = nullptr);

    // median(A), median(A, dim), median(..., "omitnan"). A NaN anywhere in a
    // slice makes that slice's answer NaN unless omitNaN is set, and a slice
    // that is ALL NaN answers NaN either way.
    static ICoreMatrix median(const ICoreMatrix& a, int dim, bool omitNaN,
                              std::string* whyNot = nullptr);

    // var(A), var(A, w), var(A, w, dim) -- and std, which is its square root
    // exactly as MATLAB computes it. `weights` is null for MATLAB's w = 0
    // (normalize by N-1), a 1x1 holding 0 or 1 for the flag, or a vector as
    // long as the reduced dimension for the weighted variance (normalized by
    // sum(w)). A one-element slice answers 0, not NaN, which is why var(5) is
    // 0 in MATLAB and here.
    static ICoreMatrix variance(const ICoreMatrix& a, const ICoreMatrix* weights,
                                int dim, std::string* whyNot = nullptr);
    static ICoreMatrix standardDeviation(const ICoreMatrix& a, const ICoreMatrix* weights,
                                         int dim, std::string* whyNot = nullptr);

    // cov(A): a matrix is observations x variables and answers the variable
    // covariance matrix; a VECTOR is one variable and answers its variance.
    static ICoreMatrix covariance(const ICoreMatrix& a, std::string* whyNot = nullptr);

    // cov(A, w) and cov(x, y) are the same call in MATLAB, told apart by the
    // second argument: a scalar 0 or 1 is the normalization flag, anything
    // else is a second variable, flattened column-major and paired with the
    // first into the 2x2 covariance matrix.
    static ICoreMatrix covariance(const ICoreMatrix& a, const ICoreMatrix& second,
                                  std::string* whyNot = nullptr);

    // mode(A), mode(A, dim), and the COUNT that [M, F] = mode(A) returns
    // beside it (the core-MATLAB board, C6.2). Ties go to the SMALLEST value,
    // as in MATLAB, and a NaN is not a candidate however often it repeats --
    // mode([1, NaN, NaN, 2]) is 1. `counts` may be null when the caller wants
    // only the value.
    static ICoreMatrix mode(const ICoreMatrix& a, int dim, ICoreMatrix* counts,
                            std::string* whyNot = nullptr);

    // bounds(A) -> [lo, hi] (C5.36): the column-wise minimum and maximum in
    // one pass, or the whole array's when `everyElement` is set ("all"). NaN is
    // omitted the way min/max omit it, and a slice that is all NaN answers NaN.
    static bool boundsOf(const ICoreMatrix& a, int dim, bool everyElement,
                         ICoreMatrix& low, ICoreMatrix& high, std::string* whyNot = nullptr);

    // rescale(A), rescale(A, l, u): the whole array is mapped onto [l, u] from
    // its own min and max. Two MATLAB rules that are not obvious: a constant
    // array answers l everywhere (not NaN), and an array holding an infinity
    // answers NaN everywhere.
    static ICoreMatrix rescale(const ICoreMatrix& a, const double& lo, const double& hi,
                               std::string* whyNot = nullptr);

    // MATLAB's normalize (the core-MATLAB board, C6.8), which is NOT the
    // console's older normalize() -- that one was a unit vector and now
    // answers to `unit` (C0.1, decision A).
    //
    //   ZScore   (x - mean) / std, MATLAB's default and the N-1 std
    //   Center   x - mean
    //   Scale    x / std
    //   Range    onto [lo, hi] from the slice's own min and max
    //   Norm     x / norm(slice, p)
    //
    // `parameter` carries the method's own argument -- the p of Norm (default
    // 2, and Inf is the largest magnitude) or the two-entry [lo hi] of Range
    // (default [0 1]) -- and is null for the three that take none.
    //
    // **Every statistic here OMITS NaN and every NaN stays where it was**,
    // which is neither what mean() nor what max() does and is measured against
    // R2026a 2026-09-02: normalize([1 NaN 3]) is [-0.7071 NaN 0.7071], the
    // z-score of 1 and 3 alone. A CONSTANT slice answers NaN for ZScore and
    // Scale (a real 0/0) but `lo` everywhere for Range, the same rule
    // rescale() already follows.
    enum class Normalization { ZScore, Center, Scale, Range, Norm };
    static ICoreMatrix normalize(const Normalization& how, const ICoreMatrix& a, int dim,
                                 const ICoreMatrix* parameter, std::string* whyNot = nullptr);

    // The mov* family. The window is described by how far it reaches BACK and
    // FORWARD from each element; `window()` derives that pair from MATLAB's own
    // argument, which is either a length k or an explicit [kb kf]. Endpoints
    // shrink (MATLAB's default 'shrink' rule), so the first and last answers
    // are computed over a shorter window rather than dropped.
    enum class Moving { Mean, Sum, Median, Max, Min, Std, Var };
    static ICoreMatrix moving(const Moving& which, const ICoreMatrix& a,
                              const long long& back, const long long& forward,
                              int dim, bool normalizeByN,
                              std::string* whyNot = nullptr);

    // k odd is centred; k even reaches one further back than forward
    // (kb = floor(k/2), kf = ceil(k/2) - 1, which is also what MATLAB does with
    // a fractional k); [kb kf] says it outright.
    static bool window(const ICoreMatrix& k, long long& back, long long& forward,
                       const std::string& who, std::string* whyNot = nullptr);

};
};

ICoreText.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreText.h

ICoreText#

ICoreText.h:37 · class · 20 declaration(s)

Core MATLAB's text surface (the core-MATLAB board, C11.3-C11.12): the conversions between numbers and text, and the operations on a char array.

class ICoreText {
public:

    // ---- sprintf (C11.3) ---------------------------------------------------

    // MATLAB's sprintf. The arguments arrive FLATTENED into one stream, which
    // is MATLAB's own model: every argument contributes its elements in
    // column-major order and `isChar` says, per element, whether it came from
    // a char array. The two vectors are parallel and must be the same length.
    // `anyArguments` distinguishes "sprintf(fmt)" from "sprintf(fmt, [])" --
    // see below, they differ.
    //
    // The conversions are `%d %i %u %o %x %X %f %e %E %g %G %s %c %%`, with the
    // `-+ 0#` flags, a width, a precision, `*` taking either from the stream,
    // and C length modifiers (`l`, `h`, `ll`) accepted and ignored. Escapes in
    // the FORMAT are sprintf's own, not the lexer's: `\n \t \r \\ \a \b \f \v`
    // and `%%`.
    //
    // THREE rules about running out of arguments, all three measured:
    //   1. no arguments at all -- output stops at the first conversion, so
    //      sprintf("a%db") is "a";
    //   2. arguments given but the stream is EMPTY (sprintf("a%db", [])) --
    //      the format is emitted once and each conversion contributes nothing,
    //      so that is "ab", NOT "a";
    //   3. the stream runs dry part-way -- the format CYCLES until it does,
    //      then stops at the unsatisfied conversion keeping the literal text
    //      before it: sprintf("%d-%d,", [1 2 3]) is "1-2,3-".
    //
    // And one about the wrong type, which is MATLAB's silent switch rather
    // than an error: an INTEGER conversion handed a non-integral value falls
    // back to `%e` with the same flags, so sprintf("%d", 1.5) is
    // "1.500000e+00". `%s` handed an integral number prints its character
    // (sprintf("%s", 65) is "A") and a non-integral one falls back the same
    // way; `%s` handed characters consumes the whole run of them.
    static std::string format(const std::string& fmt,
                              const std::vector<double>& values,
                              const std::vector<bool>& isChar,
                              bool anyArguments,
                              std::string* whyNot = nullptr);

    // ---- number to text (C11.4, C11.6, C11.12) -----------------------------

    // num2str(x) and string(x) -- the same text for a numeric argument.
    //
    // MATLAB picks the format from the DATA, and the arithmetic is exact
    // rather than approximate, because the padding between the entries of a
    // vector is a visible part of the answer:
    //   - every finite entry integral and max|x| below flintmax: "%<w>d" with
    //     w = (digits in the largest magnitude) + 2;
    //   - otherwise "%<w>.<p>g" with p = floor(log10(max|x|)) + 5 clamped to
    //     [5, 16] and w = p + 7;
    //   - either way, w gains 2 more when any entry is Inf or NaN, and the
    //     finished row is trimmed at both ends.
    // So num2str(pi) is "3.1416", num2str(123.456) is "123.456" (the width
    // follows the magnitude, it is not a fixed 5 significant digits), and
    // num2str([1 2.5]) is "1" then NINE spaces then "2.5".
    //
    // A char argument is answered unchanged, as MATLAB does. A matrix with
    // more than one ROW is refused: MATLAB answers a char MATRIX there and
    // this console has no such value (C0.3).
    static std::string numberToText(const ICoreMatrix& a, std::string* whyNot = nullptr);

    // num2str(x, n) -- n significant digits, "%<n+7>.<n>g" per entry, same
    // trim. num2str(pi, 8) is "3.1415927".
    static std::string numberToText(const ICoreMatrix& a, int significantDigits,
                                    std::string* whyNot = nullptr);

    // int2str(x) -- every entry rounded to the nearest integer, HALF AWAY FROM
    // ZERO (int2str(2.5) is 3 and int2str(-2.5) is -3), then num2str's integer
    // path. Inf and NaN pass through as words.
    static std::string integerToText(const ICoreMatrix& a, std::string* whyNot = nullptr);

    // mat2str(A) and mat2str(A, n) -- the form that reads BACK as an
    // expression: "[1 2;3 4]", a scalar bare, an empty "[]". Default precision
    // is 15 significant digits, which is what makes mat2str(pi) round-trip
    // where num2str(pi) does not. A LOGICAL answers the words -- "true",
    // "[true false]" -- because that is what reads back as a logical (C9.4).
    static std::string matrixToText(const ICoreMatrix& a, int significantDigits);

    // ---- text to number (C11.5) --------------------------------------------

    // str2double(s). NaN for anything that is not ONE number, which is
    // MATLAB's answer for "abc", for "" and for "1 2" alike. Surrounding
    // whitespace is allowed, embedded commas are DROPPED (str2double("1,234.5")
    // is 1234.5 and str2double("3,14") is 314, not 3.14), "Inf"/"NaN" are
    // taken in any case with an optional sign, and a "0x" prefix is read as
    // hexadecimal. There is no binary form: str2double("0b101") is NaN.
    static double textToNumber(const std::string& s);

    // ---- char array operations (C11.7-C11.12) ------------------------------

    // strcat(...). MATLAB REMOVES THE TRAILING WHITESPACE of every char
    // argument and keeps the leading whitespace, so strcat("a ", "b") is "ab"
    // while strcat("  a", "b") is "  ab". `[s1 s2]` does not do this -- that
    // is plain concatenation and it is the reason both spellings exist.
    static std::string concatenateTrimmed(const std::vector<std::string>& parts);

    // strcmp / strcmpi / strncmp / strncmpi (C11.8). `n` compares the first n
    // characters only and is false when EITHER side is shorter than n; n = 0
    // is true.
    enum class Case { Sensitive, Insensitive };
    static bool same(const std::string& a, const std::string& b, const Case& how);
    static bool samePrefix(const std::string& a, const std::string& b, size_t n,
                           const Case& how);

    // upper / lower / strtrim / deblank / blanks (C11.9). strtrim takes
    // whitespace off BOTH ends, deblank only off the end -- that is the whole
    // difference between them.
    static std::string upperCase(const std::string& s);
    static std::string lowerCase(const std::string& s);
    static std::string trimmed(const std::string& s);
    static std::string trimmedEnd(const std::string& s);

    // strfind(s, pattern) -- every 1-based start position, OVERLAPPING:
    // strfind("aaa", "aa") is [1 2], not [1]. An empty pattern matches
    // nothing.
    static std::vector<double> findAll(const std::string& s, const std::string& pattern);

    // strrep and replace (C11.10) -- and they DISAGREE, which is not a bug in
    // either. strrep replaces at every position strfind reports, so the
    // overlapping matches of strrep("aaaa", "aa", "b") each fire and the
    // answer is "bbb"; replace scans left to right and skips past what it
    // matched, so replace("aaaa", "aa", "b") is "bb". Both were measured.
    static std::string replaceOverlapping(const std::string& s, const std::string& pattern,
                                          const std::string& with);
    static std::string replaceScanning(const std::string& s, const std::string& pattern,
                                       const std::string& with);

    // contains / startsWith / endsWith. An empty pattern is true for all three.
    static bool contains(const std::string& s, const std::string& pattern);
    static bool startsWith(const std::string& s, const std::string& pattern);
    static bool endsWith(const std::string& s, const std::string& pattern);

    // extractBefore / extractAfter, in both MATLAB spellings: before or after
    // the FIRST occurrence of a pattern, or before or after a 1-based
    // POSITION. A pattern that is not there answers "" for both, and so does a
    // position at the far end.
    static std::string before(const std::string& s, const std::string& pattern);
    static std::string after(const std::string& s, const std::string& pattern);
    static std::string beforePosition(const std::string& s, size_t position);
    static std::string afterPosition(const std::string& s, size_t position);

    // ---- regular expressions (C11.13) --------------------------------------
    //
    // The flavour is ECMAScript (std::regex), not MATLAB's own. The common
    // subset -- character classes, quantifiers, anchors, groups, alternation,
    // `\d \w \s`, `$1` backreferences in a replacement -- means the same thing
    // in both, and the parity corpus only uses that subset. MATLAB-only
    // constructs (named tokens `(?<name>...)`, lookbehind, `\<`) are a stated
    // divergence rather than a silent one: a pattern this engine cannot read
    // is REFUSED with the engine's own reason, never quietly matched
    // differently.
    //
    // `$0` is translated to ECMAScript's `$&` on the way in, because MATLAB
    // spells the whole match `$0` and ECMAScript does not have it at all --
    // measured: regexprep("ab", "b", "$0$0") is "abb".
    //
    // matchPositions  regexp(s, pat) / regexp(s, pat, "start") / "end" -- the
    //                 1-based positions of every match, as a row
    // firstMatch      regexp(s, pat, "once", "match") -- the matched TEXT, or
    //                 "" when there is none (`found` says which)
    // replaceAll      regexprep(s, pat, rep)
    static std::vector<double> matchPositions(const std::string& s, const std::string& pattern,
                                              bool ignoreCase, bool wantEnd,
                                              std::string* whyNot = nullptr);
    static std::string firstMatch(const std::string& s, const std::string& pattern,
                                  bool ignoreCase, bool* found, std::string* whyNot = nullptr);
    static std::string replaceAll(const std::string& s, const std::string& pattern,
                                  const std::string& with, bool ignoreCase,
                                  std::string* whyNot = nullptr);

    // char(A) (C11.12) -- code points to characters. The conversion TRUNCATES
    // toward zero rather than rounding: char(65.9) is "A", not "B".
    static std::string fromCodePoints(const ICoreMatrix& a, std::string* whyNot = nullptr);

    // double(s) -- the code points of s as a 1 x n ROW, MATLAB's shape for a
    // char array.
    static ICoreMatrix toCodePoints(const std::string& s);
};
};

ICoreTimeSeries.h#

src/ICoreBlocks/ICoreMath/Foundation/ICoreTimeSeries.h

ICoreTimeSeries#

ICoreTimeSeries.h:24 · class · pImpl · 20 declaration(s)

A sampled signal: N time stamps and an N x M block of values, one column per channel.

class ICoreTimeSeries {
public:
    // NOT `= default` any more: the residue at the bottom of this class is a
    // unique_ptr, and a defaulted default constructor would leave it NULL --
    // which compiles, links, and dereferences null on first use.
    ICoreTimeSeries();

    // The series is stored in the variables space and handed around by value,
    // so it stays copyable; the unique_ptr residue deletes the implicit copy
    // operations, so all four are written out in the .cpp.
    ICoreTimeSeries(const ICoreTimeSeries& other);
    ICoreTimeSeries& operator=(const ICoreTimeSeries& other);
    ICoreTimeSeries(ICoreTimeSeries&& other) noexcept;
    ICoreTimeSeries& operator=(ICoreTimeSeries&& other) noexcept;
    ~ICoreTimeSeries();

    // Single-channel construction from parallel vectors.
    ICoreTimeSeries(std::vector<double> times, std::vector<double> values);

    // Multi-channel construction. valuesRowMajor holds times.size() * channels
    // entries, sample-major. Produces an invalid series (isValid() == false)
    // rather than throwing if the counts disagree.
    ICoreTimeSeries(std::vector<double> times, std::vector<double> valuesRowMajor, size_t channels);

    // The console constructor: an N-element time vector (either orientation) and
    // an N x M value matrix. A 1 x N value row vector is accepted as N samples of
    // one channel, since that is how a time-shaped vector is usually typed.
    // Returns false with `error` set on any size mismatch, never a partial series.
    static bool fromMatrices(const ICoreMatrix& time, const ICoreMatrix& values,
                             ICoreTimeSeries& out, std::string& error);

    void clear();
    void append(const double& time, const double& value);              // single channel
    void appendSample(const double& time, const std::vector<double>& channelValues);
    void reserve(const size_t& expectedSamples);

    // Drops the oldest samples until at most maxSamples remain. A recorder on a
    // long or infinite run would otherwise grow without bound.
    void truncateToMostRecent(const size_t& maxSamples);

    [[nodiscard]] bool isValid() const;      // one value row per time stamp
    [[nodiscard]] bool isEmpty() const;
    [[nodiscard]] size_t sampleCount() const;
    [[nodiscard]] size_t channelCount() const;

    [[nodiscard]] const std::vector<double>& getTimes() const;

    // Flat, sample-major. For a single-channel series this is simply the values
    // in order; for several channels the caller must stride by channelCount().
    [[nodiscard]] const std::vector<double>& getValuesRowMajor() const;

    // One channel's samples in order. Empty when the index is out of range.
    [[nodiscard]] std::vector<double> getChannel(const size_t& channelIndex) const;

    // Mean sample interval, or 0 when there are fewer than two samples. Only
    // meaningful for a fixed-step run -- see the class note.
    [[nodiscard]] double meanSamplingTime() const;

    [[nodiscard]] ICoreMatrix getTimesAsColumnVector() const;   // N x 1
    [[nodiscard]] ICoreMatrix getValuesAsMatrix() const;        // N x M

    // [time, ch0, ch1, ...] as an N x (1 + M) matrix -- exactly the shape
    // ICoreChart::plot() reads as "column 0 is X, every later column is a line".
    [[nodiscard]] ICoreMatrix getAsTimeValueMatrix() const;

private:
    class Impl;                    // the two-line residue; state lives here
    std::unique_ptr<Impl> impl;
};