kumulant

schema.expr

The serializable expression AST used by the input 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 three trees

There are three sealed hierarchies, one per result shape. ScalarExpr evaluates to a single number from an update's value, its paired second component, and the current vector. VectorExpr evaluates to a whole vector and backs the per-feature projection and scaling operations. BoolExpr evaluates to a boolean and drives filter predicates and the guards on conditional nodes. JSON uses operator keys, numeric literals, and named references, such as {"add":[{"ref":"value"},1]}.

Leaves and operators

The scalar leaves are the update value X, the paired second component Y, a vector coordinate V, and a literal Const. Arithmetic composes them with the ordinary operators and with dedicated nodes for the transcendental functions: powers, logs, exponentials, square roots, and absolute value. Comparisons between scalars produce a BoolExpr, and the boolean combinators join several predicates into one.

Sugar nodes

Common shapes have dedicated nodes so a serialized tree stays readable rather than ballooning into nested arithmetic. Standardizing, min-max scaling, centering, and range tests each have their own node, as does the multi-branch Switch and the conditional IfExpr. Vector projection has nodes for dot products, element selection, single-coordinate access, and a fold whose operator picks sum, min, max, or mean.

Filters, transforms, computed weights, and feedback projections use these expression trees through com.eignex.kumulant.schema.ops. The live adapters that evaluate them are internal implementation details.

Portable expression profile

The portable subset matches Klause's expression JSON without sharing an AST. A named scalar is {"ref":"value"}, a constant is a bare number, and an object contains exactly one operator. String expressions are not parsed.

PayloadOperators and operand order
Single bare operandabs, not
Exactly two operandsadd, mul, min, max, pow, and, or, lt, le, eq, ge, gt
Exactly three operandsif: condition, then branch, else branch

The common numeric domain consists of finite integral literals in Klause's signed 32-bit range, with nonnegative integral literal exponents for pow. 0^0 is one. Klause evaluates integer expressions exactly; Kumulant evaluates each node in binary64. Integers through magnitude 2^53 are exactly representable in binary64. Beyond that, rounding can change a value. Both environments encode integral literal values with no fraction part. Kumulant encodes negative zero as -0.0 so its sign survives the round trip.

Kumulant retains every binary grouping and evaluates operands in their written order. For example, adding two small numbers before a large one can produce a different result from adding each small number to the large one in turn. Variadic add, min, max, and, and or are Klause integer extensions; Kumulant rejects those shapes. Klause writes its own chains nested to the left, subtraction as add of mul by -1, and constant multiplication with the constant first, so its DSL output stays portable.

div is binary64 division, including IEEE 754 infinities and NaN from division by zero. Klause's integer operations use truncDiv and truncMod; Kumulant rejects them. Fractional literals, fractional or expression-valued exponents, negative exponents, sub, neg, and transcendental functions are Kumulant extensions. Negative bases with fractional exponents can produce NaN. JSON does not carry non-finite literals under the default settings; an evaluation may still produce a non-finite result.

Kotlin construction uses the spellings Klause's integer expressions share: infix lt, le, eq, ne, ge and gt with a literal on either side, arithmetic operators with literal operands, the receiver helpers x.abs(), x.min(y), x.max(y) and x.pow(n), and ifThenElse(cond, then, otherwise) with literal or expression branches. Literals are binary64 here, so / builds div rather than Klause's truncating division. ne builds not over eq and holds when either side is NaN. A literal on the left stays the left operand, so 0.0 lt X and X gt 0.0 agree in value but not in encoding.

The shared portable-expression/conformance.json JVM resource is a byte-identical copy of Klause's fixture. Each case records accepted shapes, canonical output, evaluation results for both numeric environments, and explicit rejections.

Explicit reference bindings

Ref names have no implicit relationship to an observation field or a feedback stat. Bind the fragment to its sources before using it in a stat:

val predicate = (Ref("value") ge 0.0).bind(mapOf("value" to X))
val statistic = Mean.filter(predicate)

The predicate serializes with its scope, so a consumer can decode and materialize the stat directly:

{"bind":{"expr":{"ge":[{"ref":"value"},0]},"bindings":{"value":{"input":"x"}}}}
Kotlin sourceKumulant extension JSONMeaning
X{"input":"x"}Primary scalar input, or the current element in an element-wise operation
Y{"input":"y"}Paired secondary input or regression response
V{"element":0}Explicit vector coordinate
Center, Scale{"feedback":"center"}, {"feedback":"scale"}Current primary's center or scale
Low, High{"feedback":"low"}, {"feedback":"high"}Current primary's minimum or maximum
VIndex{"feedback":"index"}Coordinate of an element-wise feedback primary

