Learning Objectives

  • To be able to attach a pre-built solver state tracker to a solver in place of a hand-written printer
  • To be able to distinguish the Console, Csv, and DataFrame trackers and their Nested* variants
  • To be able to describe the iteration and life-cycle events that every solver emits
  • To be able to apply simulated annealing with random restarts using a nested tracker
  • To be able to identify the additional KSL solver families (GA, PSO, Bayesian optimization, ISC) and the SolverPortfolio racing approach
  • To be able to choose an appropriate solver for a given problem

Applying a Solver: From Printer to Tracker

  • To use a solver you need three things: a ProblemDefinition, a ModelBuilderIfc, and an EvaluatorIfc.
  • The Solver companion object provides create... factory functions that build a local Evaluator and wire in a MemorySolutionCache() for you.
  • Rather than passing a custom printer function to react to progress, construct the solver and attach a tracker.
val problemDefinition = makeRQInventoryModelProblemDefinition()
val modelBuilder = BuildRQModel

val solver = Solver.createCrossEntropySolver(
    problemDefinition = problemDefinition,
    modelBuilder = modelBuilder,
    maxIterations = 100,
    replicationsPerEvaluation = 50
)
val tracker = ConsoleSolverStateTracker(solver)
tracker.startTracking()
solver.runAllIterations()

Trackers: What They Are

  • A tracker lives in the ksl.simopt.solvers.trackers package. It attaches itself as an observer to a solver and reacts automatically to the two kinds of events every Solver emits:
    • an iteration event, carrying a SolverStateSnapshot — the iteration number, cumulative oracle calls and replications requested so far, and the current and best solutions found so far
    • a life-cycle event, carrying a SolverStatusINITIALIZED, STARTED, COMPLETED, or ERROR
  • All built-in trackers extend AbstractSolverStateTracker, which handles attaching to and detaching from the two emitters.
    • A specific tracker only needs to say what to do with each snapshot or status change — replacing the old hand-written progress printer.

The Three Trackers

The ksl.simopt.solvers.trackers package currently provides three trackers, differing only in where each snapshot goes:

  • ConsoleSolverStateTracker — prints each snapshot and life-cycle transition to the console. This is the tracker used in the running example.
  • CsvSolverStateTracker — appends each snapshot to a CSV file, managing the file across multiple solver runs.
  • DataFrameSolverStateTracker — accumulates snapshots in memory and compiles them into a Kotlin DataFrame for further analysis.

All three inherit the same attach/detach machinery from AbstractSolverStateTracker; they differ only in their reaction to a snapshot.

Attaching a Tracker

  • Using a tracker takes two lines: construct it around an already-built solver, then call startTracking() before runAllIterations().
  • startTracking() attaches the tracker to the solver’s iterationEmitter and lifeCycleEmitter; calling it again on an already-tracking tracker has no effect.
  • A matching stopTracking() detaches the tracker if you want to silence it before the solver finishes.
  • A solver only builds and emits a snapshot when something is listening. Attaching a tracker is how you see intermediate iterations — without one, only the final results are available once runAllIterations() returns.
  • The snapShotFrequency property (default 1, i.e. every iteration) controls how often iteration snapshots are emitted.

Console Tracker: The Trace

>>> SOLVER INITIALIZED: Baseline captured.
[Iter:  0 | Oracle:    0] EstObj: 1.79769E+308 | PenObj: 1.79769E+308 | Cur (q,r)=(-2.15e9, -2.15e9)
>>> SOLVER STARTED: Beginning iterations...
[Iter:  1 | Oracle:   34] EstObj: 13.7967 | PenObj: 13.7967 | Cur (q,r)=(1,11)  | Best (q,r)=(1,11)
[Iter:  2 | Oracle:   67] EstObj: 9.06001 | PenObj: 9.06001 | Cur (q,r)=(10,5)  | Best (q,r)=(10,5)
[Iter:  7 | Oracle:  238] EstObj: 8.45271 | PenObj: 8.45271 | Cur (q,r)=(15,2)  | Best (q,r)=(15,2)
   . . .
