API Reference
Reference every public API contract, default, and error behavior in `timetable-sa`.
API Reference
This document is the source-of-truth reference for the public API exposed by
timetable-sa. It is derived from the implementation in src/, not from
planned features, and it focuses on precise runtime behavior, type contracts,
default values, and failure modes.
Export surface
The package exports one primary class, four error types, and the public type system that you use to model a domain-specific optimization problem.
export { SimulatedAnnealing } from './core/index.js';
export {
SAError,
SAConfigError,
ConstraintValidationError,
SolveConcurrencyError,
} from './core/index.js';
export type {
Constraint,
MoveGenerator,
SAConfig,
LoggingConfig,
Solution,
OperatorStats,
SolverDiagnostics,
PhaseTimingDiagnostics,
FeasibilityDiagnostics,
IntensificationDiagnostics,
Violation,
ProgressStats,
OnProgressCallback,
} from './core/index.js';SimulatedAnnealing<TState>
SimulatedAnnealing<TState> is the main solver class. A solver instance is
stateful at runtime, but its problem definition is fixed after construction:
the initial state, constraints, move generators, and configuration are all
captured in the constructor.
Constructor
The constructor validates input eagerly, partitions constraints into hard and soft sets, resolves configuration defaults, initializes logging, and prepares operator statistics.
new SimulatedAnnealing<TState>(
initialState: TState,
constraints: Constraint<TState>[],
moveGenerators: MoveGenerator<TState>[],
config: SAConfig<TState>
)Parameters
| Parameter | Type | Meaning |
|---|---|---|
initialState | TState | Initial candidate solution. It must not be null or undefined. |
constraints | Constraint<TState>[] | Constraint set used for fitness evaluation and violation reporting. |
moveGenerators | MoveGenerator<TState>[] | Neighborhood operators used to generate candidate states. |
config | SAConfig<TState> | Annealing, tabu, intensification, logging, and progress settings. |
Constructor behavior
The constructor performs these operations in order:
- It calls
validateSolverInputs(...). - It stores the original arrays and partitions constraints into hard and soft subsets.
- It resolves defaults with
mergeConfigWithDefaults(...). - It creates a
Loggerfrom the resolved logging config. - It creates a
TabuMemoryinstance backed by an internalMap. - It initializes
operatorStatswith zeroed counters for every move generator.
Throws
The constructor throws SAConfigError for all documented validation failures,
including null initial state, malformed constraints, malformed move
generators, invalid numeric values, and invalid optional tuning parameters.
It does not throw TypeError as part of its explicit validation contract.
solve()
solve() runs the full optimization lifecycle and returns the best solution
encountered across all phases.
solve(): Promise<Solution<TState>>Runtime contract
The method is asynchronous because progress callbacks may be asynchronous. A
single solver instance permits only one in-flight solve() call.
- If
solve()is invoked while another invocation is still running on the same instance, the solver throwsSolveConcurrencyError. - The internal
isSolvingguard is always reset in afinallyblock. - Runtime state such as tabu memory, progress counters, and operator stats is reset at the start of each new solve.
High-level lifecycle
The implementation in src/core/SimulatedAnnealing.ts executes these stages:
- Clone
initialStatewithconfig.cloneState. - Evaluate the initial state and emit an initial progress callback when
onProgressis configured. - Run Phase 1 to reduce hard violations.
- If hard violations remain and
enableIntensificationis true, run Phase 1.5 intensification. - Run Phase 2 to improve overall fitness while forbidding degradation beyond the best hard-violation count found so far.
- Build and return
Solution<TState>.
getStats()
getStats() returns a snapshot of operator statistics keyed by move generator
name.
getStats(): OperatorStatsThe method copies the current counters, so callers cannot mutate the solver's internal state through the returned object.
getDiagnostics()
getDiagnostics() returns a snapshot of the solver diagnostics collected during
the most recent solve() run.
getDiagnostics(): SolverDiagnosticsThe returned object is a shallow snapshot of the diagnostics groups. Mutating the returned value does not mutate internal solver state.
Constraint<TState>
Constraint<TState> defines a scored condition over a candidate state. The
library treats the score as a normalized satisfaction measure, not as a raw
penalty.
interface Constraint<TState> {
name: string;
type: 'hard' | 'soft';
weight?: number;
evaluate(state: TState): number;
describe?(state: TState): string | undefined;
getViolations?(state: TState): string[];
}Semantic contract
The implementation enforces the following contract at runtime:
evaluate(state)must return a finite number in the closed interval[0, 1].1means fully satisfied.0means maximally violated.- Intermediate values encode partial satisfaction.
This direction is important because the solver converts lack of satisfaction
into penalty by using 1 - score.
Hard constraints
Hard constraints contribute to fitness as:
hardPenalty += 1 - score
fitness += hardPenalty * hardConstraintWeightFor hard constraints, getViolations() affects two outputs:
- the
hardViolationscount used inSolution<TState>and parts of phase control, - the detailed
violationsarray returned at the end ofsolve().
If getViolations() is absent, the engine infers a violation count from the
score using:
score > 0 ? max(1, round(1 / score - 1)) : 1This inferred count is heuristic. If you need accurate multiplicity, implement
getViolations() explicitly.
Soft constraints
Soft constraints contribute to fitness as:
softPenalty += (1 - score) * (weight ?? 10)The default soft-constraint weight is 10, not 1.
Reporting helpers
describe() and getViolations() are optional diagnostic helpers.
- If
getViolations()is present, the engine emits oneViolationobject for each returned string. - Otherwise, the engine emits a single
Violationobject, optionally enriched bydescribe().
Example
This example matches the actual satisfaction-oriented score contract.
const noOverlap: Constraint<MyState> = {
name: 'No overlap',
type: 'hard',
evaluate(state) {
return findOverlapCount(state) === 0 ? 1 : 0;
},
getViolations(state) {
return findOverlaps(state).map(
(pair) => `${pair.left} overlaps with ${pair.right}`
);
},
};
const morningPreference: Constraint<MyState> = {
name: 'Morning preference',
type: 'soft',
weight: 15,
evaluate(state) {
const ratio = fractionScheduledInMorning(state);
return Math.max(0, Math.min(1, ratio));
},
};MoveGenerator<TState>
MoveGenerator<TState> defines a neighborhood operator. The solver clones the
current state before calling generate(...), so the move generator receives a
mutable working copy.
interface MoveGenerator<TState> {
name: string;
generate(state: TState, temperature: number): TState;
canApply(state: TState): boolean;
}Runtime contract
The effective contract is as follows:
canApply(state)decides whether the operator is eligible for selection in the current state.generate(state, temperature)is called with a cloned state.- The method may mutate the passed state directly and return it.
- If all move generators return
falsefromcanApply(...), solving stops early because the engine cannot generate neighbors.
Selection implications
Move generator names are not only labels. In Phase 1, the engine still contains
name-aware heuristics that can prefer generators whose names include terms such
as fix, swap, friday, lecturer, exclusive, or capacity.
Phase 1.5 is more explicit in the current branch. If you provide
intensificationTargetedOperatorNames, the solver uses those exact names with a
case-insensitive comparison instead of relying on substring heuristics.
SAConfig<TState>
SAConfig<TState> controls the annealing schedule, reheating, tabu memory,
intensification, telemetry, and state cloning strategy.
interface SAConfig<TState> {
initialTemperature: number;
minTemperature: number;
coolingRate: number;
maxIterations: number;
hardConstraintWeight: number;
cloneState: (state: TState) => TState;
reheatingThreshold?: number;
maxReheats?: number;
reheatingFactor?: number;
tabuSearchEnabled?: boolean;
tabuTenure?: number;
aspirationEnabled?: boolean;
maxTabuListSize?: number;
enableIntensification?: boolean;
intensificationIterations?: number;
maxIntensificationAttempts?: number;
intensificationStagnationLimit?: number;
intensificationStartTemperatureMode?: 'phase1-end' | 'initial-reset';
intensificationStartTempMultiplier?: number;
intensificationStartTempCapRatio?: number;
intensificationUseTabu?: boolean;
intensificationTargetedOperatorNames?: string[];
intensificationTargetedSelectionRate?: number;
intensificationEarlyStopNoBestImproveIterations?: number;
intensificationBudgetFractionOfMaxIterations?: number;
getStateSignature?: (state: TState) => string;
operatorSelectionMode?: 'hybrid' | 'roulette-wheel';
logging?: LoggingConfig;
onProgress?: OnProgressCallback<TState>;
onProgressMode?: 'await' | 'fire-and-forget';
}Required fields
These fields have no defaults and must be provided.
| Field | Validation |
|---|---|
initialTemperature | finite number, > 0 |
minTemperature | finite number, > 0 |
coolingRate | finite number, 0 < coolingRate < 1 |
maxIterations | positive integer |
hardConstraintWeight | finite number, > 0 |
cloneState | function |
The validator does not enforce minTemperature < initialTemperature.
Optional fields and resolved defaults
The table below reflects mergeConfigWithDefaults(...) exactly.
| Field | Default |
|---|---|
reheatingThreshold | undefined |
maxReheats | 3 |
reheatingFactor | 2.0 |
tabuSearchEnabled | false |
tabuTenure | 50 |
maxTabuListSize | 1000 |
aspirationEnabled | true |
enableIntensification | true |
intensificationIterations | 2000 |
maxIntensificationAttempts | 3 |
intensificationStagnationLimit | 300 |
intensificationStartTemperatureMode | 'phase1-end' |
intensificationStartTempMultiplier | 1.0 |
intensificationStartTempCapRatio | 1.0 |
intensificationUseTabu | true |
intensificationTargetedOperatorNames | [] |
intensificationTargetedSelectionRate | 0.7 |
intensificationEarlyStopNoBestImproveIterations | 800 |
intensificationBudgetFractionOfMaxIterations | 0.25 |
onProgressMode | 'await' |
logging.enabled | true |
logging.level | 'info' |
logging.logInterval | 1000 |
logging.output | 'console' |
logging.filePath | './sa-optimization.log' |
Validation rules for optional fields
The validator applies these rules when the corresponding field is provided:
reheatingThreshold: positive integer.maxReheats: non-negative integer.reheatingFactor: number greater than1.tabuTenure: positive integer.maxTabuListSize: positive integer.intensificationIterations: positive integer.maxIntensificationAttempts: positive integer.intensificationStagnationLimit: positive integer.intensificationStartTemperatureMode:'phase1-end'or'initial-reset'.intensificationStartTempMultiplier: finite number greater than0.intensificationStartTempCapRatio: finite number greater than0.intensificationTargetedOperatorNames: array of non-empty strings.intensificationTargetedSelectionRate: probability in the closed interval[0, 1].intensificationEarlyStopNoBestImproveIterations: positive integer.intensificationBudgetFractionOfMaxIterations: finite number greater than0and less than or equal to1.logging.logInterval: positive integer.- soft
weight: finite number greater than or equal to0.
Intensification-specific semantics
The new Phase 1.5 fields have runtime meaning beyond their types.
intensificationStartTemperatureModecontrols whether an intensification attempt starts from the Phase 1 terminal temperature or resets toinitialTemperature.intensificationStartTempMultiplierscales the Phase 1 terminal temperature before the cap is applied.intensificationStartTempCapRatiocaps the scaled start temperature atinitialTemperature * ratio.intensificationUseTabuenables tabu gating inside Phase 1.5 whentabuSearchEnabledis also true.intensificationTargetedOperatorNamesmatches operator names with a case-insensitive exact comparison.intensificationTargetedSelectionRatecontrols how often the solver chooses from the targeted set when that set is non-empty.intensificationEarlyStopNoBestImproveIterationsends an intensification attempt early when the global best hard-violation objective does not improve.intensificationBudgetFractionOfMaxIterationscaps total Phase 1.5 iterations atfloor(maxIterations * fraction).
LoggingConfig
LoggingConfig controls the built-in logger used by the solver.
interface LoggingConfig {
enabled?: boolean;
level?: 'debug' | 'info' | 'warn' | 'error' | 'none';
logInterval?: number;
output?: 'console' | 'file' | 'both';
filePath?: string;
}When output is 'file' or 'both', the logger creates missing parent
directories with mkdirSync(..., { recursive: true }) before appending log
lines.
OnProgressCallback<TState>
OnProgressCallback<TState> is the public callback type used for progress
telemetry.
type OnProgressCallback<TState> = (
iteration: number,
currentCost: number,
temperature: number,
state: TState | null,
stats: ProgressStats
) => void | Promise<void>;Callback behavior
The implementation has a few details that matter in production:
stateis alwaysnull. This is an intentional performance decision to avoid cloning the current state for telemetry.- The callback can be synchronous or asynchronous.
- In
'await'mode, the solver waits for completion. - In
'fire-and-forget'mode, the solver schedules the callback and continues. - If the callback throws or rejects, the error is caught and logged at
warnlevel; the solve continues. - The callback is not invoked twice for the same iteration because
ProgressReportertrackslastProgressIteration.
Solution<TState>
Solution<TState> is the result returned by solve().
interface Solution<TState> {
state: TState;
fitness: number;
hardViolations: number;
softViolations: number;
iterations: number;
reheats: number;
finalTemperature: number;
violations: Violation[];
operatorStats: OperatorStats;
diagnostics?: SolverDiagnostics;
}Field semantics
The field names are straightforward, but their exact meanings are worth making explicit.
| Field | Meaning |
|---|---|
state | Best state found during the solve. |
fitness | Final objective value computed from hard and soft penalties. Lower is better. |
hardViolations | Count of hard-constraint violation records in the final violations array. |
softViolations | Count of soft-constraint violation records in the final violations array. |
iterations | Total loop iterations completed across all phases. |
reheats | Number of reheating events triggered in Phase 1 and Phase 2. |
finalTemperature | Temperature value at the time solving stopped. |
violations | Detailed violation objects generated from constraints. |
operatorStats | Final per-operator attempt, acceptance, and improvement counters. |
diagnostics | Optional telemetry snapshot for timing, feasibility, and intensification behavior. |
softViolations is a count, not a weighted penalty sum.
Fitness function
At the end of each evaluation, the solver computes:
fitness(state) = hardConstraintWeight * hardPenalty(state) + softPenalty(state)
hardPenalty(state) = sum over hard constraints of (1 - score)
softPenalty(state) = sum over soft constraints of (1 - score) * weightThis means fitness is not simply hardViolations * hardConstraintWeight.
Hard violations are counted separately for reporting and phase control, while
the fitness function uses the fractional deficit 1 - score.
SolverDiagnostics
SolverDiagnostics groups the additive telemetry returned by
solution.diagnostics and solver.getDiagnostics().
interface SolverDiagnostics {
phaseTimings: PhaseTimingDiagnostics;
feasibility: FeasibilityDiagnostics;
intensification: IntensificationDiagnostics;
}The solver resets this structure at the start of every solve and repopulates it throughout the run.
PhaseTimingDiagnostics
PhaseTimingDiagnostics captures elapsed wall-clock timing for the major solve
stages.
interface PhaseTimingDiagnostics {
phase1Ms: number;
phase15Ms: number;
phase2Ms: number;
totalRuntimeMs: number;
}These values are non-negative and are measured with performance.now().
FeasibilityDiagnostics
FeasibilityDiagnostics captures hard-violation progress across the solve.
interface FeasibilityDiagnostics {
initialHardViolations: number;
bestHardViolationsAfterPhase1: number;
bestHardViolationsAfterPhase15: number;
bestHardViolationsFinal: number;
timeToFirstFeasibleMs: number | null;
iterationToFirstFeasible: number | null;
}timeToFirstFeasibleMs and iterationToFirstFeasible remain null when the
solver never reaches zero hard violations.
IntensificationDiagnostics
IntensificationDiagnostics captures Phase 1.5 behavior in detail.
interface IntensificationDiagnostics {
triggered: boolean;
attemptsRun: number;
iterationsRun: number;
phase15BudgetLimitIterations: number;
phase15BudgetUsedIterations: number;
acceptedMoves: number;
hardImprovingAcceptedMoves: number;
equalHardAcceptedMoves: number;
hardWorseningAcceptedMoves: number;
phase15TabuSkips: number;
localReheats: number;
bestUpdates: number;
phase15EndedByBudget: boolean;
phase15EndedByEarlyStop: boolean;
phase15StartHard: number | null;
phase15WorstCurrentHard: number | null;
phase15EndCurrentHard: number | null;
phase15BestHardDelta: number | null;
}This structure is useful when you need to distinguish between three common cases: Phase 1.5 never triggered, it triggered but ended by budget, or it triggered but ended early because the global best hard-violation objective stalled.
Violation
Violation is the normalized diagnostic record returned in Solution<TState>.
interface Violation {
constraintName: string;
constraintType: 'hard' | 'soft';
score: number;
description?: string;
}Construction rules
The engine builds violations as follows:
- If
constraint.getViolations()exists, each returned string becomes a separateViolationwith the samescore. - Otherwise, the engine emits one
Violationwhenscore < 1. - If
constraint.describe()returns a string, it is copied intodescription.
ProgressStats
ProgressStats is the structured metric payload attached to every progress
callback.
interface ProgressStats {
iteration: number;
currentCost: number;
bestCost: number;
temperature: number;
hardViolations: number;
softViolations: number;
tabuHits: number;
tabuSize: number;
phase: 'phase1' | 'phase15' | 'phase2' | 'initial';
reheatingCount: number;
acceptedMoves: number;
rejectedMoves: number;
stagnationCount: number;
bestCostIteration: number;
progressPercent: number;
initialCost: number;
improvement: number;
timestamp: number;
}Metric interpretation
softViolationsis the count passed by the solver at callback time, not the weighted soft penalty.progressPercentis estimated asmin(100, iteration / maxIterations * 100).improvementis reported as a percentage relative toinitialCost.phasecomes from internal phase transitions:initial,phase1,phase15, andphase2.
OperatorStats
OperatorStats is the per-operator online-learning record.
interface OperatorStats {
[operatorName: string]: {
attempts: number;
improvements: number;
accepted: number;
successRate: number;
};
}successRate is computed as improvements / attempts whenever an operator's
stats are updated.
Errors
The error hierarchy is intentionally small and concrete.
SAError
SAError is the base class for library-defined errors.
class SAError extends Error {
readonly code: string;
}It contains message, name, and code. It does not carry a context
object.
SAConfigError
SAConfigError reports invalid constructor inputs or invalid config values.
Typical causes include:
- a missing or invalid
cloneState, - a non-finite numeric setting,
- an out-of-range score weight,
- malformed constraints or move generators,
- invalid optional integer parameters.
ConstraintValidationError
ConstraintValidationError is thrown when a constraint returns an invalid score.
Typical causes include:
evaluate()returnsNaN,Infinity, or-Infinity,evaluate()returns a value outside[0, 1].
If evaluate() itself throws an exception, that exception propagates directly;
it is not wrapped automatically into ConstraintValidationError.
SolveConcurrencyError
SolveConcurrencyError is thrown when solve() is called concurrently on the
same solver instance.
Example: fully typed configuration
This example matches the current runtime contract, including the real callback
signature and the fact that state is null.
const config: SAConfig<MyState> = {
initialTemperature: 1000,
minTemperature: 0.01,
coolingRate: 0.995,
maxIterations: 50000,
hardConstraintWeight: 10000,
cloneState: deepClone,
tabuSearchEnabled: true,
tabuTenure: 50,
aspirationEnabled: true,
enableIntensification: true,
intensificationIterations: 2000,
intensificationStartTemperatureMode: 'phase1-end',
intensificationTargetedOperatorNames: ['Repair hard conflict'],
intensificationBudgetFractionOfMaxIterations: 0.25,
logging: {
enabled: true,
level: 'info',
logInterval: 1000,
output: 'console',
},
onProgress: async (iteration, currentCost, temperature, state, stats) => {
console.log({
iteration,
currentCost,
temperature,
stateIsNull: state === null,
phase: stats.phase,
bestCost: stats.bestCost,
});
},
onProgressMode: 'await',
};
const solver = new SimulatedAnnealing(initialState, constraints, moves, config);
const solution = await solver.solve();
console.log(solution.diagnostics?.intensification.phase15EndedByBudget);Next steps
If you are choosing where to go next, use the documents by intent:
- Read Configuration for tuning guidance and parameter trade-offs.
- Read Algorithm and Runtime Behavior for algorithm lifecycle and acceptance behavior.
- Read Internal Architecture for internal component relationships and data flow.