Bindings can refer to other named bindings or to scalar expressions. Binding checks all branches eagerly, rejects unresolved names and cycles, and preserves grouping. Nested binding scopes are independent. Feedback sources retain their primary-result requirements, and vector coordinates retain bounds checks. A name such as x remains a user-defined reference even when a built-in input source occurs in the same tree. Raw fragments can be decoded for a host to bind; statistical recipes with unresolved references are rejected when materialized. Materializing also rejects an expression that reads an input its context never supplies: a vector coordinate, fold or dot product outside vector and regression operations, VIndex outside element-wise feedback, or a feedback source outside feedback or over a primary whose result lacks the trait it reads.

Embedding and JSON Schema

com.eignex.kumulant.schema.KumulantJson.serializers exposes every vocabulary for composition with a host library's own compact serializers. com.eignex.kumulant.schema.KumulantJson.catalog describes nested stats, expressions, and their custom payload layouts for JSON Schema generation. Decoding needs no catalog: the compact serializers are attached to the types, so a host model decodes with plain kotlinx serialization:

import com.eignex.skema.SchemaJson
import com.eignex.skema.serialization.toJsonSchema

val model = SchemaJson.decodeFromString(Model.serializer(), payload)
val schema = Model.serializer().toJsonSchema(KumulantJson.catalog)

A host with its own compact vocabulary can build a JsonNodeCatalog whose supplier returns its serializers plus KumulantJson.serializers.

Types

Link copied to clipboard
@Serializable(with = BoolExprSerializer::class)
sealed interface BoolExpr

Wire-serialisable AST for boolean predicates over the same input environment as ScalarExpr. Consumed by every spec-side filter operator and by IfExpr's condition slot.

Link copied to clipboard
@Serializable
@SerialName(value = "Center")
data object Center : ScalarExpr

Reads the center field of the feedback primary's snapshot. Requires the primary's result to implement HasCenterScale; raises IllegalStateException when evaluated without a primary, or when the primary's result does not expose a center.

Link copied to clipboard
@Serializable
@SerialName(value = "Const")
data class Const(val v: Double) : ScalarExpr

Wire spec for a constant scalar.

Link copied to clipboard
@Serializable
@SerialName(value = "High")
data object High : ScalarExpr

Reads the max field of the feedback primary's snapshot. Requires the primary's result to implement HasMinMax; raises IllegalStateException when evaluated without such a primary.

Link copied to clipboard
@Serializable
@SerialName(value = "IfExpr")
data class IfExpr(val cond: BoolExpr, val then: ScalarExpr, val otherwise: ScalarExpr) : ScalarExpr

Wire spec for a ternary if (cond) then else otherwise.

Link copied to clipboard
@Serializable
@SerialName(value = "In")
data class In(val of: ScalarExpr, val values: List<Double>) : BoolExpr

Membership test: of in values (exact equality). Use it to flatten chains of Eq predicates joined by Or; e.g. VIndex In listOf(0.0, 3.0).

Link copied to clipboard
@Serializable
@SerialName(value = "Low")
data object Low : ScalarExpr

Reads the min field of the feedback primary's snapshot. Requires the primary's result to implement HasMinMax; raises IllegalStateException when evaluated without such a primary.

Link copied to clipboard
@Serializable
@SerialName(value = "MinMax")
data class MinMax(val targetLow: Double = DEFAULT_TARGET_LOW, val targetHigh: Double = DEFAULT_TARGET_HIGH) : ScalarExpr

Min-max projection mapping X from [primary.min, primary.max] to [targetLow, targetHigh], emitting targetLow while the running range is still degenerate. Reusable AST sugar for the min-max-scaler pattern; requires a HasMinMax primary.

Link copied to clipboard
@Serializable
@SerialName(value = "Ref")
data class Ref(val name: String) : ScalarExpr

Named scalar reference. Bind it explicitly before evaluating or materializing a statistical recipe.

Link copied to clipboard
@Serializable(with = ScalarExprSerializer::class)
sealed interface ScalarExpr

Wire-serialisable scalar expression over per-update inputs:

Link copied to clipboard
@Serializable
@SerialName(value = "Scale")
data object Scale : ScalarExpr

Reads the scale field of the feedback primary's snapshot. Requires the primary's result to implement HasCenterScale; raises IllegalStateException when evaluated without a primary, or when the primary's result does not expose a scale.

Link copied to clipboard
@Serializable
@SerialName(value = "Standardize")
data object Standardize : ScalarExpr

Z-score projection: (X - Center) / Scale, emitting 0 when Scale is still zero. Reusable AST sugar for the standard-scaler pattern; requires a HasCenterScale primary.

