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

API — ICoreBlocks/ICoreMath/Optimization

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

ICoreConstrainedNonlinearOptimization.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreConstrainedNonlinearOptimization.h

ICoreConstrainedNonlinearOptimization#

ICoreConstrainedNonlinearOptimization.h:58 · class · nested Program, Result · 1 declaration(s)

MATLAB's fmincon -- minimise a function HANDLE subject to linear constraints, bounds and NONLINEAR constraints (the toolbox board's T5.1).

class ICoreConstrainedNonlinearOptimization {
public:

    // f(x1..xn) -> fx. False means the call itself failed (the caller's
    // objective is a console function handle whose body can be in error, C2.8).
    using Objective = std::function<bool(const std::vector<double>& x, double& fx)>;

    // f(x1..xn) -> [fx, gradient], the two-output objective MATLAB reads
    // under 'SpecifyObjectiveGradient'. `gradient` is null when only the
    // value is wanted.
    using ObjectiveWithGradient =
        std::function<bool(const std::vector<double>& x, double& fx, std::vector<double>* gradient)>;

    // nonlcon(x) -> [c, ceq], MATLAB's TWO-output constraint handle: `c <= 0`
    // and `ceq == 0`. Either may be empty, and an empty one is written `[]`
    // there and answers an empty vector here.
    using Constraints = std::function<bool(const std::vector<double>& x, std::vector<double>& c,
                                           std::vector<double>& ceq)>;

    // The linear half of the problem, in MATLAB's own argument order. Every
    // one may be empty, which is how MATLAB spells "no constraint of this
    // kind" too -- `fmincon(fun, x0, [], [], [], [], lb, ub)`.
    struct Program {
        ICoreMatrix* inequality = nullptr;        // A
        ICoreMatrix* inequalityBound = nullptr;   // b
        ICoreMatrix* equality = nullptr;          // Aeq
        ICoreMatrix* equalityBound = nullptr;     // beq
        ICoreMatrix* lower = nullptr;             // lb
        ICoreMatrix* upper = nullptr;             // ub
    };

    // What `fmincon` answers, in MATLAB's own vocabulary. `exitflag` carries
    // MATLAB's integers unchanged: 1 first-order optimality met, 2 the step
    // fell below StepTolerance at a feasible point, 5 the merit function could
    // not be reduced further, 0 the iteration or evaluation budget was spent,
    // -2 no feasible point was found. They are an answer about the problem,
    // not an error in the call, which is why they are returned rather than
    // reported as a failure.
    struct Result {
        std::vector<double> x;                 // the minimiser, in x0's own order
        double fval = 0.0;                     // the objective there
        int exitflag = 0;
        int iterations = 0;
        int funcCount = 0;
        double constraintViolation = 0.0;      // output.constrviolation
        double stepsize = 0.0;                 // output.stepsize
        double firstOrderOptimality = 0.0;     // output.firstorderopt
        std::vector<double> gradient;          // GRAD, n entries
        // LAMBDA, in MATLAB's own blocks. Each is empty when the problem
        // carries no constraint of that kind.
        std::vector<double> lambdaInequalityLinear;   // lambda.ineqlin
        std::vector<double> lambdaEqualityLinear;     // lambda.eqlin
        std::vector<double> lambdaLower;              // lambda.lower
        std::vector<double> lambdaUpper;              // lambda.upper
        std::vector<double> lambdaInequalityNonlinear;// lambda.ineqnonlin
        std::vector<double> lambdaEqualityNonlinear;  // lambda.eqnonlin
    };

    // fmincon(fun, x0, A, b, Aeq, beq, lb, ub, nonlcon, options) with the
    // defaults MATLAB's own option set carries: OptimalityTolerance 1e-6,
    // ConstraintTolerance 1e-6, StepTolerance 1e-10 (the 'sqp' default, which
    // is NOT interior-point's 1e-10 by accident -- both are 1e-10 and
    // 'active-set' alone uses 1e-6), MaxIterations 400,
    // MaxFunctionEvaluations 100*n.
    //
    // `constraints` is null for a problem with no `nonlcon`. `options` is a
    // resolved `optimoptions('fmincon', ...)` set (T5.11); null, or one whose
    // fields are all unset, runs those defaults. `analytic` is the
    // 'SpecifyObjectiveGradient' objective, used INSTEAD of the
    // finite-difference gradient at every point -- which changes `funcCount`
    // on both sides, as it does for `fminunc` (T5.2).
    //
    // ⚠ `whyNot` and the two option parameters are LAST for the reason
    // `ICoreNonlinearOptimization` records: every existing caller passes
    // `whyNot` positionally, so a parameter inserted before it would change
    // the meaning of a call silently rather than failing to compile.
    static bool minimum(const Objective& f, const std::vector<double>& x0,
                        const Program& program, const Constraints* constraints, Result& out,
                        std::string* whyNot = nullptr,
                        const ICoreOptimizationOptions::Values* options = nullptr,
                        const ObjectiveWithGradient* analytic = nullptr);
};
};

ICoreConstrainedOptimization.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreConstrainedOptimization.h

ICoreConstrainedOptimization#

ICoreConstrainedOptimization.h:32 · class · nested Multipliers, Statistics · 0 declaration(s)

The constrained solvers of the toolbox board's T5.6 to T5.9 -- MATLAB's linprog, quadprog, lsqlin and intlinprog -- over one feasibility engine.

class ICoreConstrainedOptimization {
public:
    // What a solver decided, in MATLAB's own vocabulary: `exitflag` 1 is a
    // solution, -2 infeasible, -3 unbounded. They are returned rather than
    // reported as failures because MATLAB returns them too -- an infeasible
    // LP is an answer about the problem, not an error in the call.
    enum class Outcome { Solved, Infeasible, Unbounded, Failed };

    // The Lagrange multipliers at a quadratic programme's solution, in
    // MATLAB's own blocks (`lambda.ineqlin`, `lambda.eqlin`, `lambda.lower`,
    // `lambda.upper`). Every one is non-negative except `equality`, whose
    // sign is the constraint's own.
    //
    // ⚠ THEY ARE RECOVERED AFTER THE FACT AND NOT READ OUT OF THE ITERATION.
    // The active set carries multipliers for its WORKING SET only, and it
    // leaves the loop by three different doors; recomputing them once at the
    // answer -- the least-squares solution of `C' z = -(H x + f)` over the
    // active constraints -- is one place instead of three, and it is the same
    // number. A constraint that is not active gets zero, which is what
    // complementarity says it must be.
    struct Multipliers {
        std::vector<double> inequality;   // one per row of A
        std::vector<double> equality;     // one per row of Aeq
        std::vector<double> lower;        // one per variable
        std::vector<double> upper;        // one per variable
    };

    // ==== mb337 T5.14 begin ====
    // What MATLAB's `output` record reports for the four programmes here (the
    // toolbox board's T5.14). Every field was already being computed and
    // thrown away: the simplex counts its own pivots, the active set its own
    // iterations, and the branch and bound already knows its node and
    // incumbent counts and its gaps -- they simply had nowhere to go.
    //
    // ⚠ `iterations` IS THIS CONSOLE'S OWN COUNT AND IS NOT MATLAB'S, and the
    // difference is the algorithm rather than a defect: a two-phase simplex
    // does not take the same number of steps as MATLAB's dual-simplex-highs,
    // and a primal active set does not take an interior point's. The number is
    // honest about what ran HERE, which is what `output` means; it compares as
    // a PROPERTY and never digit-for-digit.
    //
    // `constraintViolation` and `firstOrderOptimality` are the two fields that
    // do NOT depend on the path taken -- both are read off the ANSWER -- so
    // they are the digit-comparable half of the record.
    struct Statistics {
        int    iterations = 0;             // output.iterations
        double constraintViolation = 0.0;  // output.constrviolation, at the answer
        double firstOrderOptimality = 0.0; // output.firstorderopt, at the answer
        int    nodes = 0;                  // intlinprog: output.numnodes
        int    feasiblePoints = 0;         // intlinprog: output.numfeaspoints
        double absoluteGap = 0.0;          // intlinprog: output.absolutegap
        double relativeGap = 0.0;          // intlinprog: output.relativegap
    };
    // ==== mb337 T5.14 end ====

    // min f'x  subject to  A x <= b,  Aeq x = beq,  lb <= x <= ub.
    //
    // Any of A/Aeq/lb/ub may be empty. A bound of +/-infinity is absent, which
    // is how MATLAB spells "no bound" too. The default when `lb` is empty is
    // FREE variables -- linprog does not assume x >= 0, and a solver that did
    // would answer a different (and often smaller) optimum without saying so.
    //
    // Two-phase simplex over the standard form, with Bland's rule on the
    // second phase so a degenerate vertex cannot cycle.
    static Outcome linearProgram(const ICoreMatrix& objective, const ICoreMatrix& inequality,
                                 const ICoreMatrix& inequalityBound, const ICoreMatrix& equality,
                                 const ICoreMatrix& equalityBound, const ICoreMatrix& lower,
                                 const ICoreMatrix& upper, ICoreMatrix& solution, double& value,
                                 std::string* whyNot = nullptr,
                                 Multipliers* multipliers = nullptr,
                                 Statistics* statistics = nullptr);

    // min 0.5 x'H x + f'x  subject to the same constraint set.
    //
    // H is symmetrised as `(H + H')/2`, which is what MATLAB does with a
    // warning, and a non-convex H (any negative eigenvalue) is refused with
    // MATLAB's own message rather than solved to a stationary point that is
    // not a minimum.
    //
    // Primal active-set: start from a feasible vertex found by the simplex
    // above, solve the equality-constrained problem on the working set through
    // its KKT system, add the first constraint the step would cross, and drop
    // a constraint whose multiplier has gone negative. It terminates on a
    // finite set of working sets, and where it stops IS the minimiser.
    static Outcome quadraticProgram(const ICoreMatrix& quadratic, const ICoreMatrix& objective,
                                    const ICoreMatrix& inequality,
                                    const ICoreMatrix& inequalityBound,
                                    const ICoreMatrix& equality, const ICoreMatrix& equalityBound,
                                    const ICoreMatrix& lower, const ICoreMatrix& upper,
                                    ICoreMatrix& solution, double& value,
                                    std::string* whyNot = nullptr,
                                    Multipliers* multipliers = nullptr,
                                    Statistics* statistics = nullptr);

