Development documentation — 0.3.0a4 / main
This is the development branch, not a published stable release. Use the source installation guide. Functional checks cover Ubuntu, Windows and macOS; interpreter, artifact and optional-feature limits still apply. Read support, release gates and known limitations before relying on results.
Usage and API guide
Start with the quickstart for a first sequential solve. This guide
covers configuration, execution modes, callbacks, result accounting and stopping.
For one algorithm use a public solver such as SA(...), followed
by the same .solve(time_limit=...) call and settings as RKO.
The parameter reference explains every fixed scalar and adaptive
mode; the extension guide covers custom operators and algorithms.
Commands and example paths assume the repository root. The Python library needs
no C++ checkout. A matching prebuilt package needs no compiler; source installation
requires Rust. See installation and version boundaries.
- Solver configuration
- Execution modes
- Offline tuning
- Process-safe callbacks
- Results and accounting
- Custom problems and stopping
- Terminal adapter
Contributor setup and validation are also summarized in CONTRIBUTING. C++ comparison goals and native-engine limits are in performance.
Development setup
Requires Python 3.14+ and uv:
git clone https://github.com/MarcosPTProenca/rko-solver.git
cd rko-solver
uv sync --locked --group dev
uv run --locked pre-commit run --all-files
For automatic commit checks, run uv run pre-commit install if Git uses its
standard hooks directory. If core.hooksPath is centrally managed, integrate
pre-commit with those existing hooks instead; no global Git settings are changed
by this setup.
The distribution is named rko-solver and imports as rko_solver. The published
0.3.0a4 predates current development changes; a matching version string does not
identify identical source. Follow the matching installation guide.
Attribution and licensing boundaries are in NOTICE.
Class-based solver API
RKO(decoder, cost, n_variables, ...).solve(time_limit) minimizes
cost(decoder(keys)). The first argument may instead be a problem object that carries
the problem definition: rko_solver.Callbacks(decoder, cost, n_variables),
rko_solver.Numba(function, n_variables), rko_solver.Blocks(function, n_variables, batch_size) or a
built-in such as rko_solver.problems.Knapsack(...); then cost and n_variables are not passed. The constructor stores configuration without reading files
or starting workers. Each solve builds a fresh portfolio and returns an immutable
RKOResult with keys, cost, elapsed_seconds, engine, execution, algorithms and
random_state. solver.result_ holds the latest successful result and is cleared
before every attempt, including failed solves.
Constructor options
The first three arguments are required in the short form. The others are keyword-only:
| Argument | Meaning / default |
|---|---|
problem |
First argument: a problem object, or the decoder in the short callback form |
decoder |
Keyword spelling of the short form's decoder; do not also pass problem |
cost |
Callable mapping that payload to a finite scalar to minimize |
n_variables |
Positive integer chromosome length |
algorithms=None |
Nonempty selection of canonical names, otherwise packaged TOML portfolio |
adaptation=None |
Configured mode; "fixed" settings or "adaptive" Q-learning where supported |
control=None |
Legacy alias: 0 fixed or 1 adaptive; conflicting aliases are rejected |
pool_size=None |
Configured elite capacity; not a prebuilt population |
prefill_pool=None |
Configured random-pool startup policy; packaged default is off |
initial_keys=None |
Optional copied matrix of caller-supplied random-key vectors evaluated before search |
params_dir=None |
Custom TOML directory, otherwise installed resources |
engine="python" |
"python", "rust" (compiled engine), "cpp" (reference engine, opt-in build) or "auto"; see Rust and C++ engines |
execution=None |
Python engine only: "process" (default), "thread" or "sequential"; rejected for other engines |
auto_time=None |
For engine="auto", separate probe allowance; defaults to 10 percent of the final search budget |
auto_candidates=None |
Optional supported subset of automatic-engine probe candidates; see automatic selection |
n_jobs=None |
Process or Rust worker cap; process default uses discovered CPU capacity, Rust default one thread per algorithm |
random_state=None |
Nonnegative library seed, or fresh assigned seeds |
verbose=False |
Opt-in lifecycle/shared-pool progress to stdout |
max_evaluations=None |
Positive number of completed evaluations after which each algorithm stops, in every engine; see evaluation limit |
history_limit=0 |
Opt-in per-search scalar history cap, 2–10000; see interactive reports |
trace=None |
Opt-in bounded original-decoder events with TraceConfig; see observed decisions |
hyperparameters=None |
Partial named scalar overrides, requiring adaptation="fixed" (or control=0) |
solve(time_limit) requires positive finite seconds, at most one year (31,536,000 s). Other
upper bounds, checked before any engine starts and with the same message on every engine:
auto_time the same year; max_evaluations at most 2^63 - 1; pool_size and the population
sizes p, size_pop and p_size (including every value of an online grid) at most 1,000,000;
and each of those times the number of keys at most 100,000,000 values (about 800 MB of
float64). They exist to stop absurd or hostile values, not real runs. Invalid dimensions/settings,
callback serialization and nonfinite costs are rejected, not silently repaired.
Process worker failures can surface as RuntimeError with their original cause;
startup/execution watchdogs can raise TimeoutError. Parameter I/O can raise
OSError; user callback exceptions are not converted into a successful result.
A fresh solve validates again, so mutating settings can make a later attempt fail.
Do not call solve concurrently on the same instance. See stopping details below;
these are not guarantees that arbitrary callbacks terminate promptly.
Select algorithms, control and pool_size directly in Python; omitted values
use the packaged TOML defaults. By default pool_size is capacity, not an initial
population. Opt in with prefill_pool=True (CLI --prefill-pool) for
reference-style shared initialization.
Startup has its own cooperative allowance equal to time_limit, followed by fresh
search allowances; total elapsed can increase. result.initialization reports its
work separately, and result.n_evaluations includes startup plus searches once.
Pass initial_keys to offer known vectors to the shared pool. Each row must have exactly
n_variables finite values in [0, 1). The solver copies the matrix at construction; an empty
matrix is a no-op. Every supplied row is evaluated once, including duplicate rows, after optional
random prefill and before search. The pool applies its usual capacity and distinct-key rules, so it
may discard a worse vector; every input row is still evaluated. result.initial_keys reports the
number of supplied rows and their completed evaluations, separately from result.initialization
(random prefill), and result.n_evaluations includes both. C++ retains its existing reference
prefill even when prefill_pool=False. With engine="auto", calibration probes do not evaluate
initial_keys; only the final selected run does. A direct model solve that bypasses search rejects
nonempty initial_keys.
The 1,440-pair quality expansion finds broader but
non-universal IPR benefits, mixed GRASP and mostly neutral MultiStart; keep startup
opt-in. Large-GRASP gains can be startup-only before construction completes.
SA now implements its requested online control
rather than falling back to offline parameters; use control=0 for its legacy
fixed-parameter path. See adaptive SA and migration.
GRASP now samples the wider reference lattice in both
control modes; seeded trajectories and callback counts change without a new option.
Its grid step is halved only after a construction that fails to improve the global
best, and the initial random vector is scored and published without refinement,
matching the reference grid progression; seeded trajectories also change here.
The alpha axis follows the reference compatibility policy:
it affects adaptive states/actions, not construction directly; no RCL is implied.
With control=0, hyperparameters supplies partial
per-algorithm scalar overrides without editing files. params_dir remains
available for custom defaults and adaptive grids. Library calls are quiet by default; verbose=True on RKO prints lifecycle
and attributed shared-pool improvements. rko_solver.run(...) retains the old tuple API
and also accepts verbose=True.
Execution modes
The engine chooses who runs the search. execution only exists for the Python engine:
engine="python", execution="process"(default):n_jobs=Noneuses at mostmin(number_of_algorithms, os.process_cpu_count() or 1)search workers, plus one manager hosting the shared elite pool. CPU discovery is affinity-aware; unavailable discovery falls back to one. A positiven_jobsoverrides this automatic cap, even above available CPUs, without dropping algorithms: assignments are round-robin and each worker runs its assigned searches sequentially. Before each search, that worker divides the time left before the common deadline by its number of remaining searches. For example, ten algorithms withn_jobs=4form groups of 3, 3, 2 and 2; their initial shares are approximately one third or one half of the budget. Early completion leaves more time for queued searches in the same worker; no search extends the original deadline. This replaces the previous fixed longest-group slice; shorter groups no longer deliberately finish early. Smaller limits change cooperation and per-algorithm budgets; they are not a promise of better optimization quality. Active callbacks can overrun a slice and leave later searches less time or no time. Algorithms sharing a worker also share its callback instances; mutable callback state is not reset between searches. Native BLAS/OpenMP threads are separate from this worker limit; use the native-thread launch guide for numerical callbacks.engine="python", execution="thread": shared-memory execution; local callbacks work. Processor-bound Python callbacks share the global interpreter lock when it is enabled. Free threading does not remove the need to manage callback state or establish a speedup.engine="python", execution="sequential": debugging/reference execution with shared elites. Each algorithm gets its own time budget, so total search time can approach algorithm count timeslimit_time. This is not an equal-wall-time benchmark against concurrent modes.engine="rust": the compiled engine; Python callbacks require the global interpreter lock when it is enabled. Built-in problems avoid Python callbacks. See Rust and C++ engines.engine="cpp": the C++ reference algorithms, available in builds made withRKO_BUILD_CPP=1.engine="auto": short probes choose among the others for your problem.
Development main rejects backend=. RKO.backend, RKOResult.backend, RKO.native,
RKO.compiled, RKO.batched and the deprecated import shims are removed; use
engine, execution and canonical problem objects. See
the replacement table. Published 0.3.0a4
predates this removal; use the documentation matching your source or package.
IPR consumes elite pairs. If fewer than two elites exist at startup, it inserts its initial evaluated candidate and tries at most one additional random seed, using the same decoder/objective and assigned deadline. This makes standalone or first-in-group IPR able to relink without waiting for a producer queued behind it. An already usable pool retains its initialization draw order. Pair occupancy alone is not population diversity for custom providers. Stock pools now retain one best observation per chromosome, including managed pools; IPR skips identical effective parents. See the fixed-work diagnosis for limits. IPR now omits unchanged-block evaluations, retaining virtual path decisions for deterministic scoring; stochastic/stateful callbacks receive fewer observations and can change results. Use a pool capacity of at least two; a pool unable to retain a pair still uses bounded waiting after those seed attempts. Seeds are not domain-specific feasibility repairs. Process occupancy queries transfer only a count rather than the entire pool.
Algorithm defaults live in packaged rko_solver/parameters/ files.
Use algorithms=["SA", "GA"] to select a smaller portfolio without copying files.
Custom default TOMLs can still be supplied through params_dir.
Constructors and configured runs validate scalar domains, population sizes and
joint constraints before execution. Online grids retain named axes and exclude
infeasible joint combinations (such as reversed perturbation bounds); an entirely
infeasible grid is rejected. No scalar value is silently truncated.
Tuning offline parameters from Python
Use the existing offline parameter names, grouped by selected algorithm:
from rko_solver import RKO
solver = RKO(
decode, objective, n_variables=64,
algorithms=["PSO", "GA"], adaptation="fixed",
hyperparameters={
"PSO": {"p_size": 12, "w": 0.6},
"GA": {"size_pop": 20, "prob_mut": 0.1},
},
n_jobs=2, random_state=42,
)
This uses the importable callbacks and guarded process entry point shown below.
Each solve snapshots the mappings, resolves aliases such as GeneticAlgorithm
to GA, and merges the values into fresh TOML defaults. Unspecified values keep
their defaults; neither input mappings nor installed files are modified. Required
TOMLs must still exist, including when using a custom params_dir.
Unknown parameter names, duplicate alias mappings, unselected algorithms,
nonfinite values, booleans and invalid scalar/joint domains raise ValueError
before execution. Exact integral floats are accepted for integer parameters;
fractional values are never truncated. Unknown-key errors list supported scalar keys.
All engines and execution modes, including grouped process workers, receive the same owned settings.
Nonempty scalar overrides require control=0. Adaptive grids remain configured
through named [online] TOML axes; control=1 rejects scalar overrides rather than
silently replacing tuned values during adaptation. Result reports retain the
validated initial scalar settings and actual adaptive axes used by each search.
Low-level kernel constructors (explicit implementation-module imports, not the
managed public solver constructors) use package-provided local search and shaking
by default. Kernels that perform local search accept a typed
local_search_operator; SA, ILS and VNS also accept shaking_operator. These
callables are optional overrides, not required problem callbacks. Their public
contracts are LocalSearchOperator and ShakingOperator from
rko_solver.metaheuristics; local-search overrides receive pool= as a keyword.
adaptation (or legacy control), pool_size and prefill_pool are supported under [run]. Remove the formerly
ignored strategy, max_runs and debug fields from custom configurations;
unsupported keys now raise an error. Portfolio local search currently uses
best-improvement defaults. Repeat solve for independent trials and use standard
Python logging for diagnostics.
Spawn-compatible callbacks
Use module-level functions, functools.partial, or callable instances carrying
serializable state. Lambdas and nested functions are rejected in process mode.
The defining module must be importable in a fresh interpreter. Run from a script
rather than an interactive notebook cell, and guard the entry point:
import numpy as np
from rko_solver import RKO
def decode(keys):
return keys
def objective(solution):
return float(np.sum(solution**2))
if __name__ == "__main__":
solver = RKO(
decode, objective, n_variables=10,
algorithms=["SA", "GA"], n_jobs=2, random_state=42,
)
result = solver.solve(time_limit=5.0)
print(result.keys, result.cost, result.elapsed_seconds)
Result accounting and configuration snapshots
RKOResult retains its original fields and adds immutable searches reports in
portfolio order, plus n_variables, requested time_limit, control, pool_size,
n_jobs, process-only available_cpus and runtime versions (RKO distribution,
Python, NumPy). CPU capacity is snapshotted once per process solve, so changing
affinity later does not rewrite earlier resource accounting. The derived
n_workers excludes pool managers and native numeric threads; it is configured
search concurrency, not measured CPU utilization.
print(result.n_evaluations, result.stop_reason, result.n_workers)
for search in result.searches:
print(search.algorithm, search.n_evaluations, search.stop_reason)
print(search.random_state, dict(search.parameters), search.parameter_grid)
With history_limit > 0, each search also exposes immutable .history points
(local elapsed seconds, evaluations, incumbent cost), .history_limit and
.history_observations. Recording is bounded; compacted curves are approximate.
Interactive reports explains offline HTML/JSON exports and their
privacy and interpretation limits. Capture is disabled by default.
n_evaluations counts successfully completed finite objective callback calls
across every algorithm, including initialization and local search—not just
improvements or the winning worker. Decoder calls, rejected/nonfinite objectives,
callback-internal work and post-solve user evaluations are not counted. Failed
solves still raise and clear result_; they do not publish partial success reports.
Each exported SearchMetadata records its actual assigned seed (even when the
requested seed was None), effective control, validated initial scalar parameters,
ordered adaptive grid axes, assigned search seconds and measured session seconds.
Adaptive grids follow the algorithm's schema order; these are input snapshots,
not final adaptive parameters or a history of visited states. Unsupported adaptive
algorithms report effective control zero. Workers report their own constructed
settings rather than rereading files later in the parent. Reports contain no
callbacks, populations or mutable RNG objects and support standard dataclass/
pickle serialization. Aggregate properties are derived, not dataclasses.asdict
fields. Legacy manually constructed six-argument results have unknown accounting
(n_evaluations=None, stop_reason="unknown"). run() and CLI CSV formats remain
unchanged.
Search reasons are completed (returned before its deadline, not a convergence
claim), time_limit (deadline stopping or deadline elapsed by return), max_evaluations
(the evaluation limit was reached), cancelled
(peer-failure signal), and not_started (queued search had no remaining budget;
its assigned time_limit is zero). Process search limits are actual allocations
computed at dispatch, not equal fixed fractions or the requested portfolio budget.
The portfolio reason is time_limit if any search used its budget or could not
start, otherwise max_evaluations if any search reached its limit; detailed reports
preserve the distinction. Unstarted entries retain their
assigned zero budget and have zero evaluations and zero elapsed search time. Cancelled peers normally belong
to a failed portfolio, which raises instead of returning a result. Session elapsed
time excludes startup and queueing; result.elapsed_seconds remains end-to-end.
These numeric settings and runtime versions are useful provenance, not a complete replay artifact: callback/data identities, development commit hashes, callback RNG state, adaptive trajectories and cooperative scheduling are not captured. Owned seeds do not promise identical timed portfolio results. Callback overruns can still exceed a reported budget.
Evaluation limit and reproducibility
max_evaluations=N caps each algorithm at N completed objective
evaluations, on the Python, Rust and C++ engines alike; time_limit still applies and
whichever comes first ends the search (search.stop_reason says which). It does not count
random pool prefill or caller-provided initial_keys, which are reported separately
under result.initialization and result.initial_keys. The C++ reference's mandatory
10-vector prefill is likewise reported under result.initialization.
Because it does not depend on machine speed, it is the way to compare engines, write
regression tests or repeat a run.
result = RKO(decode, cost, n_variables, execution="sequential", max_evaluations=5000,
random_state=42).solve(time_limit=60.0)
assert all(0 < s.n_evaluations <= 5000 for s in result.searches)
Measured on a 60-item Knapsack with the default portfolio and a generous time limit, every
search ended at exactly the limit in all five configurations (Python sequential, thread and
process, Rust and C++), with block objectives truncated at the limit instead of overshooting
it. Two runs with the same random_state returned identical keys, costs and counters for
Python sequential, Python process with n_jobs=1 and Rust with one worker (the default
for Python callbacks and block objectives); they differed for Python thread, for several
concurrent Rust threads and for C++, where algorithms exchange elites concurrently or
(C++) draw part of the descent from an unseeded generator. The limit is exact for every
configuration; only reproducibility depends on the worker setup.
Adapting a custom problem
Keep the problem-specific reader outside RKO. Load the data once, then pass a
one-dimensional NumPy key vector to a decoder that returns your domain solution;
the objective receives that solution and must return a finite scalar to
minimize. For execution="process", callbacks must be importable and
pickleable: use module-level functions or callable objects that hold the loaded
data. The Spawn-compatible callbacks example above shows a runnable setup; for
a structured problem, replace its identity decoder and sphere objective with
your own adapter. RKO itself does not parse problem files or assume a solution
representation.
random_state gives distinct, repeatable seed allocation to privately owned
Python and NumPy RNGs in all three execution modes, including threads. Library
operations do not reseed or restore caller-global generators, so independent
solver instances can overlap without contaminating each other's library draws.
Timed searches still depend on scheduling and elite exchange; identical seeds
do not guarantee identical results.
Custom decoders, objectives and operator overrides must manage their own
randomness and concurrent mutable state. In particular, random_state no longer
implicitly seeds global RNG calls inside callbacks, and callback-owned global
draws are not rolled back after a sequential solve. Use explicitly owned generators
for stochastic callbacks. Standalone built-in operators called outside RKO keep
their existing global-RNG behavior. A single mutable RKO instance is not intended
for simultaneous solve calls.
Process startup is excluded from the requested search duration. On WSL, Windows-mounted virtual environments and editable source can dominate short runs; see tested Linux-local setup guidance. Workers start searching together after construction. A per-search callback session carries the same deadline through initialization and every nested operator, stopping between decoder/objective calls and preserving the best completed evaluation. A first valid evaluation is allowed even if initialization used the budget. User callbacks receive ordinary decoded payloads; candidate arrays are isolated from decoder mutation. Low-level operator calls made outside RKO do not acquire this budget.
Startup has a 60-second watchdog; process execution has five seconds of grace
for callbacks that are still running. When a callback overruns that grace after at least one
solution was found, the workers are stopped and solve returns the best solution found with a
RuntimeWarning; the stopped searches are reported with stop_reason="time_limit" and zero
evaluations, because their counts are lost with the process. If no solution exists yet, or
startup or initialization exceeds its watchdog, solve raises TimeoutError. Thread mode propagates worker
failures and cooperatively cancels peers, but cannot force-kill a blocked Python
thread. This is not a hard real-time API. Algorithm failures identify the search
and preserve exception causes. The parent cleans up processes and the manager
on failure or keyboard interruption.
The managed pool is a correctness-first baseline. IPC and process startup can outweigh parallelism on tiny problems; no universal speedup is promised.
Run an instance from the terminal
Like C++ runTest INSTANCE T, the supplied problem adapter runs Knapsack:
uv sync --locked
uv run --locked python -m examples.run_kp_cpp_instances examples/instances/kp50.txt \
--limit-time 10 --execution process --random-state 42 \
--output-csv results/kp50.csv
The terminal command shows clock/elapsed timestamps and new-best algorithms, plus
pool status approximately every second even during plateaus. Add --quiet for
final results only.
Pass --params-dir configs/my-run to select your own config.toml and algorithm
TOMLs. See running an instance for copying/editing configuration,
a four-worker portfolio, instance format, PowerShell and legacy batch selection.
The supplied runner does not infer arbitrary problem formats: other problems
require their own reader/decoder, analogous to adapting C++ Problem.h.
The two reference instances live in examples/instances/, with upstream
revision and MIT license, in the repository only. No C++ checkout or PYTHONPATH setting is
required when running from the repository root.
Omit --output-csv to save under the caller's results/ directory.
The decoder uses key > 0.5. Cost is negative profit with a penalty of 100000
per excess unit of weight, matching the reference example. Independently
computed exact optima are 31 for kp10.txt and 149 for kp50.txt. The
CSV records feasibility, objective, engine, execution, requested worker budget, seed and
end-to-end elapsed time. A blank budget requests automatic affinity-aware process
workers; it is not a measured worker count. To retain dedicated workers, explicitly
request the selected portfolio size. See default-worker migration.
These two small instances are correctness checks, not production-scale evidence.
The historical benchmark preserves its source-identified reference CSV separately from the runnable current-checkout experiment. In the archived October 18 run, all six trials were feasible, but the best process gap was 17.82%, versus 0.00474% for a simple greedy baseline. Those observations are not current-source measurements, competitive-quality evidence or a fixed-work speedup.
Quality checks
uv run --locked ruff check .
uv run --locked ruff format --check .
uv run --locked pytest tests -q
GitHub Actions runs the full suite on Linux, Windows and macOS. Tests include real spawn execution, shared elites, independent RNG streams, algorithm execution, callback exceptions, abrupt exits, blocked callbacks and interruption cleanup. Property-based tests use deterministic generated cases for pool, key-vector, validation, Q-learning and solver-result invariants.
Scoped mutation testing runs weekly and on demand because it reruns tests for generated
code mutations. It covers selected high-risk components listed in pyproject.toml;
per-module floors are enforced by tools/mutation_report.py. Surviving mutants still
need review; a passing score is not proof that every bug is detectable. The mutation run
excludes the runtime-Protocol conformance test because mutmut instrumentation
adds methods to Protocols; the test remains in the normal CI suite:
uv run --locked mutmut run
uv run --locked mutmut results
Release and evidence boundaries
The published prerelease and development main are distinct snapshots. Neither a
successful solve nor a passing installation check makes the library a stable release
or proves competitive optimization. Follow installation/version guidance,
current limitations, C++ differences and the
controlled release procedure.
Internal audits and session plans are not maintained public documentation.