Combo optimizes typed Klause configurations under constraints. The library separates algorithms by how they consume observations:
| Package | Data model | Algorithms |
|---|---|---|
com.eignex.combo.bandit |
Streaming updates to model state | Online random forest, linear bandit, univariate bandits |
com.eignex.combo.bo |
Retained evaluation history and batch refitting | SMAC-style expected improvement, bootstrap forest Thompson sampling, GP/EI |
com.eignex.combo.shared |
Common learner interfaces, feature encoding, feedback, and feasible search | Used by both families |
All public packages use the com.eignex.combo prefix. Common learner contracts and
numeric feature encoders live in com.eignex.combo.shared.
Streaming learners keep sufficient statistics and incremental tree state. History-based optimizers keep the evaluations themselves and can reconsider every observation when rebuilding a model. This data-flow distinction applies whether an application targets cumulative reward or the best final configuration.
Both families use the same decision-space schema and contextual variables. Klause owns constraints and feasible witnesses; Kumulant supplies streaming models, statistical summaries, posterior sampling, and shared tree snapshots. Combo owns full-history model fitting and acquisition functions, including Gaussian and lognormal expected improvement, and their optimization over feasible configurations.
Using it requires three steps:
- Declare the decision space with its variables, sub-spaces, and constraints.
- Pick a streaming learner or history-based optimizer and wire it to the compiled space.
- Loop choose → serve the configuration → update with the observed reward.
The decision space describes what the optimizer can vary. In the example below the bandit picks an LLM agent setup (model, temperature, prompt style, and tools) per request, conditioned on the task type and user tier, and learns from whether the run succeeded.
{
"decisionSpace": {
"name": "AgentPolicy",
"context": {
"taskType": { "type": "nominal", "labels": ["coding", "writing", "research", "casual"] },
"userTier": { "type": "nominal", "labels": ["free", "pro", "enterprise"] }
},
"variables": {
"model": { "type": "nominal", "labels": ["haiku", "sonnet", "opus"] },
"temperature": { "type": "float", "min": 0.0, "max": 1.0 },
"maxTokens": { "type": "int", "min": 256, "max": 8192 },
"promptStyle": { "type": "nominal", "labels": ["terse", "detailed", "chainOfThought"] },
"tools": {
"type": "multiple",
"labels": ["web_search", "code_exec", "file_read", "bash"]
}
},
"constraints": {
"freeTierCantUseOpus": "userTier == \"free\" implies model != \"opus\"",
"codeExecNeedsBash": "tools.contains(\"code_exec\") implies tools.contains(\"bash\")",
"codingTasksNeedTools": "taskType == \"coding\" implies (tools.contains(\"code_exec\") or tools.contains(\"file_read\"))",
"casualKeepsItCheap": "taskType == \"casual\" implies (maxTokens <= 1024 and model != \"opus\")"
}
}
}Every choose call returns a feasible configuration. Variables are typed: bool, int, float, nominal (pick one), or multiple (pick any subset).
The flagship is the random forest bandit: an online, constraint-aware contextual bandit built on Hoeffding-bound decision trees, each carrying a Bayesian posterior at every leaf. At decision time it combines candidate sampling with the original cross-tree decision algorithm, reusing feasible witnesses and asking Klause to repair or backtrack only when necessary. Context values stay fixed throughout the search.
Exploration is per-leaf Thompson sampling on a Bayesian posterior over the leaf reward, with online bagging diversifying the trees as updates stream in and mtry restricting each tree's split candidates to a random subspace. Bools, integers, nominals, multi-selects, and floats all carry their own typed split kinds, and every constraint is honored at every choose call by construction, with no rejection-sample fallback that might quietly violate the model.
The default builder wires the forest to a hybrid Klause search:
val space = MyDecisionSpace().compileSpace()
val bandit = RandomForestBandit.build(
space = space,
nbrTrees = 25,
candidateBatchSize = 8,
poolCapacity = 64,
randomSeed = 42,
)Kumulant supplies the growing trees and Normal-Gamma Thompson policy. Each round samples a batch of feasible candidates and scores their merged leaf posteriors. Candidates reaching the same leaf tuple share one posterior draw for that round; adding correlated trees does not multiply the effective observation count.
The original cross-tree descent then pins Boolean and nominal decisions. Compatible cached witnesses answer those checks without a solver call. A missing witness triggers a local-search repair seeded from a nearby solution, followed by bounded backtracking when repair is inconclusive. The best feasible candidate is retained even if a preferred branch exhausts its search budget. Integer and float splits also affect candidate scoring. Continuous proposals include solver-produced boundary points and interior points drawn in the declared parameter geometry. Klause independently checks each interior proposal against the original problem; a failed proposal retains its feasible solver seed.
Defaults are 2,000 local-search flips and 10,000 backtracking decisions per request;
pass localSearch = LocalSearchParams(...) and backtrack = BacktrackParams(...)
to tune them. A search budget miss is Unknown, not an infeasibility proof.
HybridDescentSession.lastResult, poolHits, proposalCalls, and backtrackCalls
are available through bandit.descentSession for inspecting the search. The existing
callback-based builder remains available for other Klause engines and strategies.
The forest's regression is Kumulant's RandomForestRegressionStat: it owns the
trees, seed streams, online bagging, and immutable statistical snapshots. Combo's
TreeVectorLayout supplies a read-only numeric view and maps learned predicates
back to Klause expressions. Numeric coordinates preserve declared units; expression
predicates use Boolean feature slots. Both linear and forest bandits use
com.eignex.combo.shared for training, prediction, reward/error tracking, and model
snapshot handling, witness caching, and Klause session adapters. The forest uses
the shared hybrid session; the linear bandit uses its supplied objective optimizer.
Tree and forest exportData/importData transfer actual Kumulant snapshots;
import merges evidence and requires the same ordered predicate layout.
Then it's a choose → serve → update loop:
val arm = bandit.choose() ?: return // no feasible witness found within the budgets
serve(arm) // hand the configuration to the user
val reward = observe() // measure clicks, sales, latency, …
bandit.update(arm, reward)Combo extends Klause's variables with context metadata. Context variables use the same types and constraints as decision variables; the caller supplies their values, and each choose call pins them through Klause assumptions. Floats retain Klause's semantics and decoded values. Combo adds no grid; newly proposed assignments require an independent Klause feasibility check before use.
Use bandit.choose(context { ... }), bandit.update(arm, context, reward), and
bandit.optimal(context) for contextual optimization. The same context remains a
hard constraint during candidate generation, branch repair, and solver fallback.
End-to-end examples covering cascade-with-backtrack, Thompson sampling on linear models, and context-conditioned arms live under src/jvmTest/kotlin/com/eignex/combo.
Numeric variables may declare a scale hint while retaining Klause's original types, bounds, constraints, and returned values:
import com.eignex.combo.decisions.Scale
val learningRate by floatVar(1e-6, 1.0, scale = Scale.Log)
val workers by intVar(1, 128, scale = Scale.Log)
val dropout by floatVar(0.0, 1.0) // Scale.Linear is the default.Linear proposals allocate equal attention to additive intervals. Log proposals
allocate equal attention to ratios and require positive lower bounds. ReverseLog
uses -log(1 + max - value) to concentrate proposals near the upper bound.
Scale hints also apply to contextInt and optionalContextInt, and survive nested
DecisionSpaceDef serialization as local-name scales metadata.
Vector feature encoding applies log/reverse-log transforms to numeric values and interactions, leaving the default linear features in their original units. Online trees retain source predicates and place default thresholds in the declared geometry. A linear bandit with nonlinear scale hints ranks shared feasible witness candidates; it cannot translate those feature weights into a linear objective in source units. Proposal generation remains bounded, and constrained spaces may return short batches.
com.eignex.combo.shared.WitnessSearch is the shared candidate-generation boundary:
val candidates = search.witnesses(
WitnessRequest(
count = 32,
assumptions = space.assumptionsFor(context),
exclude = pendingAssignments,
maxAttempts = 128,
),
random,
)WitnessBatch.witnesses contains distinct feasible assignments. termination is
CountReached, BudgetExhausted, or Infeasible; a short batch is not an UNSAT proof.
Excluded assignments are compared by their coordinates, ignoring proof metadata.
External anchors are independently verified before cache reuse. maxAttempts
bounds proposal and fallback calls together; each engine keeps its configured local
search/backtracking budget. A zero-attempt request can reuse verified cached witnesses.
The default HybridDescentSession implements this interface alongside incremental
streaming-tree descent. Optimizers can accept other WitnessSearch implementations.
import com.eignex.combo.bo.InMemoryObservationStore
import com.eignex.combo.bo.RandomForestOptimizer
val history = InMemoryObservationStore()
val optimizer = RandomForestOptimizer.smac(space, store = history)
// Alternative: RandomForestOptimizer.bootstrapThompson(space, store = history)
val configuration = optimizer.chooseOrThrow()
optimizer.update(configuration, evaluate(configuration))
val recommendation = optimizer.optimalOrThrow()Both algorithms use Combo's batch CART forest fitter, which finds numeric split thresholds from the observed data. Continuous values retain their declared units.
- SMAC-style expected improvement refits a forest when the store revision changes. It models the distribution of tree predictions as Gaussian and maximizes expected improvement. This implements the forest/EI pattern described in SMAC, using Klause's feasible candidate search. The optional intensifier below supplies paired racing; censored trials and multi-fidelity scheduling remain unimplemented.
- Bootstrap Thompson sampling resamples the full evaluation history with replacement and fits a fresh random forest for each exploratory choice. The sampled model stays fixed throughout that choice. This follows the bootstrap-model sampling approach in TreeBootstrap.
ForestOptimizerConfig controls the batch forest, initial random observations,
random exploration probability, and EI margin. optimal always uses the mean of a
forest fitted to the full history. Contextual choose/update overloads match those of
the streaming forest; observations across contexts train a shared response model.
Historical configurations also enter candidate ranking after Klause checks their
feasibility, preserving observed interior points. Fresh continuous candidates use
scaled, independently checked interior proposals alongside boundary seeds. This is a
bounded candidate search, with no guarantee of global acquisition maximization.
ObservationStore is the storage boundary. Its append operation advances a revision,
and snapshot() returns a stable, ordered history. The default in-memory store retains
all observations without eviction. A persistent implementation can use the same
interface. Appends must atomically store the row and advance the revision; failed
appends leave both unchanged. Stores belong to one decision-space schema. train records off-policy
evaluations without reward/error bookkeeping; zero weight is a no-op and negative
weights are rejected. Import/export transfers schema-bound history snapshots.
Prediction learners accept testScore and trainScore as Kumulant PairedStat
scorers. They receive (prediction, observed reward) before and after learning,
respectively, with the observation weight. Use MaeLossStat() for mean absolute
error or MseLossStat() for mean squared error; callers can provide other paired
scorers appropriate to their model. Off-policy train leaves reward and score
tracking untouched. Configuring these scorers can trigger additional fits during updates.
The former testAbsError / trainAbsError scalar sinks are replaced by these paired
scorers. Replace an absolute-error MeanStat() sink with MaeLossStat() and read its
weighted mean and total weight. For a former SumStat() sink, the weighted total
is result.mean * result.totalWeights. Loss calculation and aggregation belong to Kumulant.
For parallel evaluations, the BO interface follows Vizier's batch suggestion and pending-trial pattern (Vizier API):
val batch = optimizer.suggest(count = 8, context = context)
for (trial in batch) {
// Dispatch trial.sample and retain trial.id for the eventual result.
}
optimizer.complete(batch[0].id, reward = result, weight = 1.0)
optimizer.fail(batch[1].id, reason = "evaluation failed")
optimizer.cancel(batch[2].id)suggest returns a TrialBatch with a termination status and reserves its points
across later requests. It may return fewer trials than requested. Completed results
may arrive in any order and are recorded once using the trial's captured context.
Pending, failed, and cancelled trials never contribute invented rewards. Failure and
cancellation release a reservation for future suggestions. Zero/nonfinite weights
and repeated or unknown trial completions are rejected. A trial is closed immediately
after its observation commits, even if a later metrics callback fails.
The default batch policy selects sequentially with pending assignments excluded.
SequentialBatchPolicy(FantasyPolicy.ConstantLiar(value)) and
SequentialBatchPolicy(FantasyPolicy.PosteriorMean) add temporary outcomes when
constructing a batch. Conditionable surrogates can condition on those observations;
other surrogates refit temporary datasets. Neither path writes fantasies into the
observation store. Bootstrap Thompson fits a fresh history-bootstrap model per slot.
Opt-in joint batch acquisition is described below. Streaming learners retain choose/update.
Bayesian optimizers exclude completed assignments by default for deterministic outcomes.
RepeatPolicy.NoisyOnly permits replicates when OutcomeSpec.noise is Noisy;
RepeatPolicy.Allow also supports deliberate deterministic repeats and legacy
choose/update loops over finite spaces. RepeatPolicy.Never excludes all completed
points. An exhausted suggestion search returns a partial/empty batch with its budget
status rather than silently repeating an evaluation. Recommendations can still return
evaluated points. Forest configuration exposes this policy as repeatPolicy.
Study owns observations, IDs, reservations, and completion independently of the
suggestion algorithm. A StudySnapshot captures history and pending evaluations
once per batch. All proposals are validated before their reservations commit, and
stale or foreign snapshots are rejected. trials and pendingTrials are immutable
snapshots. exportData/importData transfers history, reservations, and the algorithm
round counter. Persisting only an ObservationStore does not preserve reservations.
Calls remain single-threaded even when external evaluations run concurrently.
For low-dimensional continuous spaces, use the same optimization loop with an exact Gaussian-process surrogate:
import com.eignex.combo.bo.GaussianProcessSurrogate
import com.eignex.combo.bo.model.GaussianProcessConfig
val optimizer = BayesianOptimizer(
projection = SearchTransform(space),
surrogate = GaussianProcessSurrogate(GaussianProcessConfig(learnNoise = true)),
initialDesign = LatinHypercubeInitialDesign(space, evaluationBudget = 100, seed = 42),
batchPolicy = SequentialBatchPolicy(FantasyPolicy.PosteriorMean),
outcome = OutcomeSpec(maximize = false, noise = ObservationNoise.Noisy, standardize = true),
randomSeed = 42,
)
val trial = optimizer.suggest(1)[0]
optimizer.complete(trial.id, cost, noiseVariance = measurementVariance)Combo owns the batch GP and CART models under com.eignex.combo.bo.model; Kumulant
retains online learning, statistical summaries, shared tree snapshots, and Cholesky
factorization. Koblas supplies triangular solves and matrix/vector products.
The GP follows Gaussian Processes for Machine Learning, chapter 2.
The default kernel is Matérn five halves; GaussianKernel.Rbf selects squared exponential
covariance. Length scales operate in encoded coordinates, so SearchTransform supplies
normalized numeric and declared log geometry. Bounded coordinate search maximizes the
marginal likelihood over length scales and signal variance; learnNoise also fits noise
for rows whose variance is unspecified. Supplied row variances remain fixed. Set
hyperparameterTrials = 0 to fix kernel/noise parameters. Mean and variance configuration
uses training-response units, after any requested standardization or response transform.
Predictions expose latent function uncertainty conditional on fitted parameters, excluding
observation noise and uncertainty about the fitted parameters themselves. Numerical jitter
is separate from noise and reported on FittedGaussianProcess. Unit statistical weights
are required: explicit variances and independent replicate rows express measurement evidence.
Exact fitting costs cubic time and quadratic memory in history size. Mixed-type kernels,
censored outcomes, and multi-fidelity scheduling are outside this first GP implementation.
predictJoint supplies latent covariance when supported and returns null for marginal-only
models; predictLogJoint preserves Gaussian log-response covariance through transforms.
Conditioning freezes fitted parameters and re-factorizes augmented data. Function draws
sample latent points jointly and cache their values across repeated, reordered, and later
queries. First visits to unseen coordinates determine seeded random assignment order;
a draw retains those points and its conditioning cost grows with them. Fantasies remain
outside recorded history. Joint acquisition uses joint moments directly rather than growing a function draw.
See the GP comparison report for frozen-history model diagnostics, acquisition-search checks, and equal-budget GP/EI, forest/EI, and random-search experiments. These comparisons use continuous synthetic spaces.
JointBatchPolicy optimizes a set of configurations using their joint latent posterior.
JointExpectedImprovement (q-EI) uses a fixed incumbent; JointNoisyExpectedImprovement
(q-NEI) jointly samples completed configurations and candidates, integrating uncertainty
about the latent incumbent. Pending points enter that sampled baseline, so only the
additional improvement from the new batch contributes. Neither acquisition invents
pending outcomes or adds observation noise to function draws. This follows the joint
Monte Carlo formulation in BoTorch's acquisition interface.
val projection = SearchTransform(space)
val jointSearch = JointPoolSearch(projection, witnesses, candidateCount = 32)
val optimizer = BayesianOptimizer(
projection,
GaussianProcessSurrogate(),
initialDesign = LatinHypercubeInitialDesign(space, evaluationBudget = 100, seed = 42),
batchPolicy = JointBatchPolicy(JointNoisyExpectedImprovement(samples = 256), jointSearch),
outcome = OutcomeSpec(maximize = false, noise = ObservationNoise.Noisy, standardize = true),
randomSeed = 42,
)
val batch = optimizer.suggest(4)
// Complete each reserved trial once; results can arrive in any order.One frozen snapshot and fitted model serve the entire request. Baseline and pending coordinates respect the requested hard context, while all pending assignments remain excluded from proposals. Study validates and reserves the full batch together. Initial exploration and contexts without a completed baseline use ordinary feasible proposals. Joint moments are required after warmup; marginal-only forest models fail explicitly. Bootstrap Thompson sampling is a separate incompatible strategy.
JointPoolSearch generates a feasible pool, starts several random sets, and replaces
one member at a time while scoring the entire set. It enforces distinct assignments,
context, exclusions, and Klause feasibility, and can return partial batches on exhaustion.
It searches within its pool and has no global-optimum guarantee. The suggestion trace
counts whole-set acquisition evaluations for joint search; these are more expensive than
single-point scores and should not be compared as equal computational units.
Both acquisitions approximate joint moments as Gaussian, or sample Gaussian log-response
moments before exponentiating for a log-transformed outcome. Fixed antithetic normal
samples make each bound objective reproducible; candidate row permutations and duplicates
do not change its value. Kumulant supplies normal draws, summaries, and Cholesky factors;
Koblas supplies matrix products. Near-singular joint covariance receives bounded relative
jitter up to 1e-8 only after factorization fails. Exactly deterministic coordinates remain
deterministic. Materially indefinite matrices fail rather than dropping correlations.
This first version re-factors the joint covariance for each candidate set. q-NEI includes the full same-context baseline, with no pruning or cached baseline factor, so large histories are expensive. Ordinary Monte Carlo EI can also develop zero-improvement plateaus; stable log-improvement acquisition remains future work. q-NEI is an improvement criterion, not an information-gain policy for choosing repeated measurements. These policies remain opt-in; see the paired joint-batch experiment for results and limits.
The compact smac factory retains its original signed-response defaults. For positive
costs, the budget-aware profile combines ten randomized all-feature batch trees,
log-response EI, a continuous Latin hypercube, 20% independent exploration, and ten
adaptive local-search starts:
val optimizer = RandomForestOptimizer.smacProfile(
space, evaluationBudget = 100, randomSeed = 42,
)
// Minimize strictly positive costs. For signed responses use responseScale = ResponseScale.Identity.
val trial = optimizer.suggest(1)[0]
optimizer.complete(trial.id, cost)Here evaluationBudget sizes the initial design; the caller still controls when to
stop ordinary suggestions. LatinHypercubeInitialDesign allocates ten configurations
per independent dimension, capped at 25% of the intended budget. Hard contexts are
pinned, categories select one label, and numeric draws follow the declared scale.
Constrained draws may be rejected and replaced by feasible witnesses, so coverage is
not a uniform constrained Latin hypercube. No variable grid is introduced. Plan slots
count issued configurations, including pending and unsuccessful ones; repeated
measurements of a configuration do not consume extra design slots. An oversized
initial batch finishes with independent feasible random proposals. Exploration bypasses
surrogate fitting and acquisition optimization entirely.
For noisy measurements, a separate RacingOptimizer owns external worker requests:
val base = RandomForestOptimizer.smacProfile(
space, evaluationBudget = 100, randomSeed = 42,
noise = ObservationNoise.Noisy, repeatPolicy = RepeatPolicy.Never,
)
val race = RacingOptimizer(
base,
keys = listOf(11, 23, 37).map { EvaluationKey(instance = "validation", seed = it) },
evaluationBudget = 100,
)
for (task in race.ask(4).tasks) {
// Dispatch task.sample with task.key.instance, seed, and optional fixed budget.
// Later: race.complete(task.id, reward = measuredCost, cost = workerCost).
}
val confirmed = race.incumbent // Null until the first configuration finishes its plan.The first incumbent completes the entire measurement plan. A challenger begins with
one matched instance/seed measurement, then doubles its stage while competitive.
Each challenger has a reproducibly shuffled measurement order to avoid favoring the
same first seed or instance in every early comparison.
Worse paired means trigger early rejection; a fully measured winner becomes the
incumbent. RacingPolicy controls stage growth, rejection slack, concurrency, and
retries. initialIncumbentMeasurements allows progressive incumbent evaluation; missing
incumbent keys are scheduled before their paired comparisons settle. Setting
rejectionStandardErrors requires a mean disadvantage exceeding that descriptive
standard-error allowance. These repeated comparisons have no sequential significance
guarantee, and a single unknown-noise difference cannot justify guarded early rejection.
Each successful worker result trains the surrogate once in raw response units;
stage means never become additional observations. fail records unsuccessful cost
without a response, and cancel releases the key. Already-issued workers may finish
in any order. Attempt limits include failures and cancellations, and short batches
explicitly distinguish waiting, budget exhaustion, and unsuccessful configuration search.
Initial-design budgets count configurations, while racing spends multiple calls on each.
Use LatinHypercubeInitialDesign(space, evaluationBudget, measurementsPerConfiguration = 3.0)
for a three-measurement allowance; this divides the initial configuration cap by three.
The allowance sizes the design and does not enforce an external evaluation budget.
smacProfile accepts an initialDesign override for composing these policies.
When search ends, race.beginConfirmation(freshKeys, candidates = 3) freezes the best
observed configurations and compares them on fresh instance/seed keys. Call after
outstanding workers finish and reserve enough remaining budget for every finalist/key.
The final recommendation uses only those fresh outcomes; they also train the model
once and consume the ordinary external budget. Rejected search configurations can
return to the shortlist. Selection never adapts to confirmation labels. The frozen
shortlist and pending fresh-key workers survive export/import. Permanent failures
exhaust their configured retries without inventing responses; if all finalists fail,
the search incumbent remains the fallback.
EvaluationStore persists closed worker outcomes separately from ObservationStore.
Use race.exportData()/importData() with a fresh compatible race to retain pending
workers, IDs, stages, and the paired plan. Restore the same policies and hard context.
Calls remain single-threaded. Independent storage backends are not a distributed
transaction: after a committed model update, a failed evaluation-store append leaves
the worker closed to prevent duplicate training; the exported race state retains its
outcome for recovery. Instances and seeds identify paired measurements; they do not
supply learned instance features. Uneven instance coverage can therefore affect the
pooled surrogate. All keys use one fixed fidelity; budget promotions, censored costs,
and automatic multi-fidelity scheduling are future work.
The refinement study separates frozen-model diagnostics, acquisition search budgets, initial measurement allocation, guarded racing, and fresh-key confirmation. Optional leaf-mean variance can overlap tree disagreement and overcover; response dispersion is not interchangeable with uncertainty about the function.
The pinned SMAC comparison runs the actual SMAC 2.4.1 HPO facade alongside this profile and feasible random search. This profile is inspired by SMAC, with different forest statistics, initialization, search, and race scheduling; it is not an implementation-equivalent port.
RandomForestOptimizer is a convenience facade over BayesianOptimizer. The components
can also be supplied directly:
val optimizer = BayesianOptimizer(
projection = SearchTransform(space),
surrogate = ForestSurrogate(),
acquisitionFactory = ExpectedImprovement(),
batchPolicy = SequentialBatchPolicy(FantasyPolicy.PosteriorMean),
outcome = OutcomeSpec(noise = ObservationNoise.Noisy, standardize = true),
randomSeed = 42,
)Surrogate.fit(TrainingData, random)produces an immutableFittedSurrogate. Feature batches are KoblasDenseMatrixrows; responses, weights, and known observation variances are separate Koblas vectors. Models expose latent moments and coherent function draws. Forest variance is ensemble disagreement, not a calibrated posterior variance. The default forest mode ignores supplied observation noise;ForestUncertainty.LeafMeanuses the batch model’s optional leaf-mean estimates and propagates known variances without treating bootstrap copies as independent evidence.ConditionableSurrogateis an optional capability for temporary conditioning.AcquisitionFactorybinds a fixed model, improvement baseline, and outcome policy. EI, posterior mean, and coherent posterior Thompson acquisitions use the same batch scoring interface. A context with no evaluated baseline uses model-mean search rather than inventing an observed reward.AcquisitionOptimizercontrols search independently of scoring. The defaultLocalAndRandomSearchstarts from feasible candidates and observations, then searches neighborhoods of promising points. Klause checks or repairs each move under the caller's hard context. Joint real moves interpolate verified seeds with identical discrete coordinates, retaining exact rational authority for affine equalities. Shared random witness sampling also tries checked joint interpolation when coordinate proposals stall, without fitting or scoring a model. Search limits bound initial attempts, local iterations per start, and neighbors.LocalSearchPolicycontrols multi-start walks, bounded plateau exploration, and shrinking/expanding radii in normalized geometry.LocalSearchPolicy(starts = 1, adaptive = false)retains the previous fixed-radius schedule. This remains a heuristic acquisition maximizer. Exact acquisition ties use seeded random ordering to explore tree-model plateaus;AcquisitionTieBreak.Firstretains candidate order for reproducing earlier searches. Verified excluded anchors still seed local geometry, but cannot be selected for evaluation. Each distinct source assignment is scored at most once per search.BatchPolicyselects all proposals against a frozen study snapshot.InitialDesignindependently controls initial and interleaved random exploration. A customSuggestionPolicycan bypass modeling entirely;StudyRunnerandRandomSuggestionPolicyprovide a feasible random-search baseline.
SearchTransform normalizes numeric base coordinates into [0, 1] using declared
scale geometry. Presence and nominal indicators retain their Boolean slots, and
inactive values are masked. It has no interaction columns or linear-model dependency.
Models that need declared products can opt into InteractionFeatureProjection(SearchTransform(space));
this appends interactions to model inputs without changing proposal coordinates.
decode accepts only base coordinates and maps them
back into a checked original assignment; invalid categorical combinations, contexts,
and constrained points return null. Constraints are never rewritten into feature space.
Custom feature encoders remain supported and are fixed for an optimizer's lifetime.
For spaces declaring interactions, default BO inputs now have fewer columns. Refit
models trained on the previous expanded vectors, or use InteractionFeatureProjection
to retain those products. Exported original-coordinate observation history can be reused.
OutcomeSpec centralizes objective direction, response scale, and optional standardization.
ResponseScale.Log requires positive observations before recording them. Predictions
use lognormal moments in original units, EI integrates improvement in those units, and
sampled functions exponentiate one coherent log-response draw. Optional standardization
is undone before lognormal prediction or acquisition. Known measurement variances use
a first-order delta approximation under the log transform; they are not exact transformed
noise distributions. A conditionable model receives consistently transformed fantasy
labels and noise. Forest uncertainty remains approximate ensemble disagreement.
Predictions, EI margins, recorded metrics, and fantasy labels stay in original reward
units. Observation.noiseVariance / complete(..., noiseVariance = ...) describe known
noise separately from statistical weight. Under ObservationNoise.Noisy, improvement
baselines use model means at evaluated points, and bestObserved aggregates replicate
measurements by weight. recommendOrThrow searches the model mean and can recommend
an unevaluated point; optimalOrThrow remains its compatibility alias.
lastSuggestion reports the study round, frozen history revision, random seed, full
model fit count, and acquisition evaluations. Model fitting is revision-cached with
content-derived seeds; metric predictions do not advance suggestion random streams.
Study round counters survive export. Solver witness caches are ephemeral and are not
exported, so replaying an entire run requires the same initial state and search setup.
OptimizationBenchmark.run records best evaluated values, cumulative evaluation cost,
and simple regret when a known optimum is supplied. Synthetic noisy objectives can
provide a separate noiseless reference value. The component tests compare seeded
forest/EI and random search on a bounded continuous quadratic, verify replay from a
fresh study, and check that local acquisition search improves beyond an initial pool.
The multi-seed benchmark additionally compares guided forest/EI,
normalized pool-only forest/EI, a legacy-style pool-only baseline, and random search
on five objectives. It records full regret-versus-cost traces and a checked-in summary.
These synthetic results do not establish universal algorithm superiority.
The forest/EI investigation traces repeated
evaluations and acquisition plateaus, with separate ablations and validation quadratics.
Koblas is a direct public dependency, with platform artifacts pinned independently of Klause and Kumulant. Vector dot products, scaling, weighted reductions, and batch matrix construction use Koblas APIs rather than parallel numerical helpers.
The build uses Kbuild 1.3.3, Kotlin 2.4.10, Gradle 9.7.0, and a JDK 25 toolchain
that Gradle provisions automatically. Klause and Kumulant use their latest published
snapshots from October 23–24, 2026. Exact platform artifact versions are pinned in
build.gradle.kts, alongside a shared Koblas version that resolves their conflicting
backend pins. The configured targets are JVM and Linux
x64, matching Klause's available snapshots; Combo's implementation is currently JVM.
Build and validate with the published dependencies:
./gradlew jvmTest
./gradlew check lintDocsFor sibling development, -Pkumulant.local substitutes ../kumulant.
-Pkoblas.local substitutes ../koblas; -Pkoblas.local=../another-checkout selects an isolated numerical checkout.
Use -Pdependencies.local to develop against both ../klause and ../kumulant.
The Detekt baseline records existing documentation and style debt surfaced by the
new Kbuild checks; new findings remain checked. ABI references live under api/.
JVM applications using the vector backend should enable
--add-modules=jdk.incubator.vector --enable-native-access=ALL-UNNAMED; the test
tasks already supply these flags.
- PGBM: Probabilistic Gradient Boosting Machines (Sprangers et al., 2021): boosted trees with predictive distributions, slotting in next to the random forest as a stronger Thompson-sampling surrogate.
- Additional history-based surrogates: extend the shared store/refit lifecycle to batch linear models and additional acquisition functions.
- Scalable joint batching and richer kernels: baseline pruning/root caching, stable log-improvement acquisitions, and mixed-type GP covariance, followed by instance features, censored observations, and multi-fidelity scheduling.
recommendOrThrow() reads a frozen history without advancing suggestion randomness or
reserving trials. Repeated calls on unchanged history use the same recommendation seed.
Its model fit is cached; configuring a recommendation search separately lets callers
increase computational effort without increasing the external evaluation budget:
val point = optimizer.recommendOrThrow(
LocalAndRandomSearch(projection, witnesses, candidateCount = 1024, maxAttempts = 2048),
)Fresh confirmation can use explicitly selected evaluated configuration IDs. For example,
rank race.configurations by the optimizer's posterior mean, select a frozen shortlist,
and call race.beginConfirmation(freshKeys, configurationIds). Every selected ID must
have a successful measurement; keys must be fresh and enough worker budget must remain.
The winner uses only confirmation measurements. This is opt-in: our
noisy refinement study finds substantial opportunity
cost at small budgets, and additional recommendation search does not consistently
improve true regret. GP, forest, racing, and search defaults remain unchanged.