[Iter: 40 | Oracle: 1129] EstObj: 4.61628 | PenObj: 4.61628 | Cur (q,r)=(4,3)   | Best (q,r)=(4,3)
  • The INITIALIZED baseline starts from the problem’s “bad solution” sentinel; the solution then improves over the first few iterations.
  • The cross-entropy solver stops once its last 5 solutions are the same — here at reorder quantity q = 4, reorder point r = 3.

Input-variable names abbreviated (q = reorder quantity, r = reorder point); sentinel values shortened; lines elided.

Nested Trackers for Composite Solvers

  • Some solvers, such as RandomRestartSolver, are really two solvers working together: an outer solver that manages the restarts and an inner solver that searches between restarts.
  • For these composite solvers the package provides nested counterparts, each tracking the outer and inner solver together:
    • NestedConsoleSolverStateTracker
    • NestedCsvSolverStateTracker
    • NestedDataFrameSolverStateTracker
  • The nested console tracker prints one MACRO line per completed restart (its cumulative oracle calls and best point so far), with each of that restart’s own iterations printed as an indented MICRO line beneath it.
    • You see the restart-level and the annealing-level trajectory in one trace.

SA with Random Restarts: The Factory

  • Simulated annealing may be sensitive to its initial point, so it makes sense to restart it at randomly generated points.
  • Each solver family has a createRandomRestart<Family>Solver factory. For simulated annealing:
fun createRandomRestartSimulatedAnnealingSolver(
    problemDefinition: ProblemDefinition,
    modelBuilder: ModelBuilderIfc,
    maxNumRestarts: Int = defaultMaxRestarts,
    temperatureConfiguration: TemperatureConfiguration = TemperatureConfiguration.AutoCalibrate(),
    coolingSchedule: CoolingScheduleIfc = ExponentialCoolingSchedule(defaultInitialTemperature),
    stoppingTemperature: Double = SimulatedAnnealing.defaultStoppingTemperature,
    maxIterations: Int = defaultMaxNumberIterations,
    replicationsPerEvaluation: Int = defaultReplicationsPerEvaluation,
    solutionCache: SolutionCacheIfc = MemorySolutionCache(),
    concurrentRestarts: Int = 1
): RandomRestartSolver
  • The default temperatureConfiguration is TemperatureConfiguration.AutoCalibrate(), not a fixed value: each restart calibrates its own starting temperature to the landscape around its own randomly chosen starting point.
  • concurrentRestarts (default 1, sequential) can be raised to run restarts at the same time.

Running SA with Restarts

val solver = Solver.createRandomRestartSimulatedAnnealingSolver(
    problemDefinition = problemDefinition,
    modelBuilder = modelBuilder,
    maxIterations = 100,
    replicationsPerEvaluation = 50
)
val tracker = NestedConsoleSolverStateTracker(solver, solver.restartingSolver)
tracker.startTracking()
solver.runAllIterations()
solver.printResults()
  • solver is the outer, restart-managing solver; solver.restartingSolver is the inner SimulatedAnnealing instance that actually searches between restarts.
  • The NestedConsoleSolverStateTracker attaches to both at once — the exact composite case the nested trackers were built for.
  • We reuse the same makeRQInventoryModelProblemDefinition() and BuildRQModel from the earlier example.

The MACRO / MICRO Trace

>>> EXPERIMENT INITIALIZED: DefaultExperiment (Run 1)
[Run 1 | MACRO: 0] Total Oracle Calls:   1 | Best EstObj: 38.95 | Best (q,r)=(14,33)
>>> MACRO EXPERIMENT STARTED
    -> [Micro: 0] EstObj: 97.82 | PenObj: 97.82 | Cur (q,r)=(32,83)
    -> [Micro: 1] EstObj: 97.82 | PenObj: 97.82 | Cur (r,q)=(83,88)
       . . .
