timetable-sa

Internal Architecture

Understand module boundaries, data flow, and orchestration inside the solver runtime.

Internal Architecture

This document explains how the implementation is organized internally and how its major subsystems collaborate during a solve. It is written for maintainers, researchers, and advanced integrators who need a code-faithful architectural view rather than a marketing-level overview.

Architectural summary

timetable-sa is a generic TypeScript solver with a narrow public surface and a relatively rich internal runtime. The architecture is centered on one orchestrator class, SimulatedAnnealing<TState>, supported by small modules for validation, acceptance rules, operator selection, tabu memory, and telemetry.

The design has four noteworthy properties:

  • generic domain modeling through Constraint<TState> and MoveGenerator<TState>,
  • eager validation of configuration and shape contracts,
  • online adaptation of move-operator selection,
  • operational observability through logging, progress callbacks, and solver diagnostics.

Module map

The core implementation lives under src/core/ and can be understood as six collaborating layers.

Public orchestration layer

src/core/SimulatedAnnealing.ts owns the end-to-end lifecycle of a solve. Every public operation ultimately routes through this class.

Its responsibilities include:

  • validating inputs at construction time,
  • resolving defaults,
  • partitioning hard and soft constraints,
  • running the three optimization phases,
  • managing tabu screening,
  • collecting operator statistics,
  • emitting logs and progress callbacks,
  • collecting diagnostics snapshots for timing, feasibility, and intensification,
  • packaging the final Solution<TState>.

Validation layer

The validation layer consists of two focused modules.

The layer is conservative and fail-fast, but it does not attempt deep semantic verification such as determinism checks or cross-field optimization advice.

Validation code highlights

The constructor validator enforces shape and numeric contracts before the solver starts running.

for (const constraint of constraints) {
  if (!constraint.name || typeof constraint.name !== 'string') {
    throw new SAConfigError('All constraints must have a name property');
  }

  if (!constraint.type || !['hard', 'soft'].includes(constraint.type)) {
    throw new SAConfigError(
      `Constraint "${constraint.name}" must have type 'hard' or 'soft'`
    );
  }
}

assertFiniteNumber(config.coolingRate, 'coolingRate');
if (config.coolingRate <= 0 || config.coolingRate >= 1) {
  throw new SAConfigError(
    `coolingRate must be between 0 and 1 (exclusive), got ${config.coolingRate}`
  );
}

The runtime validator wraps every score evaluation to enforce the score range contract on every call.

const score = constraint.evaluate(state);

if (typeof score !== 'number' || !Number.isFinite(score)) {
  throw new ConstraintValidationError(
    `Constraint "${constraint.name}" returned invalid score (${score}).`
  );
}

if (score < 0 || score > 1) {
  throw new ConstraintValidationError(
    `Constraint "${constraint.name}" returned out-of-range score (${score}).`
  );
}

For full implementation detail, use the source links above as the canonical reference.

Policy layer

The policy layer encapsulates small decision functions rather than full framework-style plug-in abstractions.

This separation keeps acceptance and selection logic testable and independent of the rest of the solver state machine.

Policy code highlights

Phase 1 and Phase 2 use separate acceptance policies so each phase can enforce its own hard-violation rules.

if (newHardViolations < currentHardViolations) {
  return 1.0;
}

if (newHardViolations > bestHardViolations) {
  return 0.0;
}

Operator selection uses a smoothed success-rate strategy with an exploration branch in hybrid mode.

if (Math.random() < 0.3) {
  return generators[Math.floor(Math.random() * generators.length)]!;
}

return (stats.improvements + alpha) / (stats.attempts + beta);

Tabu layer

The tabu layer provides state-signature generation and bounded short-term memory.

Tabu code highlights

Tabu skipping and aspiration are handled in one focused decision method.

if (!this.contains(signature, currentIteration)) {
  return false;
}

if (aspirationEnabled && newFitness < globalBestFitness) {
  return false;
}

return true;

Telemetry layer

The telemetry layer is split between logging and callback reporting.

Telemetry code highlights

Logger sanitization redacts sensitive keys before serialization.

const sensitiveKeyPattern =
  /(password|secret|token|apikey|api_key|authorization|cookie|session)/i;

if (sensitiveKeyPattern.test(key)) {
  redacted[key] = '[REDACTED]';
}

Progress callbacks are isolated so telemetry failures do not break solve logic.

try {
  const result = onProgress(iteration, currentCost, temperature, null, stats);
  if (result instanceof Promise) await result;
} catch (error) {
  onError(error);
}

Diagnostics data flow