Link copied to clipboard
@Serializable
@SerialName(value = "Switch")
data class Switch(val on: ScalarExpr, val cases: List<SwitchCase>, val otherwise: ScalarExpr) : ScalarExpr

Multi-way branch on a scalar key. Replaces nested IfExpr cascades. The first case whose SwitchCase.value equals on.eval(...) exactly wins; if none match, otherwise is returned.

Link copied to clipboard
@Serializable
@SerialName(value = "SwitchCase")
data class SwitchCase(val value: Double, val then: ScalarExpr)

One case of a Switch expression: when on evaluates to value (exact equality), the result is then. Exact double equality is fine for integer-valued keys like VIndex; not recommended for general continuous comparisons.

Link copied to clipboard
@Serializable
@SerialName(value = "V")
data class V(val index: Int) : ScalarExpr

v[index] - out-of-bounds throws at eval time.

Link copied to clipboard
@Serializable(with = VectorExprSerializer::class)
sealed interface VectorExpr

Wire-serialisable AST for vector-valued expressions over the same input environment as ScalarExpr / BoolExpr. Used by the spec-side transformVector(VectorExpr) operator when the output is a fresh vector rather than a per-element transform.

Link copied to clipboard
@Serializable
enum VFoldOp : Enum<VFoldOp>

Reduction over the entire vector input. Distinct from element-level arithmetic (Add, Mul) - this collapses a DoubleArray of arbitrary length to a single scalar via the chosen operation.

Link copied to clipboard
@Serializable
@SerialName(value = "VIndex")
data object VIndex : ScalarExpr

In element-wise feedback contexts (vector / regression / paired), returns the coordinate index of the currently evaluating element as a Double. Outside such contexts (primary is not an IndexedResult), raises IllegalStateException.

Link copied to clipboard
@Serializable
@SerialName(value = "X")
data object X : ScalarExpr

Refers to the primary scalar input x.

Link copied to clipboard
@Serializable
@SerialName(value = "Y")
data object Y : ScalarExpr

Refers to the secondary scalar input y (paired stats only).

Functions

Link copied to clipboard

Absolute value of this expression.

Link copied to clipboard

True when every input to the update is finite, whichever modality the stat is.

Link copied to clipboard
infix fun BoolExpr.and(rhs: BoolExpr): BoolExpr

Short-circuiting conjunction.

Link copied to clipboard

Bind every scalar reference, including references in branches that may not execute.

Bind every reference eagerly, preserving the tree's grouping and retaining the scope on the wire.

Bind every scalar reference in a vector projection. Nested binding scopes resolve independently.

Link copied to clipboard
operator fun ScalarExpr.div(rhs: ScalarExpr): ScalarExpr

Build Div of two expressions.

operator fun ScalarExpr.div(rhs: Double): ScalarExpr

Divide this expression by a literal rhs.

operator fun Double.div(rhs: ScalarExpr): ScalarExpr

Divide a literal receiver by an expression.

Link copied to clipboard
infix fun ScalarExpr.eq(rhs: ScalarExpr): BoolExpr

Exact equality (no tolerance).

infix fun ScalarExpr.eq(rhs: Double): BoolExpr

Exact equality against a literal (no tolerance).

infix fun Double.eq(rhs: ScalarExpr): BoolExpr

Exact equality with a literal left operand (no tolerance).

Link copied to clipboard
fun BoolExpr.eval(x: Double = 0.0, y: Double = 0.0, v: Vector, primary: Result? = null): Boolean

Evaluate this boolean expression against a borrowed KoBLAS vector without materialising it.

fun ScalarExpr.eval(x: Double = 0.0, y: Double = 0.0, v: Vector, primary: Result? = null): Double

Evaluate this scalar expression against a borrowed KoBLAS vector without materialising it.

fun VectorExpr.eval(x: Double = 0.0, y: Double = 0.0, v: Vector, primary: Result? = null): DoubleArray

Evaluate this vector expression against a borrowed KoBLAS vector, allocating only its owned output.

Link copied to clipboard

e raised to this expression. Overflows to +Infinity well before Double runs out of range.

Link copied to clipboard
infix fun ScalarExpr.ge(rhs: ScalarExpr): BoolExpr

Greater-or-equal comparison.

infix fun ScalarExpr.ge(rhs: Double): BoolExpr

Greater-or-equal against a literal.

infix fun Double.ge(rhs: ScalarExpr): BoolExpr

Greater-or-equal with a literal left operand.

Link copied to clipboard
infix fun ScalarExpr.gt(rhs: ScalarExpr): BoolExpr

Strictly-greater-than comparison.

infix fun ScalarExpr.gt(rhs: Double): BoolExpr

Strictly-greater-than against a literal.