    // min 0.5 * ||C x - d||^2 over the same constraint set -- MATLAB's
    // `lsqlin` (T5.6). It is `quadraticProgram` above on H = C'C and
    // f = -C'd, which is exactly the reduction `lsqlin.m` makes for its
    // 'active-set' algorithm, so nothing new is solved here: the row is the
    // reduction and the two extra outputs.
    //
    // `residualNorm` is MATLAB's `resnorm` and it is NOT the halved objective:
    // resnorm = ||C x - d||^2 while the quadratic being minimised is half of
    // it. `residual` is `C x - d`, the signed vector, not its norm.
    //
    // ⚠ WITH NO CONSTRAINTS THIS DOES NOT GO THROUGH THE QP AT ALL. The
    // unconstrained minimiser is the ordinary least-squares solution and is
    // taken from a rank-revealing QR of C directly, because forming C'C
    // SQUARES the condition number -- on a problem MATLAB answers to full
    // precision, a normal-equations route would answer half the digits and
    // the parity band would have to be widened to hide it. Once a constraint
    // is present there is no such route and C'C is formed, which is what
    // MATLAB does there too.
    static Outcome constrainedLeastSquares(const ICoreMatrix& design, const ICoreMatrix& target,
                                           const ICoreMatrix& inequality,
                                           const ICoreMatrix& inequalityBound,
                                           const ICoreMatrix& equality,
                                           const ICoreMatrix& equalityBound,
                                           const ICoreMatrix& lower, const ICoreMatrix& upper,
                                           ICoreMatrix& solution, double& residualNorm,
                                           ICoreMatrix& residual, std::string* whyNot = nullptr,
                                           Multipliers* multipliers = nullptr,
                                           Statistics* statistics = nullptr);

    // min f'x over the same constraint set, with the variables named by
    // `integerIndices` (MATLAB's `intcon`, ONE-BASED) required to be integers
    // -- MATLAB's `intlinprog` (T5.9).
    //
    // Branch and bound over `linearProgram` above: solve the relaxation, pick
    // a variable that came back fractional, and split the problem in two by
    // TIGHTENING THAT VARIABLE'S BOUNDS -- x_i <= floor(v) on one side and
    // x_i >= ceil(v) on the other. A node whose relaxation is already worse
    // than the best integer point found so far cannot contain a better one
    // and is dropped unopened; that bound is the whole method and it is why
    // the search terminates on problems whose tree is astronomically large.
    //
    // The optimum VALUE is again the problem's and not the method's, which is
    // what lets this be compared to MATLAB's own branch and bound (the same
    // argument the class comment makes for the LP). x itself is unique only
    // when the optimal integer point is.
    static Outcome integerLinearProgram(const ICoreMatrix& objective,
                                        const ICoreMatrix& integerIndices,
                                        const ICoreMatrix& inequality,
                                        const ICoreMatrix& inequalityBound,
                                        const ICoreMatrix& equality,
                                        const ICoreMatrix& equalityBound, const ICoreMatrix& lower,
                                        const ICoreMatrix& upper, ICoreMatrix& solution,
                                        double& value, std::string* whyNot = nullptr,
                                        Statistics* statistics = nullptr);
};
};

ICoreContinuousTimeIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreContinuousTimeIdentification.h

ICoreContinuousTimeIdentification#

ICoreContinuousTimeIdentification.h:56 · class · nested Model · 0 declaration(s)

MATLAB's tfest(data, np, nz[, ioDelay]) with no 'Ts' -- a CONTINUOUS-time transfer function fitted to a DISCRETE-time record (toolbox row T6.2).

class ICoreContinuousTimeIdentification {
public:
    struct Model {
        // Descending powers of s. `num` is nz + 1 long, `den` np + 1 and monic
        // -- the shapes `m.Numerator` and `m.Denominator` read on the idtf.
        std::vector<double> num;
        std::vector<double> den;
        // The io delay as it was asked for, in seconds, echoed back the way
        // `m.IODelay` does. The samples are shifted by it before anything else
        // happens and it is never estimated (T6.2: MATLAB disables iterative
        // delay estimation for a continuous model over discrete data).
        double ioDelay = 0.0;

        // V at the answer -- e'e/N, `Report.Fit.LossFcn`.
        double loss = 0.0;
        size_t iterations = 0;
        size_t functionCount = 0;
        std::string whyStop;
    };

    // `input` and `output` are the record's samples, `sampleTime` its own Ts
    // (> 0). `np` poles, `nz` zeros, `np >= nz` -- MATLAB refuses `nz > np` for
    // a continuous estimate in as many words ("the number of poles must be
    // greater than or equal to the number of zeros") and so does this.
    //
    // `ioDelay` is in SECONDS and must be a non-negative whole number of
    // samples; a fractional one is refused with the reason. Measured on
    // R2026a: an integer io delay answers EXACTLY the estimate over the record
    // whose input has been shifted by that many samples with zeros prepended,
    // bit for bit over 36 order sets, which is what this does. A fractional one
    // does not reduce to a shift -- it becomes a fractional input delay inside
    // the sampled simulation -- and is the one input format of this row that is
    // refused rather than answered.
    static bool estimate(const ICoreMatrix& input, const ICoreMatrix& output,
                         const double& sampleTime, const size_t& np, const size_t& nz,
                         const double& ioDelay, Model& out, std::string* whyNot = nullptr);

    // The initialisation on its own -- `inival_time.m`'s `'iv'` mode. Exposed
    // for the same reason the polynomial one is: an initialisation that is one
    // filter out lands the search in a different minimum, and from the outside
    // that failure looks exactly like a bad search.
    static bool initialize(const ICoreMatrix& input, const ICoreMatrix& output,
                           const double& sampleTime, const size_t& np, const size_t& nz,
                           const double& ioDelay, Model& out, std::string* whyNot = nullptr);
};
};

ICoreExcitationSignals.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreExcitationSignals.h

ICoreExcitationSignals#

ICoreExcitationSignals.h:47 · class · 1 declaration(s)

MATLAB's idinput (toolbox row T6.10) -- the four signals the System Identification Toolbox generates to EXCITE a plant before anything is identified from it.

class ICoreExcitationSignals {
public:
    // MATLAB's four type words. `rs` is its own alias for `rgs`.
    enum class Excitation { Prbs, RandomBinary, RandomGaussian, Sine };

    static bool excitationOf(const std::string& word, Excitation& type);

    // `[u, freqs] = idinput([P nu M], type, band, levels, sinedata)`.
    //
    // `period` is P, `inputs` is nu, `periods` is M (the whole signal repeated).
    // `band` is the pair `[low high]` in units of the Nyquist frequency,
    // `levels` the pair `[min max]`, and `sineData` MATLAB's
    // `[sinusoids trials odd]` (defaulting to 10, 10 and 1).
    //
    // `frequencies` comes back empty for every type but `sine`, which is what
    // MATLAB's second output does.
    static bool generate(const Excitation& type, const size_t& period, const size_t& inputs,
                         const size_t& periods, const ICoreMatrix& band,
                         const ICoreMatrix& levels, const ICoreMatrix& sineData,
                         ICoreMatrix& signal, ICoreMatrix& frequencies,
                         std::string* whyNot = nullptr);

};
};

ICoreIdMinimizer.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreIdMinimizer.h

ICoreIdMinimizer#

ICoreIdMinimizer.h:29 · class · nested Result · 0 declaration(s)

idminimizer.m with SearchMethod 'auto' -- the search EVERY iterative estimator of the System Identification Toolbox stops in, and therefore the half of those estimators' answers that is not the c...

class ICoreIdMinimizer {
public:
    // What the search did, for the caller's own record and for a refusal that
    // has to say why nothing better came back. `whyStop` is MATLAB's sentence.
    struct Result {
        double loss = 0.0;
        size_t iterations = 0;
        size_t functionCount = 0;
        std::string whyStop;
    };

    // The criterion at a point. `loss` is MATLAB's `resnorm` -- e'e/N for the
    // SISO det-criterion. When `jacobian` and `residual` are both non-null the
    // criterion also answers the QR factors the step is taken from: `jacobian`
    // is MATLAB's R and `residual` its `-Re`, i.e. `getErrorAndJacobian`'s
    // final `e = -e` already applied. Answering false means the point is
    // infeasible (an unstable model, a divergent filter) and the line search
    // reads that as an infinite loss and bisects back.
    using Criterion = std::function<bool(const std::vector<double>& x, double& loss,
                                         ICoreMatrix* jacobian, ICoreMatrix* residual)>;

    // `samples` is `sum(ut.DataSize)`, the N the expected-improvement test
    // divides by.
    //
    // ⚠ THE NORMALISATION FACTOR `F` IS THE LOSS FOR BOTH CALLERS, and it gets
    // there by two different routes -- which is why it is not a parameter.
    // `idminimizer.m` sets `F = resnorm` when `isPoly || isTraceCrit`.
    // `isPoly` is what makes it so for an idpoly (T6.4). For the structured
    // idss of T6.2 `isPoly` is false, but `isTraceCrit` is
    // `hasOutputWeight(option) && isnumeric(option.OutputWeight)` and
    // `tfestOptions`' OutputWeight is `[]`, which IS numeric -- so it is true
    // there too. Measured: with F = 1 the continuous `tfest` stops one to
    // three iterations early on five of nine order sets; with F = resnorm
    // every iteration and function count matches R2026a exactly.
    static bool minimize(const Criterion& criterion, const size_t& samples,
                         std::vector<double>& x, Result& out, std::string* whyNot = nullptr);
};
};

