kumulant API reference
Kumulant is a streaming machine-learning library for Kotlin Multiplatform. Every model runs online: feed observations into an accumulator one at a time, ask for a result whenever you want one, and memory stays bounded no matter how long the stream runs. Two accumulators of the same shape can be merged into one, so partial results from parallel workers stitch back together without re-reading inputs.
Mental model
Every accumulator implements com.eignex.kumulant.core.Stat. The contract is three verbs and one constant:
update(...)folds a single observation into the running state. The exact signature depends on the modality (see the stat package for the four modality interfaces).read()materialises the current state as an immutable Result. Reads never mutate; call them as often as you like.merge(snapshot)folds another accumulator's snapshot into this one. Snapshots are the merge unit, not live accumulators; that is what lets merge cross a process boundary.concurrency: Concurrencyrecords the thread-safety contract the stat was built for. See com.eignex.kumulant.core.Concurrency.
reset() returns the stat to its construction baseline. create() spawns a fresh accumulator with the same configuration, optionally overriding the concurrency mode.
val mean = MeanStat()
for (x in doubleArrayOf(1.0, 2.0, 3.0)) mean.update(x)
val snapshot = mean.read()
println(snapshot.mean) // 2.0
val peer = MeanStat()
for (x in doubleArrayOf(4.0, 5.0)) peer.update(x)
mean.merge(peer.read())
println(mean.read().mean) // 3.0The compile-checked version of this example lives at com.eignex.kumulant.samples.basicMeanLifecycle, pulled into the rendered API docs via @sample directives on individual classes.
What's in the library
com.eignex.kumulant.core: the Stat and Result interfaces, the Concurrency enum, and the cross-cutting result traits.
com.eignex.kumulant.stat: concrete accumulators grouped by family: summary, quantile, cardinality, sketch, rate, decay, regression (with glm and tree subfamilies), score, calibration, anomaly, event, change, forecast.
com.eignex.kumulant.operation: composable wrappers that change how a stat sees its input (filtering, weighting, windowing, sampling, lagging) or how it reports its output (folding, transforming, projecting).
com.eignex.kumulant.schema: typed, named, wire-portable schemas. Declare a bag of stats once, materialise it into a live StatGroup, encode the schema to wire and rehydrate on the other side.
com.eignex.kumulant.bandit: multi-armed and contextual bandits built on the same Stat/Result foundation.
Conventions
Every concrete stat ships a sibling
StatSpecdata class (ordata objectfor parameter-less stats) carrying configuration only. Specs are@Serializablewith@SerialNamediscriminators matching the Kotlin class names, so polymorphic JSON / CBOR / Protobuf put the same type strings on the wire regardless of format.Every public type has KDoc with a one-sentence summary, then Use cases, Memory, Update, and Concurrency sections. See com.eignex.kumulant.stat.summary.MeanStat for the canonical shape.
Results are immutable, sealed where it makes sense, and structurally comparable via
equals/hashCode. The value that comes out ofread()is the same value that goes intomerge()over the wire.
Packages
Indexless multi-armed bandits. Each round, the bandit picks one of K arms via choose(), the caller observes a reward, and update(arm, value, weight) folds it back into that arm's accumulator. No per-round feature vector; for that, see com.eignex.kumulant.bandit.contextual.
Distribution sampling and stream-hash functions consumed by the rest of the library. The vector / matrix primitives and BLAS-style linear algebra live in the separate koblas library (com.eignex.koblas); this package holds only the sampling and hashing helpers.
The wire-portable weighting strategies for the time-decay and smoothing stats. A DecayWeightingSpec is the serializable counterpart of the runtime weighting in com.eignex.kumulant.stat.decay: it says how fast old observations should fade, and inflates to the live form when the stat is materialized.
The serializable expression AST behind the lambda-bound operations in com.eignex.kumulant.schema. When an operation needs a projection or a predicate that must travel on the wire rather than as a live Kotlin lambda, it carries one of these trees instead. The whole tree is serializable, so a filter or a transform round-trips as data and rebuilds an identical closure on the far side.
The composition operators for the wire specs in com.eignex.kumulant.schema. Each operator is an extension on a modality-specific spec that returns another spec, so compositions stay pure data and round-trip on the wire exactly like the leaf specs they wrap. They are the spec-side mirror of the live operators in com.eignex.kumulant.operation: where the live form wraps a running stat, the form here wraps its spec, and the two produce the same behaviour once materialized.
The wire-portable optimizer strategies that the online linear-model stats take to pick their per-coordinate update rule. An OptimizerSpec is pure configuration: it carries the learning-rate schedule and the optimizer's hyperparameters, and materializes into a live optimizer in com.eignex.kumulant.stat.regression when the stat is built. As with every spec, the concurrency mode is supplied at materialize time rather than stored on the wire.
Turns the pure-data specs of com.eignex.kumulant.schema into live, running accumulators. The specs in the parent package say what to build; this package builds it, groups the results, and fans every update out to the right stat. Nothing here goes on the wire: it is the materialization and grouping layer that sits between a decoded schema and the live stats it stands up.
The spec catalog: a pure-data recipe for every stat in the library. Each spec is a small serializable value carrying only configuration, with no live cells, locks, or concurrency mode. This is the vocabulary that travels on the wire; the live stats it describes are built from it in com.eignex.kumulant.schema.runtime.
Approximate distinct-count estimators. Both entries take a stream of opaque Long keys (com.eignex.kumulant.core.DiscreteStat) and answer "how many distinct keys have I seen?" in bounded memory. The right pre-step is to hash domain-specific keys through com.eignex.kumulant.math.hash64 / com.eignex.kumulant.math.Hasher64 so the input has uniform 64-bit entropy; value.hashCode().toLong() only provides 32 bits and skews the estimators on low-cardinality domains.
Predictive recurrences with multi-cell state. Their results expose forecast(steps) projections, distinguishing them from the decay family's running-moment shape.
Throughput estimators. All three implement com.eignex.kumulant.core.HasRate, so a downstream consumer can pull rate (events per second) or per(duration) through one trait regardless of which underlying estimator produced the snapshot.
Family root for the regression-modality stats and the cross-cutting infrastructure they share. The single-output linear-model family lives in glm, the decision-tree and random-forest family in tree. What sits directly in this package is the small set of stats that don't fit either subfamily, plus the strategy types both rely on.