API — ICoreBlocks/ICoreMath/CurveFitting
The public contract of 5 header(s) under src/ICoreBlocks/ICoreMath/CurveFitting — 5 class/struct definition(s), 40 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
ICoreCurveDataPreparation.h#
src/ICoreBlocks/ICoreMath/CurveFitting/ICoreCurveDataPreparation.h
ICoreCurveDataPreparation#
ICoreCurveDataPreparation.h:41 · class · 1 declaration(s)
Curve Fitting's data-preparation half (toolbox row T8.14) -- MATLAB's prepareCurveData, prepareSurfaceData and excludedata.
class ICoreCurveDataPreparation {
public:
// `prepareCurveData(x, y)` / `(x, y, w)` and `prepareSurfaceData(x, y, z)`
// / `(x, y, z, w)` share one body, because MATLAB's do: both end in
// `prepareFittingData`, and the surface one only adds the meshgrid step
// in front of it. `columns` comes back holding one column vector per
// input, in the same order.
//
// An EMPTY first input to the curve form is replaced by the index vector
// `1:numel(y)` -- MATLAB's `prepareCurveData([], y)`, which is how a
// caller says "the samples are evenly spaced".
static bool prepareCurve(const std::vector<ICoreMatrix>& inputs,
std::vector<ICoreMatrix>& columns,
std::string* whyNot = nullptr);
// The surface form. When `x` and `y` are VECTORS and `z` is a matrix whose
// rows match `y` and whose columns match `x`, the two are expanded by
// meshgrid before anything else; when they match the other way round
// MATLAB expands them swapped (and warns), and this does the same. Any
// other combination is left alone and only has to agree in numel.
static bool prepareSurface(const std::vector<ICoreMatrix>& inputs,
std::vector<ICoreMatrix>& columns,
std::string* whyNot = nullptr);
// MATLAB's four exclusion rules, matched by prefix in this order.
enum class Exclusion { Indices, Domain, Range, Box };
// The method word, MATLAB's own prefix matching: a word that begins more
// than one of "indices", "domain", "range", "box" is ambiguous and is not
// a rule.
static bool exclusionOf(const std::string& word, Exclusion& rule);
// `excludedata(x, y, method, opt)` -- a 0/1 mask the shape of `x`, true
// where the point is to be EXCLUDED. `x` and `y` must be vectors of the
// same size, which is MATLAB's own check and not a console limitation.
static bool excludeData(const ICoreMatrix& x, const ICoreMatrix& y,
const Exclusion& rule, const ICoreMatrix& option,
ICoreMatrix& mask, std::string* whyNot = nullptr);
};
};
ICoreCurveFit.h#
src/ICoreBlocks/ICoreMath/CurveFitting/ICoreCurveFit.h
ICoreCurveFit#
ICoreCurveFit.h:39 · class · pImpl · nested Goodness · 25 declaration(s)
The Curve Fitting Toolbox's fittype and cfit -- the MODEL and the FITTED model -- as one value (toolbox rows T8.1, T8.2, T8.3, T8.6, T8.8, T8.9, T8.10, T8.12, over T0.8 (A) and X3's Kind::Fit).
class ICoreCurveFit {
public:
ICoreCurveFit();
ICoreCurveFit(const ICoreCurveFit& other);
ICoreCurveFit& operator=(const ICoreCurveFit& other);
~ICoreCurveFit();
// ---- Making a model (a `fittype`) ------------------------------------
// `fittype('poly2')`, and the name `fit(x, y, 'poly2')` resolves. False
// with MATLAB's own sentence for a name that is not a library model.
static bool libraryModel(const std::string& name, ICoreCurveFit& out,
std::string* whyNot = nullptr);
// `fittype('a*exp(-b*x)', ...)`. `coefficients` empty means MATLAB's own
// rule: every free name in the expression that is not the independent
// variable, in ALPHABETICAL order -- the trap for a `'StartPoint'` vector,
// since `fittype('c*x^2 + a*x + b')` orders its coefficients a, b, c.
//
// ⚠ `independent2` is the SECOND independent variable and it has NO
// DEFAULT (T8.13): a two-variable model is `fittype('a*x + b*y',
// 'independent', {'x', 'y'})` on MATLAB's side, and a call site that
// passed nothing here would silently build a curve out of an expression
// that names two variables -- the second would be read as a COEFFICIENT,
// and the model would fit and answer numbers nobody asked for. Empty is
// the curve; a name is the surface.
static bool customModel(const std::string& expression,
const std::vector<std::string>& coefficients,
const std::string& independent, const std::string& independent2,
const std::string& dependent,
ICoreCurveFit& out, std::string* whyNot = nullptr);
// `fittype({'x', '1'})` -- a LINEAR model given by its terms, fitted by
// one least-squares solve rather than by an iteration.
static bool linearTermsModel(const std::vector<std::string>& terms,
const std::vector<std::string>& coefficients,
const std::string& independent, const std::string& independent2,
const std::string& dependent,
ICoreCurveFit& out, std::string* whyNot = nullptr);
// ---- Fitting it ------------------------------------------------------
// `fit(x, y, model[, options])`. `x` and `y` are COLUMN vectors of the
// same length -- MATLAB refuses a row with "X must be a matrix with one or
// two columns.", which is why `prepareCurveData` (T8.14) exists -- and the
// answer carries everything `confint`, `predint`, `differentiate` and the
// goodness record need.
static bool fitTo(const ICoreCurveFit& model, const ICoreMatrix& x, const ICoreMatrix& y,
const ICoreFitOptions::Values& options, ICoreCurveFit& out,
std::string* whyNot = nullptr);
// ---- Asking it things ------------------------------------------------
bool isFitted() const;
std::string typeName() const; // 'poly1', 'customnonlinear'
std::string categoryName() const; // 'library' | 'custom' | 'interpolant' | 'spline'
std::string formulaText() const;
std::vector<std::string> coefficientNames() const;
std::string independentName() const;
std::string dependentName() const;
size_t coefficientCount() const;
// Whether one least-squares SOLVE answers it (`poly*` and a `fittype`
// given by its terms) or an iteration has to. It is what decides the
// option set's `Method`, which is why it is asked from outside.
bool isLinearModel() const;
// The fitted coefficients, in MATLAB's coefficient order. Empty for a
// model that has not been fitted, and for an interpolant or a spline --
// MATLAB answers a `pp` STRUCT there, which this console has no kind for
// and refuses by name rather than inventing one.
bool coefficientValues(std::vector<double>& out, std::string* whyNot = nullptr) const;
bool coefficient(const std::string& name, double& out, std::string* whyNot = nullptr) const;
// `f(x)` and `feval(f, x)` -- the same call, as MATLAB's are.
bool evaluate(const ICoreMatrix& x, ICoreMatrix& out, std::string* whyNot = nullptr) const;
// `[d1, d2] = differentiate(f, x)`. ANALYTIC for a library model (each one
// carries its own derivative, as MATLAB's `derexpr` does) and central
// differences with MATLAB's own step for a custom one.
bool derivatives(const ICoreMatrix& x, ICoreMatrix& first, ICoreMatrix& second,
std::string* whyNot = nullptr) const;
// `integrate(f, x, x0)` -- the integral from `x0` to each entry of `x`.
bool integral(const ICoreMatrix& x, const double& start, ICoreMatrix& out,
std::string* whyNot = nullptr) const;
// `gof` (T8.12): MATLAB's five fields, in MATLAB's order and its lower-case
// spelling. ⚠ `rmse` is `sqrt(sse/dfe)` and NOT `sqrt(sse/n)` -- core
// MATLAB's own `rmse(y, yfit)` since R2022b divides by n, so the two
// disagree BY DESIGN and `curvefit.itest` asserts both.
struct Goodness {
double sse = 0.0;
double rsquare = 0.0;
double dfe = 0.0;
double adjrsquare = 0.0;
double rmse = 0.0;
};
bool goodness(Goodness& out, std::string* whyNot = nullptr) const;
// `confint(f[, level])` -- 2 x ncoeff, lower row then upper.
//
// V = inv(X'*X) * (sse/dfe), read off the QR factor as Rinv*Rinv'
// ci = b -/+ tinv(1 - (1-level)/2, dfe) * sqrt(diag(V))
//
// which is `confint.m` line for line. Refused for an interpolant and a
// spline with MATLAB's own sentence: there are no coefficients to bound.
bool confidenceBounds(const double& level, ICoreMatrix& out, std::string* whyNot = nullptr) const;
// `predint(f, x[, level, 'observation'|'functional', 'on'|'off'])`, all
// four combinations -- `predint.m` line for line:
//
// E = J * Rinv (J the Jacobian at the solution)
// delta = sqrt(1 + sum(E.^2, 2)) observation
// = sqrt(sum(E.^2, 2)) functional
// crit = sqrt(p * finv(level, p, dfe)) simultaneous 'on'
// = tinv(1 - (1-level)/2, dfe) 'off'
// ci = yhat -/+ delta * sqrt(sse/dfe) * crit
enum class Interval { Observation, Functional };
bool predictionBounds(const ICoreMatrix& x, const double& level, const Interval& interval,
const bool& simultaneous, ICoreMatrix& out,
std::string* whyNot = nullptr) const;
// MATLAB's display of the value, with the NAME MATLAB puts in the formula
// line (`f(x) = p1*x + p2`) -- `ans` when the console has no name for it,
// which is MATLAB's own name for an unassigned value.
std::string matlabDisplay(const std::string& name) const;
// The re-parseable form the workspace stores and `parseCanonical` reads
// back. Full round-trip precision: a stored fit answers the same numbers
// it answered before it was stored, bounds included.
std::string canonicalText() const;
static bool parseCanonical(const std::string& text, ICoreCurveFit& out);
static bool looksCanonical(const std::string& text);
// Whether normalization was asked for, and the two numbers it used --
// MATLAB prints them in the display ("where x is normalized by mean 5 and
// std 3.317") and they are part of the fitted value.
bool normalization(double& mean, double& deviation) const;
// The SECOND independent's centre and scale (T8.13). ⚠ MATLAB normalizes
// each independent SEPARATELY and the two pairs are different numbers --
// measured on R2026a, the same surface answers mean 0.5748 / std 0.3175
// for x and mean 0.3613 / std 0.2451 for y -- so one pair cannot describe
// a normalized surface and the display prints both lines ("where x is
// normalized by ... and where y is normalized by ..."). False for a curve,
// which has no second independent to scale.
bool normalizationOfSecond(double& mean, double& deviation) const;
// The x-range of the data the fit was made from -- MATLAB's `xlim`, the
// one thing a `cfit` carries that is neither a coefficient nor a residual
// statistic, and the only thing `plot(f)` with no data of its own has to
// draw over (T8.11).
//
// ⚠ It is the range of the points the fit KEPT and it is stated in the
// CALLER's coordinates. Both halves were measured on R2026a rather than
// assumed: `'Exclude'` MOVES it (data over 0..5 with `x > 3` excluded
// answers [0 3]), and `'Normalize'` does NOT (the same data normalized
// still answers [0 5]) -- so it is a plotting range, not a fitting one,
// and the two coordinate systems are easy to confuse because every other
// number on this class lives in the fitting one.
//
// False for a model that was never fitted. MATLAB's own `plot` answers
// [-pi, pi] for a `cfit` built from coefficients alone, and that default
// belongs to the plot verb rather than here: a range nobody measured is
// not a property of this fit.
bool dataRange(double& low, double& high) const;
// The `'Span'` and `'Robust'` a `'lowess'`/`'loess'` fit ran on (T8.13) --
// the caller's own, or the 0.25 and `"Off"` the transcription defaults to.
// ⚠ THEY HAVE TO TRAVEL WITH THE FIT for the same reason the smoothing
// spline's `p` does: `fitoptions(f)` reads an option set back OUT of a
// fitted model on MATLAB's side, and a set that answered the defaults for
// a fit made at `'Span', 0.4` would be a wrong answer rather than a
// missing one. False for every model that is not a local regression.
bool localRegressionSettings(double& span, std::string& robust) const;
// The smoothing parameter a `'smoothingspline'` fit ran on -- the one the
// caller gave, or the one `csaps.m`'s own rule chose, which is what
// MATLAB's `[pp, p] = csaps(x, y)` answers as its second value and what
// `fitoptions(f).SmoothingParam` reads back (T8.4). False for every other
// model, which has no such number.
bool smoothingParameter(double& out) const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreDataSmoothing.h#
src/ICoreBlocks/ICoreMath/CurveFitting/ICoreDataSmoothing.h
ICoreDataSmoothing#
ICoreDataSmoothing.h:45 · class · 0 declaration(s)
smooth -- toolbox row T8.5, the half of it that answers numbers.
class ICoreDataSmoothing {
public:
// MATLAB's six method words, plus the choice it makes when none is given.
// ⚠ `Automatic` is not "moving": `smooth.m` picks `moving` for a uniform
// `x` and `lowess` for an uneven one, so the default method depends on the
// DATA and a caller who never passes a word can get either.
enum class Method {
Automatic, Moving, Lowess, Loess, RobustLowess, RobustLoess, SavitzkyGolay,
};
// `c = smooth(y)`, `smooth(y, span)`, `smooth(y, method)`,
// `smooth(y, span, method)` and the `smooth(x, y, ...)` forms of each.
//
// `x` may be empty, which is MATLAB's `smooth(y, ...)`: the samples are
// then `1:numel(y)` and the series is uniform by construction.
//
// `span` of 0 means "not given" and becomes 5. ⚠ A span BELOW 1 is a
// FRACTION of the data, not a window of one point: `smooth(y, 0.3)` is
// `ceil(0.3 * numel(y))` samples. A span at or below zero is an error on
// both sides.
//
// `degree` is `sgolay`'s polynomial order (MATLAB's default 2) and is
// ignored by the other five; it must be below the span.
static bool smooth(const ICoreMatrix& x, const ICoreMatrix& y, const double& span,
const Method& method, const size_t& degree,
ICoreMatrix& smoothed, std::string* whyNot = nullptr);
};
};
ICoreFitOptions.h#
src/ICoreBlocks/ICoreMath/CurveFitting/ICoreFitOptions.h
ICoreFitOptions#
ICoreFitOptions.h:35 · class · nested Setting, Values · 8 declaration(s)
fitoptions -- the option SET the Curve Fitting Toolbox's fit takes, and the reader fit goes through to find out what it was asked for (toolbox row T8.7, over T0.6 (A)).
class ICoreFitOptions {
public:
// MATLAB's `Method` words. `None` is the bare `fitoptions()` set,
// which names no method: MATLAB's base option object has only the four
// properties every method shares, and whichever model receives the set
// decides what it reads.
// ⚠ ELEVEN, not nine, since T8.13: the two DENSE surface interpolants have
// option classes of their own (`thinplateinterpoptions`,
// `biharmonicinterpoptions`), each with an `ExtrapolationMethod` and the
// four base properties -- measured off R2026a's own display.
enum class Method { None, LinearLeastSquares, NonlinearLeastSquares,
SmoothingSpline, LowessFit, CubicSplineInterpolant,
LinearInterpolant, NearestInterpolant, PchipInterpolant,
ThinPlateInterpolant, BiharmonicInterpolant };
// One `name, value` pair as it was typed. A value is a WORD (`'off'`,
// `'Bisquare'`, a method name) or a LIST OF NUMBERS -- MATLAB's
// `StartPoint`, `Lower`, `Upper`, `Weights` and `Exclude` are vectors and
// its tolerances are scalars, which is the same shape with one entry.
struct Setting {
std::string name;
bool isWord = false;
std::string word;
std::vector<double> numbers;
};
// What `fit` reads out of a set. ⚠ AN UNSET NUMBER IS NaN AND NOT A
// DEFAULT, for T5.11's reason: the defaults are per METHOD (a nonlinear
// fit's MaxIter is 400 and a linear one has no MaxIter at all), so a field
// is read only when it was ASKED for and otherwise the fit runs the number
// its own transcription carries.
struct Values {
Method method = Method::None;
bool normalize = false;
std::string robust = "Off"; // Off | Bisquare | LAR
std::vector<double> startPoint;
std::vector<double> lower;
std::vector<double> upper;
std::vector<double> weights;
std::vector<double> exclude; // MATLAB's logical mask
double smoothingParam = notSet();
double span = notSet();
double maxIterations = notSet();
double maxFunctionEvaluations = notSet();
double functionTolerance = notSet();
double stepTolerance = notSet();
};
static double notSet();
static bool wasGiven(const double& value);
// 'NonlinearLeastSquares' in any case, and MATLAB's own abbreviations are
// NOT accepted -- MATLAB matches a method name in full.
static bool methodOf(const std::string& word, Method& out);
static std::string nameOf(const Method& method);
// The property sheet MATLAB shows for that method, in MATLAB's own display
// ORDER. It is what decides whether a name is refused, and the order is
// what the canonical text is written in, so two sets built from the same
// names in different orders are the same text.
static std::vector<std::string> propertiesFor(const Method& method);
// `fitoptions(...)`. `existing` is the text of a set being updated
// (`fitoptions(opts, 'Name', value)`) or empty for the constructor form.
// A `'Method'` in `settings` wins over the one `existing` carries, which is
// MATLAB's rule and is why the method is resolved before any other name is
// checked against the sheet.
static bool build(const std::string& existing, const std::vector<Setting>& settings,
std::string& text, std::string* whyNot = nullptr);
// `fitoptions('Method', m)` with nothing else -- the set that method runs
// on, which is the canonical text with only its method in it. The defaults
// live in the fit, not here (see Values).
static bool defaultsFor(const Method& method, std::string& text, std::string* whyNot = nullptr);
// Is this string the canonical text of an option set?
static bool isOptionsText(const std::string& text);
// The set the text describes. False, with `whyNot`, for a text that is not
// one -- which is what `fit(x, y, model, opts)` asks before it starts.
static bool resolve(const std::string& text, Values& out, std::string* whyNot = nullptr);
// One property back out of a set, as MATLAB's `opts.Name` reads it. A name
// the set never set answers its METHOD's default through `resolve`, so
// this answers only what the TEXT holds, and says which it was.
static bool read(const std::string& text, const std::string& name,
Setting& out, std::string* whyNot = nullptr);
};
};
ICoreSplineForm.h#
src/ICoreBlocks/ICoreMath/CurveFitting/ICoreSplineForm.h
ICoreSplineForm#
ICoreSplineForm.h:49 · class · nested PP, BForm · 6 declaration(s)
The Curve Fitting Toolbox's SPLINE half -- csapi, csape, spap2, spaps, fnval, fnder, fnint, ppmak, spmak and fn2fm.
class ICoreSplineForm {
public:
// The piecewise-polynomial form. `coefs` is ROW-MAJOR, l rows of k, the
// layout MATLAB's `pp.coefs` prints: row i holds the coefficients of the
// polynomial on [breaks(i), breaks(i+1)) in LOCAL coordinates, highest
// power first.
struct PP {
std::vector<double> breaks;
std::vector<double> coefs;
size_t pieces = 0;
size_t order = 0;
};
// The B-form: a knot sequence, one coefficient per B-spline, and the
// order. `number` is coefs.size() and is not stored twice.
struct BForm {
std::vector<double> knots;
std::vector<double> coefs;
size_t order = 0;
};
// csape's end conditions. MATLAB spells them as words, as the pair
// `conds` of integers (0 periodic, 1 first derivative, 2 second), or by
// handing two extra data values; all three reach this enum, and the two
// ends are INDEPENDENT -- `csape(x, y, [2 1])` is a legal call.
//
// ⚠ `Variational` is NOT a fourth kind of end: it is `Second` with the end
// second derivatives forced to ZERO (csape.m sets conds = [2 2] and then
// discards any valconds), which is why it is here as its own name rather
// than as a flag on Second.
enum class End { Periodic = 0, FirstDerivative = 1, SecondDerivative = 2 };
// `csapi(x, y)` -- the not-a-knot interpolant, which is core `spline`'s
// curve (C8.14) handed over as a form rather than evaluated.
static bool csapi(const ICoreMatrix& x, const ICoreMatrix& y,
PP& out, std::string* whyNot = nullptr);
// `csape(x, y[, conds[, valconds]])`.
//
// `valuesGiven` false is MATLAB's `valsnotgiven`, and it is not the same
// as passing zeros: with no end values and a first-derivative end, csape1
// does NOT clamp the slope to 0 -- it estimates the end slope by local
// interpolation through the first three or four points, which is why
// `csape(x, y)` and `csape(x, y, 'complete', [0 0])` are different curves.
static bool csape(const ICoreMatrix& x, const ICoreMatrix& y,
const End& leftEnd, const End& rightEnd,
const double& leftValue, const double& rightValue,
const bool& valuesGiven, PP& out, std::string* whyNot = nullptr);
// MATLAB's five end-condition WORDS, mapped to the pair of ends. Returns
// false for a word csape.m does not know. `variational` also clears the
// end values, which `valuesCleared` reports.
static bool endConditionFromWord(const std::string& word, End& left, End& right,
bool& valuesCleared);
// `spap2(knots, k, x, y[, w])` and `spap2(pieces, k, x, y[, w])` -- the
// least-squares spline of order k. `knotsOrPieces` holding ONE number is
// MATLAB's piece count and the knots are then chosen by `aptknt` over a
// subsequence of the sites; anything longer is the knot sequence itself.
static bool spap2(const ICoreMatrix& knotsOrPieces, const size_t& order,
const ICoreMatrix& x, const ICoreMatrix& y, const ICoreMatrix& weights,
BForm& out, std::string* whyNot = nullptr);
// `[sp, values, rho] = spaps(x, y, tol[, w[, m]])` -- the smoothing spline
// by TOLERANCE, which is `csaps`'s dual (T8.4 takes the parameter, this
// takes the error it must not exceed).
//
// ⚠ A NEGATIVE `tol` IS NOT AN ERROR, it names rho directly: spaps.m's own
// "we are to work with a specified rho" branch, and the one call form that
// skips the Reinsch iteration entirely.
//
// ⚠ THE DEFAULT WEIGHTS ARE THE TRAPEZOIDAL RULE, not ones -- `chckxywp`
// gives `w = ([dx; 0] + [0; dx])/2`, so the two end samples carry HALF the
// weight of an interior one. Measured: passing ones changes every
// coefficient. An empty `weights` here is MATLAB's default, not ones.
static bool spaps(const ICoreMatrix& x, const ICoreMatrix& y, const double& tolerance,
const ICoreMatrix& weights, const size_t& smoothingOrder,
BForm& out, std::vector<double>& values, double& rho,
std::string* whyNot = nullptr);
// `fnval(f, x[, 'l'])` on either form, `fnder(f, n)` (a NEGATIVE n
// integrates, fnderp/fnderb's own arm) and `fnint(f[, value])`.
//
// ⚠ `leftLimit` is MATLAB's `'l'`, and it is NOT decoration: a form can
// jump, and there the two answers differ. Measured on R2026a,
// `fnval(ppmak([0 1 2], [1 2 3 4]), 1)` is 4 and the same call with `'l'`
// is 3.
//
// ⚠ AND THE TWO FORMS DISAGREE OUTSIDE THE BASIC INTERVAL, by design: a
// pp CONTINUES its end piece and a B-form is ZERO. Measured at x = -1 on
// the same two objects: 1 and 0.
static bool evaluate(const PP& pp, const ICoreMatrix& query, const bool& leftLimit,
ICoreMatrix& out, std::string* whyNot = nullptr);
static bool evaluate(const BForm& sp, const ICoreMatrix& query, const bool& leftLimit,
ICoreMatrix& out, std::string* whyNot = nullptr);
static bool derivative(const PP& pp, const int& order, PP& out,
std::string* whyNot = nullptr);
static bool derivative(const BForm& sp, const int& order, BForm& out,
std::string* whyNot = nullptr);
static bool integral(const PP& pp, const bool& hasValue, const double& value, PP& out,
std::string* whyNot = nullptr);
static bool integral(const BForm& sp, const bool& hasValue, const double& value,
BForm& out, std::string* whyNot = nullptr);
// `ppmak(breaks, coefs[, d])` and `spmak(knots, coefs[, sizec])`, with
// MATLAB's own sizing arithmetic -- which is the whole content of both
// names and is not obvious: `ppmak(b, c)` reads the ROWS of c as the
// target dimension and its COLUMNS as k*l, so `ppmak([0 1 2], [1 2; 3 4])`
// is a 2-VALUED spline of order 1 and not a scalar one of order 2.
static bool ppmak(const ICoreMatrix& breaks, const ICoreMatrix& coefs,
const bool& hasDimension, const double& dimension,
PP& out, std::string* whyNot = nullptr);
static bool spmak(const ICoreMatrix& knots, const ICoreMatrix& coefs,
const bool& hasSize, const ICoreMatrix& sizec,
BForm& out, std::string* whyNot = nullptr);
// `fn2fm(f, 'pp')` and `fn2fm(f, 'B-')`, both directions.
//
// ⚠ pp -> B- DROPS THE BREAKS THE SPLINE IS SMOOTH ACROSS, and that is
// pp2sp.m's documented behaviour rather than an optimisation: it measures
// the jump in each derivative at every interior break against a relative
// tolerance (1e-12) and gives that break only the knot multiplicity its
// ACTUAL smoothness needs. A not-a-knot cubic through six points comes
// back with four knots, not six, because it is C3 across the second and
// the second-to-last.
//
// `smoothness` is MATLAB's THIRD argument, `fn2fm(f, 'B-', sconds)`: one
// count per INTERIOR break saying how many smoothness conditions to
// impose there, which decides that break's knot multiplicity (k - sconds).
// Empty means guess them from the derivative jumps, which is what
// `fn2fm(f, 'B-')` does; a scalar strictly between 0 and 1 is the
// TOLERANCE of that guess, and reaches this through `tolerance`.
static bool toBForm(const PP& pp, const double& tolerance,
const std::vector<size_t>& smoothness, BForm& out,
std::string* whyNot = nullptr);
static bool toPP(const BForm& sp, PP& out, std::string* whyNot = nullptr);
// ---- the record seam (X3) --------------------------------------------
//
// The two forms as the value this console holds, and back. `form` is
// WITHHELD on the way out with the reason a reader can act on.
static ICoreRecord recordFromPP(const PP& pp);
static ICoreRecord recordFromBForm(const BForm& sp);
// Which form a record is, if either: a `breaks` field makes it a pp and a
// `knots` field a B-form. Both readers verify the field SHAPES agree with
// each other before answering, so a record built by hand out of the wrong
// numbers is refused here rather than evaluated into nonsense.
static bool isForm(const ICoreRecord& record);
static bool isPPRecord(const ICoreRecord& record);
static bool ppFromRecord(const ICoreRecord& record, PP& out, std::string* whyNot = nullptr);
static bool bFormFromRecord(const ICoreRecord& record, BForm& out,
std::string* whyNot = nullptr);
};
};