ICoreMinimaxOptimization.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreMinimaxOptimization.h

ICoreMinimaxOptimization#

ICoreMinimaxOptimization.h:53 · class · nested Result · 1 declaration(s)

The two EPIGRAPH solvers of the Optimization Toolbox -- MATLAB's fminimax and fgoalattain (toolbox board row T5.10).

class ICoreMinimaxOptimization {
public:
    // F(x) -> the vector of objectives. False means the call itself failed --
    // the caller's F is a console handle whose body can be in error (C2.8).
    using VectorObjective =
        std::function<bool(const std::vector<double>& x, std::vector<double>& F)>;

    // What either solver answers. `attainment` is `fminimax`'s `maxfval` (the
    // largest objective at the solution) and `fgoalattain`'s `attainfactor`
    // (how far the goals were over- or under-attained); they are the same
    // number in the same place -- the epigraph variable -- which is why one
    // struct serves both.
    struct Result {
        std::vector<double> x;
        std::vector<double> values;      // F at the solution, in F's own order
        double attainment = 0.0;
        int    iterations = 0;
        int    functionCalls = 0;
    };

    // MATLAB's exitflag vocabulary is NOT reproduced, and the reason is that
    // its integers say which stopping TEST fired inside its SQP (1 first-order
    // optimality, 4 the search direction was small, 5 the directional
    // derivative was) -- a fact about MATLAB's iteration and not about the
    // problem. These four are about the PROBLEM, the way `linprog`'s -2 and -3
    // are, and the row refuses the exitflag output rather than answering a
    // number under MATLAB's name that means something else. `Failed` also
    // carries the nonlinear refusal, which is a fact about this console.
    enum class Outcome { Solved, Infeasible, Unbounded, Failed };

    // min max_i F_i(x)  subject to  A x <= b, Aeq x = beq, lb <= x <= ub.
    // Any constraint matrix may be empty. `x0` is where F is read and where
    // its Jacobian is measured; for an affine F the answer does not depend on
    // it, which is worth knowing when a result looks surprising.
    static Outcome minimax(const VectorObjective& objective, const std::vector<double>& start,
                           const ICoreMatrix& inequality, const ICoreMatrix& inequalityBound,
                           const ICoreMatrix& equality, const ICoreMatrix& equalityBound,
                           const ICoreMatrix& lower, const ICoreMatrix& upper, Result& result,
                           std::string* whyNot = nullptr);

    // min gamma  subject to  F_i(x) - w_i * gamma <= goal_i, and the same
    // constraint set.
    //
    // ⚠ A ZERO WEIGHT IS NOT "IGNORE THIS OBJECTIVE": it makes the goal a HARD
    // constraint (F_i(x) <= goal_i, with no gamma to relax it), which is what
    // MATLAB's own documentation means by a weight of zero and is the one
    // thing about this solver that a reader guesses wrong.
    static Outcome goalAttainment(const VectorObjective& objective,
                                  const std::vector<double>& start, const ICoreMatrix& goal,
                                  const ICoreMatrix& weight, const ICoreMatrix& inequality,
                                  const ICoreMatrix& inequalityBound, const ICoreMatrix& equality,
                                  const ICoreMatrix& equalityBound, const ICoreMatrix& lower,
                                  const ICoreMatrix& upper, Result& result,
                                  std::string* whyNot = nullptr);
};
};

ICoreModelPrediction.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreModelPrediction.h

ICoreModelPrediction#

ICoreModelPrediction.h:67 · class · 0 declaration(s)

What an identified model DOES once it has been fitted -- MATLAB's sim, predict, forecast and resid (toolbox row T6.9).

class ICoreModelPrediction {
public:
    // `sim(sys, u)` -- the model's forced response from a ZERO state, which is
    // `filter(B, A, u)`. The console's own `simtf` computes the same thing and
    // stays as a marked extra (X7).
    //
    // `input` is a vector written either way round; `output` comes back the
    // same length and always a COLUMN, which is MATLAB's shape (see below).
    static bool simulate(const ICoreTransferFunction& model, const ICoreMatrix& input,
                         ICoreMatrix& output, std::string* whyNot = nullptr);

    // `predict(sys, u, y, Ts, k)` -- the console's numeric form of MATLAB's
    // `predict(sys, iddata(y, u, Ts), k)` with `'InitialCondition', 'z'`.
    //
    // `horizon` is k. It is a whole number at least 1, or infinity for the
    // pure simulation MATLAB spells `Inf`; anything else is refused by name
    // rather than rounded, because a fractional horizon has no meaning and
    // silently taking its floor is the one answer that cannot be right.
    static bool predictHorizon(const ICoreTransferFunction& model, const ICoreMatrix& input,
                               const ICoreMatrix& output, const double& horizon,
                               ICoreMatrix& predicted, std::string* whyNot = nullptr);

    // `forecast(sys, past, K)` -- K samples BEYOND the record, continuing the
    // model's own recursion with the noise set to its mean of zero.
    //
    // `futureInput` may be empty, which is MATLAB's own default of a zero
    // future input; when it is not empty it holds exactly `steps` samples and
    // is refused otherwise. `forecast` answers only the K new samples, not the
    // record with them appended -- MATLAB's shape.
    static bool forecast(const ICoreTransferFunction& model, const ICoreMatrix& input,
                         const ICoreMatrix& output, const size_t& steps,
                         const ICoreMatrix& futureInput, ICoreMatrix& forecastOut,
                         std::string* whyNot = nullptr);

    // `resid(u, y, Ts, sys)` -- the one-step prediction errors `e = y - yhat`,
    // which is what MATLAB's first output is (verified digit for digit against
    // `y - predict(m, d, 1)` on this row's fixture).
    //
    // ⚠ MATLAB'S SECOND OUTPUT IS NOT AVAILABLE HERE AND IS NOT APPROXIMATED.
    // `[e, r] = resid(d, m)` answers an `r` measured at 26 x 2 x 2 on R2026a
    // -- the residual autocorrelation and the residual/input cross-correlation
    // stacked in a THIRD dimension -- and an N-D result is off this board by
    // Z10. The console answers `e` and refuses the second output by name; it
    // does not flatten the array to a shape MATLAB has no spelling for, which
    // is C0.9's rule that a row states why rather than inventing a value.
    static bool residuals(const ICoreTransferFunction& model, const ICoreMatrix& input,
                          const ICoreMatrix& output, ICoreMatrix& errors,
                          std::string* whyNot = nullptr);
};
};

ICoreModelValidation.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreModelValidation.h

ICoreModelValidation#

ICoreModelValidation.h:25 · class · 1 declaration(s)

compare and goodnessOfFit -- toolbox row T6.8, the pair every other System Identification row is scored by.

class ICoreModelValidation {
public:
    // MATLAB's three measures. `Mse` answers one number for the whole matrix
    // (`trace(e' e) / Ns`, summed over every channel and divided by the sample
    // count only); the other two answer one number per COLUMN.
    enum class FitMeasure { Nrmse, Nmse, Mse };
    static bool measureFromName(const std::string& name, FitMeasure& out);

    // `goodnessOfFit(x, xref, measure)`. `value` comes back 1 x 1 for `Mse`
    // and 1 x N for the other two, MATLAB's own shapes.
    //
    // The degenerate case is MATLAB's and is not a guard against dividing by
    // zero: when a channel's reference is CONSTANT *and* the test data equals
    // it -- both compared against `eps(||xref||_inf * Ns)` -- the ratio is
    // taken to be 0 rather than 0/0. A constant reference that the model does
    // NOT match still divides by zero and answers Inf, which is the honest
    // answer and what MATLAB gives.
    static bool goodnessOfFit(const ICoreMatrix& test, const ICoreMatrix& reference,
                              const FitMeasure& measure, ICoreMatrix& value,
                              std::string* whyNot = nullptr);

    // `[yh, fit, x0] = compare(u, y, Ts, sys)` -- the console's numeric form of
    // MATLAB's `compare(iddata(y, u, Ts), sys)`.
    //
    // ⚠ THE INITIAL STATE IS ESTIMATED, NOT ZERO, and a simulation from a zero
    // state is a different fit. MATLAB's default `InitialCondition` is
    // `'auto'`, which for a model with no noise component resolves to
    // `'estimate'`: `x0` is the least-squares minimiser of
    // `|| y - forced - Phi x0 ||` over the free-response basis
    // `Phi(t, :) = C A^t`, exactly `findstates`' own rule. Measured on this
    // row's twenty-sample fixture: 35.209 with the estimated state against
    // 34.312 from zero.
    //
    // `initialState` is in the basis of `ICoreStateSpace::fromTransferFunction`
    // when `sys` is a transfer function, so it is the one output of the three
    // that is a property of the REALIZATION rather than of the model -- `yh`
    // and `fit` are the same in every basis.
    static bool compare(const ICoreStateSpace& system, const ICoreMatrix& input,
                        const ICoreMatrix& output, ICoreMatrix& simulated, double& fitPercent,
                        ICoreMatrix& initialState, std::string* whyNot = nullptr);
};
};

ICoreNonlinearEquations.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreNonlinearEquations.h

ICoreNonlinearEquations#

ICoreNonlinearEquations.h:38 · class · nested Result · 1 declaration(s)

MATLAB's fsolve -- a system of nonlinear equations solved by the trust-region dogleg method (the toolbox board's T5.3).

class ICoreNonlinearEquations {
public:

    // F(x1..xn) -> [f1..fm]. False means the call itself failed.
    using VectorObjective =
        std::function<bool(const std::vector<double>& x, std::vector<double>& F)>;