The current branch also records additive diagnostics in the orchestration layer itself.

  • runtime reset initializes the diagnostics structure at the start of solve(),
  • phase timing is measured with performance.now(),
  • feasibility milestones record the initial hard count, best hard counts after each phase, and the first-feasible milestone when one exists,
  • intensification records attempts, accepted-move categories, tabu skips, local reheats, budget usage, and stop reasons,
  • getDiagnostics() returns a snapshot copy of the grouped diagnostics object.

Type and error layer

Type and error definitions formalize the public and internal contracts.

Solve pipeline

The easiest way to understand the architecture is to follow one solve() call.

1. Construction stage

Before solving begins, the constructor performs static setup.

constructor(...)
  -> validateSolverInputs(...) in ConfigValidator
  -> partition constraints into hard and soft sets
  -> mergeConfigWithDefaults(...) in ConfigValidator
  -> create Logger
  -> create TabuMemory
  -> initialize operatorStats

This stage establishes the immutable problem definition for the instance.

2. Runtime reset stage

At the beginning of solve(), the solver resets runtime-only state.

It clears or resets:

  • tabu memory,
  • progress-reporter state,
  • hard-constraint hint cache,
  • hard-breakdown logging cache,
  • per-operator counters.

This is why repeated solves on the same instance start from the same initial problem definition but not from leftover runtime state.

3. Initial evaluation stage

The solver clones the initial state, computes its fitness and violations, logs the initial summary, and optionally emits an initial progress callback.

Architecturally, this stage performs two roles:

  • it establishes baseline telemetry, and
  • it seeds the incumbent currentState and bestState.

4. Phase execution stage

The orchestration layer then moves through Phase 1, optional intensification, and Phase 2.

Each phase reuses lower-level services but applies them differently.

  • neighbor generation is reused across phases,
  • acceptance rules differ by phase,
  • progress telemetry is reused but phase-tagged,
  • tabu logic is reusable and phase-agnostic.

5. Packaging stage

At the end, createSolution(...) recomputes the final violation set from the best state, counts hard and soft violations from that set, logs completion, and returns the structured result.

The architectural point is important: final counts are derived from the materialized Violation[], not from stale counters accumulated during search.

Data model and contracts

The solver's architecture depends on a small number of contracts that every integration must satisfy.

Constraint contract

Constraint<TState> models satisfaction, not penalty.

score in [0, 1]
1 = satisfied
0 = violated

This single design choice propagates through the entire architecture:

  • fitness computation uses 1 - score,
  • violation materialization uses score < 1,
  • runtime validation rejects non-finite or out-of-range scores.

Move generator contract

The engine clones a state before calling generate(...). This means the move generator contract is mutation-friendly even though the public API looks pure.

That decision simplifies operator implementations and centralizes cloning policy in configuration.

Configuration contract

SAConfig<TState> is partially required and partially defaulted.

The architecture separates:

This split avoids mixing user-facing diagnostics with internal convenience.

Fitness and violation architecture

Fitness evaluation and violation reporting are related but distinct subsystems.

Fitness path

calculateFitnessAndViolations(...) computes two things:

  • scalar fitness,
  • integer hardViolations.

The function iterates through hard constraints first, then soft constraints. For hard constraints, it aggregates both fractional penalty and a discrete violation count. For soft constraints, it aggregates only weighted penalty.

Violation-reporting path

getViolations(...) constructs Violation[] records for both hard and soft constraints.

The architectural split is intentional:

  • search decisions need fast scalar and count summaries,
  • final reporting needs human-readable diagnostic structure.

Because these paths are separate, advanced users should implement getViolations() when they need discrete multiplicity to match the real-world problem semantics.

Phase architecture

The solver is not a single homogeneous loop. Each phase specializes the search process around a different objective.

Phase 1 as feasibility engine

Phase 1 is structurally biased toward feasibility.

  • it caps itself at 60 percent of the total iteration budget,
  • it terminates once bestHardViolations reaches zero,
  • it uses targeted operator selection when possible,
  • it forbids acceptance of moves that worsen hard-violation count.

Phase 1.5 as bounded intensification engine

Phase 1.5 is a separate sub-engine with its own temperature variable, acceptance logic, and stagnation counter.

This is architecturally important because intensification is not simply a flag inside the main loop. It is a standalone procedure with restart semantics.

In the current branch, that procedure also has:

  • an explicit global Phase 1.5 budget cap,
  • an optional exact-name targeted operator set,
  • optional tabu gating inside Phase 1.5,
  • a per-attempt early-stop rule based on the global best hard-violation objective.

Phase 2 as constrained refinement engine

Phase 2 reuses much of the main-loop structure but changes the acceptance baseline from currentHardViolations to bestHardViolations.

