@danielsimonjr/mathts-functions
v0.73.0
Published
Mathematical functions for MathTS - arithmetic, algebra, trigonometry, statistics, and more
Maintainers
Readme
@danielsimonjr/mathts-functions
Math functions for MathTS.
The mathematical function library for MathTS — arithmetic, trigonometry, algebra, statistics, signal processing, and more — dispatched polymorphically via typed-function across number / Complex / Fraction / BigNumber / Matrix.
Install
npm install @danielsimonjr/mathts-functionsWhat it provides
- Hundreds of typed functions (
add,sin,svd-aware ops,parallelFFT, …). - A wired
parse/evaluateagainst the full activated math scope. scalar/evaluateScalar— evaluate a typed scalar tree (+ - * / ^,ln→ naturallog,log→log10, and the other scalar calls). A formula string is rejected.compileScalarevaluates the same tree overFloat64Arraycolumns and marks a row invalid whereevaluateScalarwould throw.compileScalarGradientreuses that tape to return exact ∂f/∂parameter for each row.fitScalarParametersfits those constants to data vialevenbergMarquardt,bfgs, ornelderMead(multi-restart search with optional bounds).levenbergMarquardtaccepts an optional analyticjacobianinLMOptions; the default remains forward-differencenumericJacobian. StiffsolveODE,solveDAE, andsolveBVPuse the same helper (forward differences; ODE/DAE usestep: { rel: 1e-7, floor: 1e-8 }, BVP shooting usesh: 1e-7).minimize(Nelder–Mead) delegates tonelderMeadwith an absolute simplex step;nelderMeadkeeps relative defaults forfitScalarParameters.scalarToNode/nodeToScalar/simplifyScalar/scalarToTex— convert between scalar trees and wired expression nodes (no formula string);simplifyScalaruses the samesimplifyas the expression language.paretoFront/scoreFront/selectFrontModel— hall of fame per complexity, log-loss scores on the log scale, andaccuracy/score/bestselection.frontToLatexTable/frontToJson— export a scored front (M15).frontToLatexTable(scored, { precision })writes onetabularwith columns complexity, loss, score, equation and a header row; loss and score aretoPrecision(precision)text (default 6, an integer from 1 to 100), anullscore (the linear loss scale) is an empty cell, and each equation cell isscalarToTexinside$…$because atabularcell is text mode and\cdot,\fracand\sinneed math mode (an infinite score is$\infty$).frontToJson(scored)returns{ schemaVersion: 1, rows: [{ complexity, loss, score, key, tex }] }withkey=scalarCanonicalKey(item)andtex=scalarToTex(item); a non-finitelossorscoreis the string'Infinity','-Infinity'or'NaN', so the report survivesJSON.stringifyandJSON.parse(an exact fit scoresInfinity, andJSONwould turn that into the linear scale'snull). A non-finite complexity throwsRangeError.inferScalarDimension/solveParameterDimensions— SI dimension of a scalar tree with exactFractionexponents onSiDimensionVector([L,M,T,I,Θ,N,J]), or one of four named violations with the path of the failing node (add-mismatch,transcendental-arg-not-dimensionless,non-literal-exponent-on-dimensional-base,unknown-symbol).solveParameterDimensionssolves the dimension of each unknown fitted parameter from the equations the tree implies (+operands agree, transcendental arguments are dimensionless, the root equalstarget), exactly viarationalNullspace; it returnssolved,underdetermined, orinconsistent.weightedMutation/mutateConstant/mutateOperator/mutateFeature/swapOperands/addNode/insertNode/deleteNode/rotateTree/randomizeTree/simplifyConstants/identityMutation/crossoverSubtrees— variation operators over a scalar tree. Each takes(node, rng, ctx)with aSeededRngandctx = { grammar, perturbationFactor, temperature }, never changes its input, and returnsnullwhen the mutation does not apply or the result failsscalarGrammarAccepts.weightedMutation(weights?)picks one operator byDEFAULT_MUTATION_WEIGHTS(summing to 8.512392). The identity operator is namedidentityMutationbecauseidentityis the matrix identity export of this package; its weight key is stillidentity.searchScalars— evolutionary search over scalar trees, an async generator ofSearchProgressthat returns aScalarSearchResult. One population evolves by tournament selection andweightedMutation; it starts fromseedsplus the smallest trees ofenumerateScalars(both filtered byscalarGrammarAcceptsand, when set,grammar.admit; a rejected seed is skipped, a rejected mutation is a failed attempt), refitsparametersof a tree with probabilityoptimizeProbability(fitScalarParameters), and keeps a hall of fame whose Pareto front is reported throughparetoFront/scoreFront(modelSelectionaddsselectFrontModel;bestLossFactorforwards to itsbestLossFactorwhenmodelSelectionis set, default 1.5).crossoverProbabilitydefaults to0(no coin, mutation-only). Above0each offspring may takecrossoverSubtreesof two tournament parents and otherwise falls through to mutation; the inherited parameters stay the first parent's.admitmust be a pure function of the node; the search does not copy it. Loss is the sum of squared errors by default (loss: 'l2'); setlossto'l1'or'huber'and the hall and every refit use that same named loss fromfitScalarParameters. OptionalrowWeights(one finite>= 0weight per row) apply to both. An optionalfitobject is forwarded tofitScalarParameters:restarts(default 2),maxIterations(default 8),bounds,algorithm, andinitial(default the inherited vector); a bad bound or length throwsRangeErrorbefore the first yield. A tree that is invalid on any positive-weight row has lossInfinityand never reaches the front; setinvalidRows: 'penalize'to score such a tree as the fitter's invalid-row penalty (1e8) times its invalid rows, plus the chosenlosson the valid rows, instead.fittedParametersgives the values behind each front row's loss.options.rngis never advanced, so one seed gives one result.runScalarSearch(problem, options)is the one-call form: it consumes the generator and returns the final result; its options takeseed(exactly one ofseedandrng) and its problem may givecolumnsas a record keyed by variable name instead of an aligned array.maxEvaluationscaps evaluations, initial population included. Withpopulations > 1independent islands (rng.fork('island:' + index)) evolve side by side; after every generation each island's worst trees are replaced by copies of other islands' best (fractionReplaced, default 0.00036), the constant-perturbation temperature cools frominitialTemperature(3.17) by 0.995 per generation (annealing), and tournaments rank byloss * exp(adaptiveParsimonyScaling * share)whereshareis how common the tree's size is (adaptiveParsimony, scaling 1040). Annealing and adaptive parsimony default to on only for several islands, sopopulations: 1is the single-population search to the last bit.parallel: { timeoutMs?, pool? }scores each generation's trees on worker threads onceobservedhas 100 or more rows (scalar-batch.worker.js, a second tsdown entry loaded by URL); parameter refits and every random draw stay on the caller thread, so a parallel run equals the serial run for the same seed, bit for bit.complexityWeights(aScalarComplexityWeightsfrom@danielsimonjr/mathts-expression) weighs the nodes of the complexity the hall, the truncation tie-break, and adaptive parsimony rank by; omitted, it is today's node count, and a non-finite weight throwsRangeErrorbefore the first yield.simplify('off' | 'constants' | 'algebraic', default'off') simplifies each seed and each bred offspring once, before the grammar check and the score:'constants'folds numeric-only subtrees (simplifyConstants),'algebraic'runssimplifyScalar; aScalarBuildError,RangeError, grammar rejection (includingadmit), or row-wise evaluation mismatch keeps the unsimplified tree (simplifyFallbackson the result), and enumerated trees are not simplified.timeoutMs(default 30000, must be positive) bounds every worker call, queueing included. Apoolyou pass is yours to terminate; otherwise the search starts one when it first needs it and terminates it when the generator ends or is closed. The 100-row floor comes from the M3 benchmark (the tape against the per-row interpreter, on the caller thread); no worker round trip was timed, so parallel is opt-in and carries no speed-up claim.numericallyEquivalent— sample two scalar trees withcreateSeededRng, evaluate viacompileScalar, compare with corenearlyEqual. Numerical agreement alone never proves symbolic equivalence; aconsistentverdict is evidence, not proof.
import { createSeededRng } from '@danielsimonjr/mathts-core';
import {
compileScalar,
compileScalarGradient,
fitScalarParameters,
numericallyEquivalent,
frontToJson,
frontToLatexTable,
paretoFront,
scoreFront,
selectFrontModel,
scalar,
simplifyScalar,
scalarToTex,
runScalarSearch,
} from '@danielsimonjr/mathts-functions';
const node = scalar.op('*', [scalar.symbol('x'), scalar.symbol('x')]);
const linear = scalar.op('+', [
scalar.op('*', [scalar.symbol('a'), scalar.symbol('x')]),
scalar.symbol('b'),
]);
const gradCompiled = compileScalarGradient(linear, { variables: ['x'], parameters: ['a', 'b'] });
const { values, grad } = gradCompiled.evaluate([new Float64Array([3])], [2, -1]);
values[0]; // 5
[...grad]; // 3, 1 — ∂/∂a = x, ∂/∂b = 1
const quotient = scalar.op('/', [
scalar.symbol('a'),
scalar.op('/', [scalar.symbol('b'), scalar.symbol('c')]),
]);
scalarToTex(simplifyScalar(quotient)); // \frac{ a\cdot c}{ b}
const compiled = compileScalar(node, { variables: ['x'] });
compiled.evaluate([new Float64Array([2, 3])]).values; // Float64Array [4, 9]
const slope = scalar.op('*', [scalar.symbol('a'), scalar.symbol('x')]);
const xCol = new Float64Array([1, 2, 3]);
const yCol = new Float64Array([2, 4, 6]);
fitScalarParameters(slope, [xCol], yCol, ['x'], { parameters: ['a'], restarts: 0 }).parameters[0]; // 2
const front = paretoFront([
{ loss: 0.5, complexity: 1, item: 'x' },
{ loss: 0.1, complexity: 3, item: 'x^3' },
]);
selectFrontModel(scoreFront(front), 'best').item; // 'x^3'
frontToJson(scoreFront([{ loss: 0.5, complexity: 1, item: scalar.symbol('x') }])).rows[0]!.key; // '{"k":"s","v":"x"}'
const rng = createSeededRng(1);
const id = scalar.op('+', [
scalar.op('^', [scalar.call('sin', scalar.symbol('x')), scalar.number(2)]),
scalar.op('^', [scalar.call('cos', scalar.symbol('x')), scalar.number(2)]),
]);
numericallyEquivalent(id, scalar.number(1), { domain: { x: [0, 1] }, rng }).verdict; // 'consistent' — evidence, not proof
const quadGrammar = {
variables: ['x'],
binary: ['+', '*', '^'],
unary: [],
literals: [1],
maxSize: 7,
} as const;
const quad = scalar.op('+', [
scalar.op('*', [scalar.symbol('x'), scalar.symbol('x')]),
scalar.symbol('x'),
]);
const xs = new Float64Array(20);
for (let i = 0; i < 20; i += 1) xs[i] = -1 + (2 * i) / 19;
const quadResult = await runScalarSearch(
{
variables: ['x'],
columns: { x: xs },
observed: compileScalar(quad, { variables: ['x'] }).evaluate([xs]).values,
},
{ grammar: quadGrammar, seed: 7, populationSize: 8, tournamentSize: 4, maxEvaluations: 120 }
);
quadResult.front.at(-1)?.item; // x^2 + x — the planted law, at loss 0
quadResult.stopReason; // 'max-evaluations' — runScalarSearch consumes the generator for you- Per-domain focused entry points are also published:
@danielsimonjr/mathts-{arithmetic,trigonometry,statistics,signal}.
License
MIT (c) Daniel Simon Jr.