    struct Result {
        std::vector<double> x;          // where the iteration stopped
        std::vector<double> fvec;       // F there
        std::vector<double> jacobian;   // m*n column-major, the finite-difference Jacobian there
        int exitflag = 0;               // MATLAB's integers: 1, 2, 3, -3, 0
        int iterations = 0;
        int funcCount = 0;
        double firstOrderOptimality = 0.0;   // norm(JAC'*F, inf)
    };

    // fsolve(F, x0) with MATLAB's default 'trust-region-dogleg'.
    // `options` is a resolved `optimoptions(...)` set (T5.11); null, or one
    // whose fields are all unset, runs MATLAB's own defaults, so the two are
    // the same iteration. It is the LAST parameter because `whyNot` already
    // had a default and every caller passes it positionally -- a parameter
    // inserted before it would change existing calls silently.
    static bool solve(const VectorObjective& F, const std::vector<double>& x0,
                      Result& out, std::string* whyNot = nullptr,
                      const ICoreOptimizationOptions::Values* options = nullptr);

    // The forward-difference Jacobian, column by column, by the step rule
    // above. `evaluations` is what it cost.
    static bool finiteDifferenceJacobian(const VectorObjective& F, const std::vector<double>& x,
                                         const std::vector<double>& Fx, size_t rows,
                                         std::vector<double>& jacobian, int& evaluations,
                                         std::string* whyNot = nullptr);
};
};

ICoreNonlinearLeastSquares.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreNonlinearLeastSquares.h

ICoreNonlinearLeastSquares#

ICoreNonlinearLeastSquares.h:40 · class · nested Result · 1 declaration(s)