This design lets the solver continue exploring while protecting the best hard-feasibility boundary achieved so far.

Operator adaptation architecture

Operator adaptation is a light online-learning subsystem rather than a full reinforcement-learning module.

Statistics model

Each operator has four counters:

  • attempts,
  • improvements,
  • accepted,
  • successRate.

The counters are updated incrementally after every selection and every accepted move. successRate is recomputed as improvements / attempts.

Selection model

OperatorSelectionPolicy uses smoothed success rates with alpha = 1 and beta = 2, which provides modest exploration pressure for operators with low sample counts.

In hybrid mode, a 30 percent random branch is preserved indefinitely. There is no warm-up stage followed by a pure exploitation stage.

Hard-constraint hinting architecture

One of the more domain-aware parts of the architecture is the hard-fix hinting system in generateNeighbor(...).

Cache design

The solver caches a Set<string> of violated hard-constraint names for 50 iterations.

This cache avoids recomputing the violated hard-constraint set on every iteration, which is a useful compromise between reactivity and cost.

Heuristic design

The architecture uses string-matching heuristics rather than formal metadata. It compares lowercased constraint names against lowercased operator names and prefers combinations that appear semantically aligned.

This makes the system simple and practical, but it also means naming conventions matter.

Tabu architecture

The tabu subsystem is intentionally minimal and composable.

Storage strategy

The backing store is a Map<string, number>, where the value is the insertion iteration.

This representation has two benefits:

  • membership tests are O(1) on average,
  • cleanup can be based on relative age without additional metadata.

Signature strategy

The signature subsystem uses a cascading strategy:

  • custom domain signature when provided,
  • compact schedule-based signature for timetable-like states,
  • deterministic generic serialization otherwise.

Architecturally, this improves portability across domains without forcing a single canonical state layout.

Failure strategy

If signature generation cannot be made deterministic, the solver throws a plain Error asking the caller to supply getStateSignature(...).

This is one of the few places where failure bypasses the SAError hierarchy.

Telemetry architecture

Observability is built into the runtime rather than layered on afterward.

Logger design

Logger is synchronous and intentionally simple.

  • it sanitizes nested data,
  • it redacts sensitive keys such as password, secret, token, and related variants,
  • it writes human-readable log lines,
  • it creates parent directories automatically for file output.

The logger does not implement batching, structured transport abstractions, or async I/O. Its design favors predictability over maximal throughput.

Progress reporter design

ProgressReporter maintains mutable internal counters independent of solver state objects.

This subsystem tracks:

  • accepted and rejected moves,
  • stagnation count,
  • iteration of the best cost,
  • current phase,
  • tabu hits,
  • last progress iteration,
  • initial cost.

This separation makes callback reporting cheap and avoids coupling telemetry to the domain state representation.

Error isolation

Progress callback failures are isolated from the optimization engine. Exceptions are caught and forwarded to the logger as warnings.

From an architectural standpoint, telemetry is therefore best-effort, not part of the correctness-critical path.

Error architecture

The explicit error taxonomy is small and easy to reason about.

SAError
  -> SAConfigError
  -> ConstraintValidationError
  -> SolveConcurrencyError

The taxonomy is used for:

  • constructor-time validation failures,
  • runtime score-contract violations,
  • instance-level concurrency violations.

Not every runtime failure is wrapped into this hierarchy. For example, user-thrown exceptions from evaluate() or failures in fallback signature generation may propagate as plain Error instances.

Complexity discussion

The codebase does not publish formal asymptotic guarantees, but the dominant cost per iteration is easy to characterize.

Time cost

Each accepted or evaluated candidate typically requires:

  • one cloned state,
  • one move application,
  • evaluation of all relevant constraints,
  • optional tabu signature generation,
  • optional logging and progress checks.

In practical terms, runtime is usually dominated by:

O(iterations * (constraint evaluation + move generation + signature cost))

Space cost

Space consumption is primarily driven by:

  • the current and best state objects,
  • the tabu map,
  • telemetry counters,
  • transient strings used for signatures and logs.

Design consequences for maintainers

The current architecture is compact and effective, but it implies several maintenance principles.

  • Keep score semantics consistent across all docs and tests: higher score means better satisfaction.
  • Be careful when changing operator names because Phase 1 heuristics depend on them.
  • Treat progress callbacks as observational and non-authoritative.
  • Preserve deterministic signature behavior whenever state structure evolves.
  • Prefer adding focused modules over growing SimulatedAnnealing.ts further, because it already concentrates most orchestration complexity.

Next steps

For adjacent technical detail, continue with:

On this page