[Run 1 | MACRO: 1] Total Oracle Calls: 132 | Best EstObj: 16.90 | Best (r,q)=(9,18)
       . . .
[Run 1 | MACRO: 4] Total Oracle Calls: 535 | Best EstObj: 11.92 | Best (r,q)=(10,5)
  • The MACRO best estimated objective improves across restarts: 38.95 → 16.90 → … → 11.92.
  • The random-restart solver remembers the initial solutions and each restart’s result; the recommended solution here is reorder point 4, reorder quantity 9 with the fill-rate constraint met — slightly worse than the cross-entropy result.

Input-variable names abbreviated; the tuple label order (q,r) vs (r,q) follows the book’s trace; lines elided.

Additional Solvers: Population-Based

  • Genetic Algorithm — like cross-entropy it searches with a population, but the population is the search: individuals persist across generations, the best are selected as parents, pairs are recombined (crossover), and each offspring is perturbed (mutation). A natural fit for combinatorial spaces — orderings, subsets, assignments — where blending one shared distribution has no meaning (Holland 1975).
    • Built with Solver.createGeneticAlgorithmSolver(...).
  • Particle Swarm Optimization — a swarm of particles moving through continuous space; each particle’s velocity is pulled toward both its own best-seen and the swarm’s best-seen position, damped by an inertia term (broad exploration early, local refinement late). It evaluates the whole swarm as one batched request each iteration — the one KSL family that defaults to parallel evaluation (Kennedy & Eberhart 1995).
    • Built with Solver.createParticleSwarmSolver(...).

Additional Solvers: Surrogate & Certified

  • Bayesian Optimization — every solver above is a direct search; this one instead reasons about a statistical surrogate (typically a Gaussian process fit to every point observed so far) and simulates only the single point the surrogate judges most promising. Best when a single run is so costly that only a few dozen–few hundred total evaluations are affordable — the opposite regime from population methods (Jones et al. 1998).
    • Built with Solver.createBayesianOptimizationSolver(...).
  • Industrial Strength COMPASS (ISC) — extends R-SPLINE and is the only KSL family that can back its recommendation with a formal statistical guarantee. Three phases: a global phase (niching genetic algorithm), a local phase (a COMPASS search per region), and a clean-up phase (ranking-and-selection to certify the returned solution at a chosen confidence). The guarantee is opt-in via a meaningful indifference-zone parameter (Hong & Nelson 2006; Xu et al. 2010).
    • Built with Solver.createISCSolver(...).

Choosing Among Solvers: SolverPortfolio

  • With eight solver families available, the SolverPortfolio class offers a different kind of choice: rather than committing to one algorithm, race several at once on the same problem.
  • Each contender gets its own private evaluator, so the runs are genuinely independent.
  • An optional confirmation stage re-evaluates the top finishers under common random numbers (CRN) before declaring a winner.
    • This is the same screening idea already used to compare designed-experiment scenarios and to pick among random restarts — applied here across different algorithms rather than different starting points or scenarios.

Summary: When to Use Which

  • Attach a tracker to any runConsole / Csv / DataFrame, or the Nested* variants for composite solvers like RandomRestartSolver. A snapshot is only emitted while something is listening.
  • Continuous/mixed space, many cheap evaluations → Cross-Entropy
  • Single-point local search that can escape local optima → Simulated Annealing (add random restarts when sensitive to the starting point)
  • Integer-ordered, local convergence → R-SPLINE; integer-ordered with a certified best → ISC
  • Combinatorial encodings (orderings, subsets, assignments) → Genetic Algorithm
  • Continuous swarm with parallel batch evaluation → Particle Swarm
  • Expensive simulation, tight evaluation budget → Bayesian Optimization
  • Not sure which → race them with a SolverPortfolio and confirm the winner under CRN
⌂ Index