MATLAB's lsqnonlin and lsqcurvefit -- the nonlinear least-squares half of the Optimization Toolbox (the toolbox board's T5.4 and T5.5), by the DEFAULT algorithm, which is `trust-region-reflecti...

class ICoreNonlinearLeastSquares {
public:

    // F(x1..xn) -> [f1..fm], the residual VECTOR (not its sum of squares --
    // the trap for anyone reading the row's title too quickly). False means
    // the call itself failed.
    using VectorObjective =
        std::function<bool(const std::vector<double>& x, std::vector<double>& F)>;

    struct Result {
        std::vector<double> x;          // where the iteration stopped
        std::vector<double> residual;   // F there
        double resnorm = 0.0;           // sum(residual.^2), MATLAB's own second output
        std::vector<double> jacobian;   // m*n column-major
        int exitflag = 0;               // MATLAB's integers: 1, 2, 3, 0
        int iterations = 0;
        int funcCount = 0;
        double firstOrderOptimality = 0.0;
    };

    // lsqnonlin(F, x0) with MATLAB's default trust-region-reflective and its
    // default options (TolFun 1e-6, TolX 1e-6, MaxIter 400, MaxFunEvals 100*n).
    // MATLAB requires at least as many equations as unknowns for this
    // algorithm and says so by name; so does this.
    // `options` is a resolved `optimoptions(...)` set (T5.11); null, or one
    // whose fields are all unset, runs MATLAB's own defaults, so the two are
    // the same iteration. It is the LAST parameter because `whyNot` already
    // had a default and every caller passes it positionally -- a parameter
    // inserted before it would change existing calls silently.
    static bool solve(const VectorObjective& F, const std::vector<double>& x0,
                      Result& out, std::string* whyNot = nullptr,
                      const ICoreOptimizationOptions::Values* options = nullptr);
};
};

ICoreNonlinearOptimization.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreNonlinearOptimization.h

ICoreNonlinearOptimization#

ICoreNonlinearOptimization.h:52 · class · nested Result · 1 declaration(s)

The Optimization Toolbox solvers that take a function HANDLE and search in n dimensions without constraints: MATLAB's fminunc and checkGradients (the toolbox board's T5.2 and T5.15).

class ICoreNonlinearOptimization {
public:

    // f(x1..xn) -> fx. False means the call itself failed.
    using ObjectiveN = std::function<bool(const std::vector<double>& x, double& fx)>;

    // f(x1..xn) -> [fx, gradient], the two-output objective MATLAB reads under
    // 'SpecifyObjectiveGradient' and the only shape `checkGradients` accepts.
    // `gradient` is null when only the VALUE is wanted, which is how the
    // finite-difference half asks -- a caller whose objective is expensive in
    // its second output should skip it there, and MATLAB's own
    // finitedifferences layer asks the same way. False means the call failed.
    using ObjectiveWithGradient =
        std::function<bool(const std::vector<double>& x, double& fx, std::vector<double>* gradient)>;

    // What `fminunc` answers, in MATLAB's own vocabulary. `exitflag` carries
    // MATLAB's integers unchanged -- 1 gradient below tolerance, 2 step below
    // tolerance, 5 the line search could not reduce the objective further,
    // 0 iteration or evaluation budget spent, -3 unbounded below the objective
    // limit -- because they are an answer about the problem, not an error in
    // the call.
    struct Result {
        std::vector<double> x;          // the minimiser, in x0's own order
        double fval = 0.0;              // the objective there
        std::vector<double> gradient;   // n entries, the finite-difference gradient there
        std::vector<double> hessian;    // n*n column-major; empty unless asked for
        int exitflag = 0;
        int iterations = 0;
        int funcCount = 0;
        double stepsize = 0.0;             // norm(deltaX, 2) of the last step
        double lineSearchStepLength = 0.0; // output.lssteplength
        double firstOrderOptimality = 0.0; // norm(grad, inf)
    };

    // fminunc(f, x0) with MATLAB's 'quasi-newton' algorithm and its default
    // options (TolX 1e-6, TolFun 1e-6, MaxIter 400, MaxFunEvals 100*n,
    // ObjectiveLimit -1e20). `wantHessian` is the six-output form: MATLAB
    // computes the finite-difference Hessian only when it is asked for, with
    // an eps^(1/4) step where the gradient uses sqrt(eps), and this does the
    // same -- so asking for it changes `funcCount`, on both sides.
    // `options` is an `optimoptions('fminunc', ...)` set that has already been
    // resolved (T5.11); null, or a set whose fields are all unset, runs
    // MATLAB's own defaults. `analytic` is the 'SpecifyObjectiveGradient'
    // objective -- the two-output handle MATLAB reads when that option is on;
    // it is used INSTEAD of the finite-difference gradient at every point,
    // which is why turning the option on changes `funcCount` from
    // (1 + n)*(1 + iterations) to 1 + iterations + the line search's trials
    // (measured on R2026a: 66 against 22 on this row's own quartic) and moves
    // the answer slightly -- an exact gradient is not the forward-difference
    // one.
    //
    // ⚠ BOTH ARE THE LAST PARAMETERS RATHER THAN THE NATURAL ONES. `whyNot`
    // has had a default since this function was written and every caller
    // passes it positionally, so a parameter inserted before it would change
    // the meaning of every existing call silently rather than failing to
    // compile.
    static bool unconstrainedMinimum(const ObjectiveN& f, const std::vector<double>& x0,
                                     bool wantHessian, Result& out,
                                     std::string* whyNot = nullptr,
                                     const ICoreOptimizationOptions::Values* options = nullptr,
                                     const ObjectiveWithGradient* analytic = nullptr);

    // MATLAB's forward-difference gradient, the one every solver here uses:
    // step h_i = sqrt(eps) * sign(x_i) * max(|x_i|, 1), with sign(0) = +1, and
    // g_i = (f(x + h_i e_i) - f(x)) / h_i. `evaluations` is how many calls it
    // cost, which the callers add to MATLAB's `funcCount`.
    static bool finiteDifferenceGradient(const ObjectiveN& f, const std::vector<double>& x,
                                         double fx, std::vector<double>& gradient,
                                         int& evaluations, std::string* whyNot = nullptr);

    // checkGradients(fun, x0) -- true when the gradient the objective returns
    // agrees with the finite-difference one to `tolerance` (MATLAB's default
    // 1e-6), relatively, at a point NEAR x0.
    //
    // ⚠ MATLAB perturbs x0 by `1e-3 * max(|x0|, 1) .* (1 - 2*rand(n, 1))` with
    // its own RNG reset to seed 0, so the point tested is MATLAB's Mersenne
    // Twister stream and not this one's (Z11). `maximumRelativeError` is
    // therefore this console's number, not MATLAB's, and only the VERDICT
    // crosses: a gradient that is right is right at either point, and one that
    // is wrong by more than a rounding is wrong at either point. The caller
    // says which of the two it publishes.
    static bool gradientCheck(const ObjectiveWithGradient& f, const std::vector<double>& x0,
                              double tolerance, bool& valid, double& maximumRelativeError,
                              std::string* whyNot = nullptr);
};
};

ICoreNonparametricIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreNonparametricIdentification.h

ICoreNonparametricIdentification#

ICoreNonparametricIdentification.h:33 · class · 1 declaration(s)

The NONPARAMETRIC estimators of the System Identification Toolbox -- MATLAB's covf, etfe and spa (toolbox board row T6.7).

class ICoreNonparametricIdentification {
public:
    // ---- covf(z, M) --------------------------------------------------------
    //
    // The covariance function of the COLUMNS of `data`, biased, laid out the
    // way MATLAB's own help block states it:
    //
    //     R(i + (j - 1) * nz, k + 1) = (1 / N) * sum_t z_i(t) * z_j(t + k)
    //
    // so the result is nz^2 rows by M columns, k running 0 .. M-1, and every
    // sum is divided by the FULL record length N rather than by the number of
    // products in it (the biased estimate -- `xcorr(..., 'biased')`, which is
    // what `@iddata/covf.m`'s own comment says this function is).
    //
    // ⚠ MATLAB REACHES THOSE NUMBERS THROUGH AN FFT and this reaches them by
    // summing, for records small enough that its own `covf.m` would sum too
    // (its `maxsize` trade-off). The two agree to 2.2e-16 on this row's
    // record, measured before this file was written -- the sums are short and
    // the FFT is exact for them. A row that needed the FFT path's exact
    // rounding would have to say so; this one does not.
    static bool covarianceFunction(const ICoreMatrix& data, const size_t& maxLag,
                                   ICoreMatrix& out, std::string* whyNot = nullptr);

    // ---- etfe(u, y, Ts, M, N) ---------------------------------------------
    //
    // The empirical transfer function estimate: the ratio of the output's
    // Fourier transform to the input's, at `points` frequencies evenly spaced
    // over (0, pi/Ts].
    //
    // `smoothing` is MATLAB's M -- 0 here means "not given", which is its `[]`
    // and means no smoothing at all. A given M applies a Hamming lag window of
    // about pi/M resolution, and MATLAB HALVES IT FIRST (`M = max(1, M/2)`,
    // "this is to make better agreement with SPA" -- its comment), so
    // `etfe(data, 8)` smooths with a 4-lag window and not an 8-lag one.
    //
    // `points` is MATLAB's N and defaults to 128. The transform length is
    // `2 * ceil(Ncap / N) * N`, so a record shorter than the grid is ZERO
    // PADDED to twice the grid and the estimate at 128 points from 40 samples
    // is an interpolation of 40 samples' worth of information -- true of
    // MATLAB's answer as much as of this one.
    static bool empiricalTransferFunction(const ICoreMatrix& input, const ICoreMatrix& output,
                                          const double& sampleTime, const size_t& smoothing,
                                          const size_t& points,
                                          std::vector<std::complex<double>>& response,
                                          std::vector<double>& frequency,
                                          std::string* whyNot = nullptr);

    // ---- spa(u, y, Ts, M, w) ----------------------------------------------
    //
    // The Blackman-Tukey spectral estimate: covariance functions to `lagWindow`
    // lags, weighted by a Hann lag window, transformed at each requested
    // frequency, and divided --
    //
    //     G(w) = PHI_yu(w) / PHI_u(w)
    //
    // -- with the noise spectrum PHI_v = Ts * |PHI_y - |PHI_yu|^2 / PHI_u|
    // coming out of the same three transforms. `noiseSpectrum` is the
    // `SpectrumData` of the `idfrd` MATLAB answers; it is returned here
    // because the transforms behind it are already computed and dropping it
    // would make the console's answer strictly poorer than MATLAB's.
    //
    // `lagWindow` 0 means "not given" -- see `defaultLagWindow`. An empty
    // `frequencies` means MATLAB's own default grid, (1:128) * pi / 128 / Ts.
    //
    // ⚠ THE LAG WINDOW'S FIRST WEIGHT IS HALVED (`window(1) = window(1)/2` in
    // `spa.m`) and the transform is then taken as `2 * real(...)`, which is
    // the same thing as summing the negative lags -- but only because of that
    // halving. An implementation that used the Hann weights as written and
    // doubled would answer a spectrum too large by exactly R(0) everywhere.
    static bool spectralAnalysis(const ICoreMatrix& input, const ICoreMatrix& output,
                                 const double& sampleTime, const size_t& lagWindow,
                                 const std::vector<double>& frequencies,
                                 std::vector<std::complex<double>>& response,
                                 std::vector<double>& frequency,
                                 std::vector<double>& noiseSpectrum,
                                 std::string* whyNot = nullptr);

    // MATLAB's default lag window for `spa` on a record of `samples` samples.
    //
    // ⚠ IT IS NOT THE RULE `spa.m` APPEARS TO STATE, and this is the one
    // measurement of this row that no reading of the source gives you. The
    // file holds two rules, one for time-domain data and one for frequency-
    // domain data:
    //
    //     Mdef = max(min(30, floor(mean(Ncaps)   / 10)), 2)   % time domain
    //     Mdef = max(min(30, floor(mean(2*Ncaps) / 10)), 2)   % frequency
    //
    // and R2026a takes the SECOND ONE for ordinary time-domain records.
    // Measured on this machine over nine record lengths, reading the window
    // back out of `spa(iddata(y, u, 1)).Report.WindowSize`: 12 -> 2, 20 -> 4,
    // 25 -> 5, 40 -> 8, 60 -> 12, 100 -> 20, 150 -> 30, 200 -> 30, 320 -> 30.
    // Every one of those is `min(30, floor(N / 5))` and none of them is
    // `floor(N / 10)`, which would have answered 4 where MATLAB answers 8 on
    // this board's own 40-sample record -- a spectral estimate half as
    // resolved, differing in the FIRST digit (0.634 against 0.701 at the
    // lowest frequency), and passing every test a session would think to
    // write about it.
    static size_t defaultLagWindow(const size_t& samples);
};
};

ICoreOptimizationOptions.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreOptimizationOptions.h

ICoreOptimizationOptions#

ICoreOptimizationOptions.h:38 · class · nested Setting, Values · 8 declaration(s)

optimoptions, optimset and optimget -- the option SET the Optimization Toolbox solvers take, and the reader every solver on this console goes through to find out what it was asked for (toolbo...

class ICoreOptimizationOptions {
public:

    // The solvers an option set can be made for. `None` is a legacy
    // `optimset(...)` set, which names no solver: MATLAB's optimset is
    // solver-agnostic and its 55 names are one flat list, with whichever
    // solver receives the set deciding which of them it reads.
    enum class Solver { Fminunc, Fsolve, Lsqnonlin, Lsqcurvefit, Lsqlin, Linprog,
                        Quadprog, Intlinprog, Fminimax, Fgoalattain, Fmincon,
                        Fminsearch, Fminbnd, Fzero, Lsqnonneg, None };

    // One `name, value` pair as it was typed. A value is a NUMBER or a WORD;
    // MATLAB's logical options (`SpecifyObjectiveGradient`) are numbers, and
    // its `'on'`/`'off'` legacy spellings are words.
    struct Setting {
        std::string name;
        bool        isWord = false;
        std::string word;
        double      number = 0.0;
    };

    // What every solver here reads out of a set.
    //
    // ⚠ AN UNSET FIELD IS NaN AND NOT A DEFAULT, and that is the whole reason
    // the struct is shaped this way: the four solvers do NOT share defaults.
    // `fminunc`'s TolX is 1e-6 and `fminbnd`'s is 1e-4; `fzero`'s is `eps`;
    // `fminsearch`'s evaluation budget is `200*n` and the gradient solvers'
    // is `100*n`. Filling one set of defaults in here would silently retune
    // three of them, so each solver reads a field only when it was ASKED for
    // and otherwise runs the number its own transcription carries. That is
    // also what makes "no option set" and "optimoptions(solver)" the same
    // run, which a regress case asserts.
    struct Values {
        double stepTolerance          = notSet();
        double functionTolerance      = notSet();
        double optimalityTolerance    = notSet();
        double objectiveLimit         = notSet();
        double maxIterations          = notSet();
        double maxFunctionEvaluations = notSet();
        double constraintTolerance    = notSet();
        bool   specifyObjectiveGradient = false;
        bool   specifyConstraintGradient = false;
    };

    // The "unset" marker, and the reader every solver uses: `given(v, d)` is
    // `v` when the caller asked for it and `d` -- that solver's own default --
    // when it did not.
    static double notSet();
    static bool   wasGiven(const double& value);
    static double given(const double& value, const double& fallback);

    // 'fminunc' or '@fminunc', in any case. False for a word that names no
    // solver at all, which is a different error from naming one optimoptions
    // does not support.
    static bool solverOf(const std::string& word, Solver& out);
    static std::string upperCaseName(const Solver& solver);
    static std::string lowerCaseName(const Solver& solver);

    // `optimoptions(solver, name, value, ...)`. `existing` is the text of a
    // set being updated (`optimoptions(opts, ...)`) or empty for the
    // constructor form; when it is not empty its solver wins and `solver` is
    // ignored.
    static bool build(const Solver& solver, const std::string& existing,
                      const std::vector<Setting>& settings, std::string& text,
                      std::string* whyNot = nullptr);

    // `optimset(name, value, ...)` and `optimset(oldopts, name, value, ...)`.
    // The names stay in their LEGACY spelling, because that is the spelling
    // `optimget` answers them under and the only one the four core solvers
    // have ever had.
    static bool buildLegacy(const std::string& existing, const std::vector<Setting>& settings,
                            std::string& text, std::string* whyNot = nullptr);

    // `optimset(oldopts, newopts)` -- MATLAB's merge, where every option
    // newopts SETS wins and every one it leaves unset keeps oldopts' value.
    static bool mergeLegacy(const std::string& older, const std::string& newer,
                            std::string& text, std::string* whyNot = nullptr);

    // `optimset(solver)` -- the option set that solver runs on, with its own
    // defaults filled in. ⚠ TWO OF THEM ARE STRINGS: MATLAB answers
    // `MaxIter = '200*numberofvariables'` for fminsearch, because the number
    // cannot be written down until n is known -- and `resolve` reads a
    // word-valued budget as "the solver's own rule" rather than as a number.
    static bool defaultsFor(const Solver& solver, std::string& text,
                            std::string* whyNot = nullptr);

    // `optimget(opts, name)`. `found` is false for a name the set does not
    // carry -- MATLAB answers `[]` there, and the caller supplies its own
    // default for the three-argument form. A name that is not an option at all
    // is an error and answers false.
    //
    // ⚠ AN optimoptions SET IS REFUSED HERE, and that is MATLAB's own rule
    // rather than a gap: `optimget(optimoptions('fminunc', ...), 'TolX')`
    // answers "First argument must be an options structure created with
    // OPTIMSET." there. The symmetry is inviting and it is not there -- this
    // console answered the value until the parity suite said otherwise. A
    // modern set is read by handing it to its SOLVER; MATLAB's other reader
    // is property access, which C0.3 leaves this console without.
    static bool read(const std::string& text, const std::string& named, bool& found,
                     Setting& value, std::string* whyNot = nullptr);

    // Is this string an option set rather than an ordinary one? Used by the
    // solvers to tell `fminunc(f, x0, opts)` from a string argument that is
    // something else entirely. The DISPLAY of a set is this same text, which
    // it already is as a `Kind::Str` -- there is no separate print form, for
    // the reason the header comment gives.
    static bool isOptionSet(const std::string& text);

    // Read a set for `solver`. Answers false with the reason when the set
    // names an option this console's algorithm cannot honour, or an option
    // belonging to a DIFFERENT solver -- MATLAB refuses that too, and silently
    // reading an fminunc set inside fsolve would honour the wrong defaults.
    static bool resolve(const std::string& text, const Solver& solver, Values& out,
                        std::string* whyNot = nullptr);

    // The algorithm this console runs for `solver`, in MATLAB's own spelling,
    // or "" for a solver whose method is this console's own rather than one of
    // MATLAB's named ones. `Algorithm` is honoured when it names this, and
    // refused with this sentence otherwise.
    static std::string implementedAlgorithm(const Solver& solver);
};
};

ICoreParametricIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreParametricIdentification.h

ICoreParametricIdentification#

ICoreParametricIdentification.h:52 · class · 3 declaration(s)

The linear-regression estimators of the System Identification Toolbox -- MATLAB's arx, ar and ivar (toolbox row T6.3) -- and the order-search loops built on top of them, arxstruc, `selstruc...