infix fun Double.gt(rhs: ScalarExpr): BoolExpr

Strictly-greater-than with a literal left operand.

Link copied to clipboard
fun ifThenElse(cond: BoolExpr, then: ScalarExpr, otherwise: ScalarExpr): ScalarExpr

then when cond holds, else otherwise; only the selected branch is evaluated. Builds an IfExpr.

fun ifThenElse(cond: BoolExpr, then: ScalarExpr, otherwise: Double): ScalarExpr

then when cond holds, else otherwise, with a literal otherwise branch.

fun ifThenElse(cond: BoolExpr, then: Double, otherwise: ScalarExpr): ScalarExpr

then when cond holds, else otherwise, with a literal then branch.

fun ifThenElse(cond: BoolExpr, then: Double, otherwise: Double): ScalarExpr

then when cond holds, else otherwise, with literal branches.

Link copied to clipboard

True when this expression falls within [min, max], both ends inclusive.

Link copied to clipboard

True when this expression evaluates to a finite number: not NaN, and not an infinity.

Link copied to clipboard

True when this expression evaluates to NaN.

Link copied to clipboard
infix fun ScalarExpr.le(rhs: ScalarExpr): BoolExpr

Less-or-equal comparison.

infix fun ScalarExpr.le(rhs: Double): BoolExpr

Less-or-equal against a literal.

infix fun Double.le(rhs: ScalarExpr): BoolExpr

Less-or-equal with a literal left operand.

Link copied to clipboard

Natural logarithm of this expression. ln(0) is -Infinity and a negative input gives NaN.

Link copied to clipboard
infix fun ScalarExpr.lt(rhs: ScalarExpr): BoolExpr

Strictly-less-than comparison.

infix fun ScalarExpr.lt(rhs: Double): BoolExpr

Strictly-less-than against a literal.

infix fun Double.lt(rhs: ScalarExpr): BoolExpr

Strictly-less-than with a literal left operand.

Link copied to clipboard

The larger of this expression and other.

The larger of this expression and a constant other.

Link copied to clipboard

The smaller of this expression and other.

The smaller of this expression and a constant other.

Link copied to clipboard

Build Sub of two expressions.

operator fun ScalarExpr.minus(rhs: Double): ScalarExpr

Subtract a literal rhs from this expression.

operator fun Double.minus(rhs: ScalarExpr): ScalarExpr

Subtract an expression from a literal receiver.

Link copied to clipboard
infix fun ScalarExpr.ne(rhs: ScalarExpr): BoolExpr

Exact inequality, the negation of eq under IEEE 754: true when either side is NaN, and false for 0.0 against -0.0. It builds !(this eq rhs), which encodes with the portable not and eq.

infix fun ScalarExpr.ne(rhs: Double): BoolExpr

Exact inequality against a literal; see ne.

infix fun Double.ne(rhs: ScalarExpr): BoolExpr

Exact inequality with a literal left operand; see ne.

Link copied to clipboard
operator fun BoolExpr.not(): BoolExpr

Logical negation.

Link copied to clipboard
infix fun BoolExpr.or(rhs: BoolExpr): BoolExpr

Short-circuiting disjunction.

Link copied to clipboard
operator fun ScalarExpr.plus(rhs: ScalarExpr): ScalarExpr

Build Add of two expressions.

operator fun ScalarExpr.plus(rhs: Double): ScalarExpr

Add a literal rhs to this expression.

operator fun Double.plus(rhs: ScalarExpr): ScalarExpr

Add an expression to a literal receiver.

Link copied to clipboard

This expression raised to exponent.

This expression raised to a constant exponent.

fun ScalarExpr.pow(exponent: Int): ScalarExpr

This expression raised to an integer exponent, which binary64 represents exactly.

Link copied to clipboard

Square root of this expression. A negative input gives NaN.

Link copied to clipboard

Build Mul of two expressions.

operator fun ScalarExpr.times(rhs: Double): ScalarExpr

Multiply this expression by a literal rhs.

operator fun Double.times(rhs: ScalarExpr): ScalarExpr

Multiply a literal receiver by an expression.

Link copied to clipboard

Unary minus: wraps in Neg.

Link copied to clipboard
fun vDot(weights: List<Double>): ScalarExpr

Weighted sum of the feature vector. weights must match the vector length at evaluation time.

fun vDot(vararg weights: Double): ScalarExpr

Weighted sum of the feature vector, varargs form.

Link copied to clipboard

Build an output vector by evaluating each expression in order; output length is exprs.size.

fun vectorOf(vararg exprs: ScalarExpr): VectorExpr

Build an output vector from expressions in order, varargs form.

Link copied to clipboard

Reduce the whole feature vector to one scalar with op.