class ICoreParametricIdentification {
public:
    // MATLAB's five `ar` approaches. `ForwardBackward` is the DEFAULT -- not
    // least squares, and not Burg, which is what a reader of the name usually
    // expects -- and it fits the forward and the time-reversed equations
    // together in one solve.
    enum class ArApproach { ForwardBackward, LeastSquares, YuleWalker, Burg, GeometricLattice };

    // How the data outside the measured interval is treated: `Pre` prepends n
    // zeros, `Post` appends n, `PrePost` both. `None` is the default for every
    // approach except `YuleWalker`, which ALWAYS uses `PrePost` -- MATLAB
    // overrides a conflicting request with a warning -- and the padding is
    // what makes Yule-Walker the autocorrelation method rather than the
    // covariance one.
    enum class ArWindow { None, Pre, Post, PrePost };

    // `arx(u, y, Ts, [na nb nk])`. `denominator` comes back monic and na+1
    // long, `numerator` nk+nb long with `nk` leading zeros -- MATLAB's own
    // `m.A` and `m.B`.
    //
    // ⚠ THE FIRST SAMPLE USED IS NOT THE ONE THE MODEL ORDER SUGGESTS, and
    // getting this wrong is not a rounding difference. The regression starts
    // at t = max(na, nb - (nk == 0), 1) + 1 -- a rule that DOES NOT MENTION
    // nk -- and any input regressor u(t - k) that reaches before the first
    // sample is filled with ZERO rather than dropping its equation. The
    // reading a careful implementer arrives at instead, "start at
    // max(na, nb + nk - 1) + 1 so every regressor exists", is a different
    // estimate: on the forty-sample record this row's corpus ships, at
    // [na nb nk] = [1 2 3], it answers a1 = -0.443333 where MATLAB and this
    // console answer -0.474552. `arx_time.m`'s `nmax` and its `I = jj > kl`
    // guard are the two lines that say so.
    //
    // ⚠ The two rules AGREE whenever nk = 0 -- `nb - (nk == 0)` and
    // `nb + nk - 1` are both `nb - 1` there -- so a case with no delay cannot
    // tell them apart however carefully it is chosen. Only nk >= 1 with
    // nb + nk - 1 above max(na, nb) separates them, which is why the corpus
    // carries [1 2 3] and not just a sweep of nk.
    //
    // `lossFunction`, when asked for, is the mean squared residual of that
    // same regression -- the number `arxstruc` scores an order set by.
    static bool arx(const ICoreMatrix& input, const ICoreMatrix& output,
                    const size_t& na, const size_t& nb, const size_t& nk,
                    std::vector<double>& denominator, std::vector<double>& numerator,
                    double* lossFunction = nullptr, std::string* whyNot = nullptr);

    // `ar(y, n[, approach[, window]])`. `denominator` comes back monic and
    // n+1 long.
    //
    // `reflection` is MATLAB's second output and is filled only for `Burg` and
    // `GeometricLattice`: row 1 the reflection coefficients with a leading 0,
    // row 2 the loss function after each stage. It comes back EMPTY for the
    // three approaches that are a solve rather than a recursion, which is what
    // MATLAB leaves there too.
    static bool ar(const ICoreMatrix& series, const size_t& order,
                   const ArApproach& approach, const ArWindow& window,
                   std::vector<double>& denominator, ICoreMatrix& reflection,
                   std::string* whyNot = nullptr);

    // `ivar(y, na[, nc])` -- the instrumental-variable AR estimate. Four
    // stages, and the third is the one nothing in the documentation mentions:
    // a least-squares AR of order na + nc whitens the series, an ARX of that
    // residual against the series gives a noise polynomial C, **C is reflected
    // into the unit disc if it came out unstable** (`fstab`), and the
    // instruments are the series filtered by 1/(C*C) after a shift of nc
    // samples. The final solve is the IV normal equation
    // `sum psi phi' theta = sum psi y`, which is NOT symmetric and is why the
    // estimate is unbiased where `ar`'s least squares is not.
    //
    // `nc` defaults to `na` -- pass `na` for MATLAB's two-argument form.
    static bool ivar(const ICoreMatrix& series, const size_t& na, const size_t& nc,
                     std::vector<double>& denominator, std::string* whyNot = nullptr);

    // The spellings MATLAB takes on the call line, lower-cased by the caller:
    // "fb", "ls", "yw", "burg", "gl" and "now", "prw", "pow", "ppw". Both
    // answer false for a name they do not know, leaving `out` untouched.
    static bool approachFromName(const std::string& name, ArApproach& out);
    static bool windowFromName(const std::string& name, ArWindow& out);

    // `arxstruc(ze, zv, NN)` -- row T6.11. One ARX fit per ROW of `orders`
    // (each row `[na nb nk]`) on the estimation pair, scored on the validation
    // pair, in MATLAB's own layout: `losses` is 4 x (rows + 1), the first row
    // the loss per order set and the three below it that set's `[na; nb; nk]`.
    // The EXTRA last column is a footer rather than a model:
    // `losses(1, end)` is the number of validation samples and
    // `losses(2, end)` the mean square of the validation output.
    //
    // ⚠ `arxstruc` DROPS ONE MORE SAMPLE THAN `arx` DOES. Its own start is
    // `max(max(na) + 1, max(nb - (nk == 0)))` -- note the `+ 1`, which
    // `arx_time.m` does not have -- so the loss it reports for an order set is
    // not the residual of the model `arx` returns for that same set. Both
    // rules are transcribed rather than unified, because unifying them would
    // make one of the two rows disagree with MATLAB.
    static bool arxstruc(const ICoreMatrix& estimationInput, const ICoreMatrix& estimationOutput,
                         const ICoreMatrix& validationInput, const ICoreMatrix& validationOutput,
                         const ICoreMatrix& orders, ICoreMatrix& losses,
                         std::string* whyNot = nullptr);

    // `selstruc(V, c)` -- the numeric criteria only; the interactive
    // `selstruc(V)` picker is Z2 and never reaches here. The penalty is
    // `V * (1 + alpha * (na + nb) / N)` with `alpha` 0 for the plain minimum,
    // 2 for `'aic'` and `log(N)` for `'mdl'` -- so this is the ORIGINAL
    // Akaike form and not the `log` of anything, which is what the name leads
    // a reader to write.
    //
    // `selectedOrder` is the winning `[na nb nk]` row; `criterionValues` is
    // MATLAB's second output, the input matrix without its footer column and
    // with the penalised losses replaced by their LOGARITHMS -- the one place
    // a log does appear.
    // `Alpha` takes the penalty weight from `alpha` -- MATLAB's numeric `c`,
    // of which `selstruc(V, 0)` is the plain minimum -- while `Aic` and `Mdl`
    // compute their own (2 and log(N)) from V's own sample count.
    enum class SelectionCriterion { Alpha, Aic, Mdl };
    static bool selstruc(const ICoreMatrix& losses, const SelectionCriterion& criterion,
                         const double& alpha, ICoreMatrix& selectedOrder,
                         ICoreMatrix& criterionValues, std::string* whyNot = nullptr);
    static bool criterionFromName(const std::string& name, SelectionCriterion& out);

    // `delayest(u, y, Ts[, na, nb, nkmin, nkmax])` -- the delay whose ARX fit
    // has the smallest loss, scored by `arxstruc` with the SAME data as both
    // the estimation and the validation set (which is what MATLAB passes) and
    // chosen by `selstruc(V, 0)`. MATLAB's defaults are na = 2, nb = 2,
    // nkmin = 0 and nkmax = nkmin + 40.
    static bool delayest(const ICoreMatrix& input, const ICoreMatrix& output,
                         const size_t& na, const size_t& nb,
                         const int& nkMin, const int& nkMax,
                         int& delay, std::string* whyNot = nullptr);
};
};

ICorePredictionErrorIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICorePredictionErrorIdentification.h

ICorePredictionErrorIdentification#

ICorePredictionErrorIdentification.h:59 · class · nested Orders, Model · 0 declaration(s)

The PREDICTION-ERROR estimators of the System Identification Toolbox -- MATLAB's armax, oe, bj and pem (toolbox row T6.4) -- over the general polynomial model A(q) y(t) = [B(q)/F(q)] u(t - ...

class ICorePredictionErrorIdentification {
public:
    // Orders in MATLAB's own names. `nk` is the input delay in samples.
    struct Orders {
        size_t na = 0;
        size_t nb = 0;
        size_t nc = 0;
        size_t nd = 0;
        size_t nf = 0;
        size_t nk = 0;
    };

    // MATLAB's A, B, C, D and F rows, in q^-1 and in MATLAB's own layout: A, C,
    // D and F monic and one longer than their order, B exactly nk + nb long
    // with nk leading zeros. A polynomial the structure does not have comes
    // back as the scalar row {1} (and B as {0} when nb is zero), which is what
    // `m.A`, `m.C`, `m.D` and `m.F` read on an idpoly that does not carry them.
    struct Model {
        std::vector<double> a{1.0};
        std::vector<double> b;
        std::vector<double> c{1.0};
        std::vector<double> d{1.0};
        std::vector<double> f{1.0};

        // V at the answer -- e'e/N, the number MATLAB reports as Report.Fit.LossFcn.
        double loss = 0.0;
        // What the search did, for the row's note and for a refusal that needs
        // to say why nothing better came back. `whyStop` is MATLAB's own
        // sentence.
        size_t iterations = 0;
        size_t functionCount = 0;
        std::string whyStop;
    };

    // `armax(u, y, Ts, [na nb nc nk])`, `oe(u, y, Ts, [nb nf nk])` and
    // `bj(u, y, Ts, [nb nc nd nf nk])` all arrive here; which one it is is
    // carried by the orders alone. Answers false with `whyNot` set when the
    // record is too short for the orders, when a polynomial cannot be
    // initialized, or when the search never left an infinite loss.
    static bool estimate(const ICoreMatrix& input, const ICoreMatrix& output,
                         const Orders& orders, Model& out, std::string* whyNot = nullptr);

    // MATLAB's `pem(data, init_sys)`: the same search from a GIVEN model
    // instead of the initialisation above. `initial` supplies every polynomial
    // and `orders` must agree with their lengths. MATLAB refuses a bare
    // `pem(data)` without an initial model and so does the caller of this.
    static bool refine(const ICoreMatrix& input, const ICoreMatrix& output,
                       const Model& initial, const Orders& orders,
                       Model& out, std::string* whyNot = nullptr);

    // The initialisation on its own -- MATLAB's `inival_time`. Exposed because
    // it is worth testing separately from the search: an initialisation that
    // is one sample out lands the search in a different minimum, and the two
    // failures look alike from the outside.
    static bool initialize(const ICoreMatrix& input, const ICoreMatrix& output,
                           const Orders& orders, Model& out, std::string* whyNot = nullptr);
};
};

ICoreRecursiveIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreRecursiveIdentification.h

ICoreRecursiveIdentification#

ICoreRecursiveIdentification.h:37 · class · 1 declaration(s)

The RECURSIVE estimators of the System Identification Toolbox that are still functions rather than objects -- MATLAB's rarx and rpem (toolbox board row T6.13).

class ICoreRecursiveIdentification {
public:
    // How the gain is adapted at each step -- MATLAB's `adm` argument, and its
    // own four spellings.
    enum class Adaptation {
        ForgettingFactor,   // "ff": recursive least squares, `adg` is lambda
        KalmanFilter,       // "kf": `adg` is R1, the parameter drift covariance
        NormalizedGradient, // "ng": `adg` is the gain, step divided by |phi|^2
        UnnormalizedGradient// "ug": `adg` is the gain
    };

    static bool adaptationFromName(const std::string& name, Adaptation& out);

    // rarx(z, [na nb nk], adm, adg).
    //
    // `data` is MATLAB's `z` and is OUTPUT FIRST: column 0 is y, the remaining
    // columns are the inputs. (The estimators of T6.3 take `(u, y, ...)`; this
    // one takes one matrix, in MATLAB's own order for it -- the argument-order
    // trap T6.1's note names, and it is MATLAB's, not this console's.)
    //
    // `orders` is [na, nb..., nk...] with one nb and one nk per input, so its
    // length is 2 * inputs + 1; a time series (no input column) takes [na]
    // alone. Every nk must be one or more.
    //
    // `gain` is `adg`: a scalar for three of the four mechanisms, and for
    // "kf" either a scalar (read as that multiple of the identity) or the full
    // d x d drift covariance.
    //
    // `trajectory` comes back N x d -- one row per sample, parameters in
    // MATLAB's order -- and `prediction` N x 1, holding `yhat(k)`, the value
    // predicted for row k from the estimate held BEFORE that row was read.
    //
    // ⚠ THE INITIAL PARAMETER IS `eps`, NOT ZERO (`th0 = eps*ones(d,1)` in
    // `rarx.m`), and P0 is `10000 * I`. The first is invisible in the answer
    // and the second is not: a different P0 is a different trajectory for the
    // whole record, not just for its first rows.
    static bool arxTrajectory(const ICoreMatrix& data, const ICoreMatrix& orders,
                              const Adaptation& adaptation, const ICoreMatrix& gain,
                              ICoreMatrix& trajectory, ICoreMatrix& prediction,
                              ICoreMatrix& covariance, std::string* whyNot = nullptr);

    // rpem(z, [na nb nc nd nf nk], adm, adg) -- the SAME recursion over the
    // general model, and the reason `rarx` is not simply a special case of it
    // in MATLAB either (they are two files, and this one is 254 lines to that
    // one's 125):
    //
    //     A(q) y(t) = [B(q)/F(q)] u(t) + [C(q)/D(q)] e(t)
    //
    // Zeroing the right orders gives every classical structure: ARX is
    // [na nb 0 0 0 nk], ARMAX is [na nb nc 0 0 nk], output error is
    // [0 nb 0 0 nf nk] and Box-Jenkins is the whole thing.
    //
    // ⚠ WHAT MAKES IT MORE THAN `rarx` WITH MORE PARAMETERS: the gain is
    // formed from a GRADIENT vector psi and not from the regressor phi. Once
    // the model has a C, D or F polynomial the prediction is no longer linear
    // in the parameters, so the two vectors part company -- psi carries the
    // filtered signals -- and the update uses psi where `rarx` uses phi. A
    // reader who followed `rarx`'s shape and reached for phi here would get an
    // answer that looks like a fit and is not the prediction-error one.
    //
    // ⚠ AND THE NOISE POLYNOMIALS ARE REFLECTED INTO THE UNIT DISC AT EVERY
    // STEP (`fstab`), C and each F, before they are used and after they are
    // written back into the parameter vector. It is not a safety net bolted
    // on: an unstable C makes the next residual diverge, so the trajectory
    // MATLAB answers is the one with the reflection in it and a run without it
    // separates from MATLAB's within a few samples on any record that provokes
    // one.
    static bool predictionErrorTrajectory(const ICoreMatrix& data, const ICoreMatrix& orders,
                                          const Adaptation& adaptation, const ICoreMatrix& gain,
                                          ICoreMatrix& trajectory, ICoreMatrix& prediction,
                                          ICoreMatrix& covariance, std::string* whyNot = nullptr);
};
};

ICoreRecursiveLeastSquares.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreRecursiveLeastSquares.h

ICoreRecursiveLeastSquares#

ICoreRecursiveLeastSquares.h:18 · class · pImpl · 7 declaration(s)

Online (streaming) ARX transfer-function estimation via recursive least squares with a scalar exponential forgetting factor.

class ICoreRecursiveLeastSquares {
public:
    // numOrder must be <= denOrder (causal/proper), matching ICoreTransferFunction's
    // own causality requirement. forgettingFactor in (0, 1]; 1.0 = no forgetting.
    ICoreRecursiveLeastSquares(const size_t& numOrder, const size_t& denOrder, const double& forgettingFactor = 1.0);

    // Feeds one new (input, output) sample and updates the parameter estimate.
    void update(const double& uk, const double& yk);

    // Builds the currently-estimated transfer function (Ts = sampling time, must be > 0).
    // CANONICAL: ICoreTransferFunction trims all-but-one leading zero coefficient, so
    // the polynomials it hands back are as short as the estimate currently justifies —
    // during warm-up, when theta is still all zeros, the numerator collapses to a single
    // coefficient. Use it to display or to go on computing with; for anything that has a
    // FIXED width to fill (a sized output port, a row of a collected matrix) use the two
    // coefficient accessors below instead.
    [[nodiscard]] ICoreTransferFunction getEstimatedTf(const double& Ts) const;

    // The estimate in this class's own convention, always at full declared length:
    // numerator descending [b0..b_numOrder] (numOrder+1 entries) and denominator
    // descending monic [1, a1..a_denOrder] (denOrder+1 entries). Nothing is trimmed,
    // so the lengths are a function of the configured orders alone and never of the
    // values — which is what a fixed-size consumer needs, and what the generated
    // code emits.
    [[nodiscard]] std::vector<double> getNumeratorCoefficients() const;
    [[nodiscard]] std::vector<double> getDenominatorCoefficients() const;

    void reset();

    // Declared, not implicit: the residue below is a unique_ptr to an Impl that
    // is incomplete in this header, and only the .cpp can destroy one.
    ~ICoreRecursiveLeastSquares();

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

ICoreScalarOptimization.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreScalarOptimization.h

ICoreScalarOptimization#

ICoreScalarOptimization.h:35 · class · 0 declaration(s)

Core MATLAB's three solvers over a function you pass in (the core-MATLAB board, C8.21-C8.23): fzero, fminbnd and fminsearch.

class ICoreScalarOptimization {
public:

    // f(x) -> fx. False means the call itself failed.
    using Objective  = std::function<bool(double x, double& fx)>;
    // f(x1..xn) -> fx, for the only one of the three that searches in n
    // dimensions.
    using ObjectiveN = std::function<bool(const std::vector<double>& x, double& fx)>;

    // fzero(f, x0) -- search outward from x0 for a sign change, then solve
    // (C8.21). MATLAB refuses rather than answering when the search leaves the
    // reals or runs off to a non-finite value, and so does this.
    // `options` is a resolved `optimset(...)` set (T5.11) -- the four core
    // solvers take optimset's names and never optimoptions', which is
    // MATLAB's own split and not this console's. Null, or a set whose fields
    // are all unset, runs the default each of these three carries, and those
    // are NOT the same number: fzero's TolX is `eps`, fminbnd's is 1e-4 and
    // fminsearch's TolX and TolFun are both 1e-4 with a 200*n budget. It is
    // the last parameter because `whyNot` already had a default.
    static bool zero(const Objective& f, double x0, double& root,
                     std::string* whyNot = nullptr,
                     const ICoreOptimizationOptions::Values* options = nullptr);

    // fzero(f, [a b]) -- a bracket the caller supplies. MATLAB requires the
    // two ends to have OPPOSITE signs and says so by name when they do not;
    // an end that is already zero is the answer.
    static bool zeroInInterval(const Objective& f, double a, double b, double& root,
                               std::string* whyNot = nullptr,
                               const ICoreOptimizationOptions::Values* options = nullptr);

    // fminbnd(f, a, b) (C8.23). `fval` is the value at the minimiser.
    static bool minimumInInterval(const Objective& f, double a, double b,
                                  double& minimiser, double& fval,
                                  std::string* whyNot = nullptr,
                                  const ICoreOptimizationOptions::Values* options = nullptr);

    // fminsearch(f, x0) (C8.22). The starting point's LENGTH is the dimension,
    // so a scalar x0 searches a line and a 3-vector searches a volume.
    static bool minimumSearch(const ObjectiveN& f, const std::vector<double>& x0,
                              std::vector<double>& minimiser, double& fval,
                              std::string* whyNot = nullptr,
                              const ICoreOptimizationOptions::Values* options = nullptr);
};
};

ICoreSubspaceIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreSubspaceIdentification.h

ICoreSubspaceIdentification#

ICoreSubspaceIdentification.h:60 · class · nested Options, Result · 1 declaration(s)

MATLAB's n4sid -- a state-space model of a chosen order estimated from input/output samples by the SUBSPACE method, transcribed from +idpack/@ssdata/n4sid_time.m (the toolbox board's T6.5).

class ICoreSubspaceIdentification {
public:
    // MATLAB's `N4Weight`. `Auto` is `CVA` and that is source rather than a
    // guess: `n4sid_time.m` line 115 reads `elseif strcmpi(n4wc,'auto'), n4wc =
    // {'CVA'};`. `'SSARX'` and `'all'` are a second algorithm (`ssarx.m`, its
    // own 340 lines over an ARX pre-estimate) and refuse by name rather than
    // being silently answered by one of these two.
    enum class Weight { Auto, CVA, MOESP };

    struct Options {
        Weight weight = Weight::Auto;
        // MATLAB's `DisturbanceModel`: 'estimate' (the default, K is free) or
        // 'none' (K fixed at zero). It is not a cosmetic flag -- it changes the
        // automatic horizon rule, it makes the state matrix pass through
        // `fstab`, and it changes B and D.
        bool estimateDisturbance = true;
        // MATLAB's `Feedthrough`: whether D is a free parameter. False by
        // default for a model with states, and forced true for `nx = 0`.
        bool feedthrough = false;
        // MATLAB's `N4Horizon` -- {r, s1, s2}, the future and the two past
        // horizons. Empty is 'auto' and is by far the common case.
        std::vector<size_t> horizon;
    };

    // The model, in plain numbers rather than in a matrix type, which is the
    // convention `ICorePredictionErrorIdentification::Model` set: this header
    // is published as an API page and a caller that only wants the polynomials
    // should not have to take ICoreMatrix with it. Every matrix here is ROW
    // MAJOR and `order` is its side.
    struct Result {
        bool        ok = false;
        std::string message;
        size_t      order = 0;
        // `nx * nx`, `nx`, `nx` and the scalar -- A, B, C, D of a SISO model.
        // `nx = 0` leaves the first three empty and answers the static gain in
        // `d` alone, which is what MATLAB's `n4sidZeroOrder` does.
        std::vector<double> a, b, c;
        double              d = 0.0;
        // The Kalman gain the subspace step implies, `nx` long. It is computed
        // whether or not anything asks for it, because B, D and x0 are fitted
        // to the error of the predictor it defines -- so a wrong K is a wrong
        // B, not merely a missing noise model.
        std::vector<double> k;
        // MATLAB's second output: the initial state the fit used, `nx` long.
        std::vector<double> x0;
        // What `n4sid`'s estimation report calls N4Horizon and N4Weight.
        std::vector<size_t> horizon;
        std::string         weight;
    };

    // `n4sid(u, y, Ts, nx)` in the console's own argument order -- input first,
    // then the output, then the sample time, which is T6.1's convention for
    // every estimator in this family and is required rather than optional here
    // for the reason that row measured: in MATLAB an omitted sample time means
    // two different things in one toolbox (`arx(u, y, [2 2 1])` answers a model
    // with Ts = 1, `tfest(u, y, 2, 1)` a CONTINUOUS one with Ts = 0).
    //
    // ⚠ Ts SCALES NOTHING. Measured on R2026a: `n4sid(u, y, 2, 'Ts', 0.1)` and
    // `n4sid(u, y, 2, 'Ts', 1)` answer the same A, B, C, D to the last bit and
    // differ only in the sample time stamped on the model. The estimate is
    // written in lags, so the sample time is a label on the answer and not an
    // input to it. A Ts of zero is MATLAB's CONTINUOUS-time estimate, a
    // different code path (`ctEstWithDTData`) that this class does not have and
    // that refuses by name at the call site.
    //
    // u and y are vectors of the same length; nx is the model order, zero or
    // more. False with `whyNot` set when the arguments do not conform or the
    // data does not excite the model.
    static bool n4sid(const ICoreMatrix& u, const ICoreMatrix& y, const double& Ts,
                      const size_t& nx, const Options& options, Result& out,
                      std::string* whyNot = nullptr);

    // The name MATLAB spells a weight with, for the estimation report and for a
    // refusal that has to say which weights exist.
    static std::string weightName(const Weight& weight);
    // False for a name that is not one of the two implemented; `whyNot` then
    // carries the sentence that says which MATLAB has and which this console
    // answers. Case-insensitive, as MATLAB's own option is.
    static bool weightFromName(const std::string& name, Weight& out,
                               std::string* whyNot = nullptr);

};
};

ICoreSystemIdentification.h#

src/ICoreBlocks/ICoreMath/Optimization/ICoreSystemIdentification.h

ICoreSystemIdentification#

ICoreSystemIdentification.h:20 · class · nested Result · 5 declaration(s)

Fits a discrete-time transfer function of a chosen order to logged input/output data.

class ICoreSystemIdentification {
public:
    // The estimators, grouped by family. They differ in what they minimize and
    // in how much they cost, not in the model they return.
    enum class Method {
        // --- Output-error / prediction-error, nonlinear ---
        NonlinearLeastSquares = 0,  // default: Levenberg-Marquardt on simulation error
        Armax,                      // pseudo-linear regression with a C(q) noise model

        // --- Linear-regression family (closed form or a few linear solves) ---
        ArxLeastSquares,            // equation-error, one least-squares solve
        InstrumentalVariables,      // ARX bias removed with model-simulated instruments
        SteiglitzMcBride,           // iteratively 1/A-prefiltered ARX; approaches the OE optimum

        // --- Realization / subspace (no initial guess, no local minima) ---
        SubspaceN4SID,              // PO-MOESP style projection + SVD of the block-Hankel data
        ImpulseRealizationEra,      // least-squares FIR, then Eigensystem Realization (ERA)
        RegularizedFirKernel,       // TC/stable-spline regularized FIR (GCV tuned), then ERA

        // --- Frequency domain, fitted to the empirical transfer function ---
        FrequencyDomainLevy,        // Levy's linearized rational fit
        SanathananKoerner,          // Levy with 1/|A| iterative reweighting
    };

    // Outcome of a fit. `ok` false means nothing usable came back and `message`
    // says why; `ok` true with a non-empty `message` is a warning worth showing
    // (a solver that hit its iteration cap, a method that could not honour the
    // requested numerator order exactly, ...).
    struct Result {
        bool        ok = false;
        std::string message;
        double      fitPercent = 0.0;   // NRMSE fit of the simulated output, 100 = exact
    };

    // u, y: input/output sample vectors of equal length (same length, same Ts).
    // numOrder, denOrder: numerator/denominator degrees, numOrder <= denOrder.
    // Ts: sample time, must be > 0.
    // `outTf` receives the fitted model; the return value carries the status.
    static Result fit(const ICoreMatrix& u, const ICoreMatrix& y,
                      const size_t& numOrder, const size_t& denOrder, const double& Ts,
                      const Method& method, ICoreTransferFunction& outTf);

    // Convenience wrapper kept for existing callers (the console's tffit() --
    // spelled tfest() until T6.2 gave that name to MATLAB's estimator -- and
    // the offline IIR identification block): fits with `method` and returns just
    // the model, logging any failure through ICoreRunDiagnosis as before.
    static ICoreTransferFunction fitTransferFunction(const ICoreMatrix& u, const ICoreMatrix& y,
                                                      const size_t& numOrder, const size_t& denOrder,
                                                      const double& Ts,
                                                      const Method& method = Method::NonlinearLeastSquares);

    // Drives tf's state-space realization with input sequence u (a vector) and
    // returns the simulated output, one sample per entry of u, starting from a
    // zero initial state. tf must be discrete (Ts > 0).
    static ICoreMatrix simulate(const ICoreTransferFunction& tf, const ICoreMatrix& u);

    ///////////////////////////////
    ///     Method naming (UI)
    //////////////////////////////

    // Display names in presentation order; index 0 is the default method.
    static const std::vector<std::string>& methodDisplayNames();
    static std::string methodDisplayName(const Method& method);
    // Returns false when `name` matches no method, leaving `out` untouched.
    static bool methodFromDisplayName(const std::string& name, Method& out);

    // One-line description of what the method does, for a tooltip or hint line.
    static std::string methodSummary(const Method& method);

};
};