Determinism and the RNG model¶
Every sezgi run is deterministic under a fixed (master_seed, run_id): the
same triple (problem, budget, seed) (and, for multi-run experiments,
run_id) always produces byte-identical results — same best_f, same
best_x, same evals_used — in this build, run after run, and (for a
shared built-in algorithm) across the Python and R frontends alike. This
page explains the mechanism, both for the pure-Rust path and for a
Python-authored Algorithm.generate() callback.
The generator: RngStream¶
sezgi's RNG core (crates/core/src/rng.rs) is a hand-written
xoshiro256++ generator (Blackman & Vigna 2019), seeded via SplitMix64
(Steele et al. 2014). Two operations matter for determinism:
RngStream::from_master(master_seed, path)folds a master seed and an arbitrary&[u64]path down to one digest (via repeated SplitMix64 mixing), then expands that digest into the four-word xoshiro state. Two calls with the same(master_seed, path)always produce the exact same stream.RngStream::split(child_id)derives an independent child stream from a parent stream's current path digest — used for restart-spawned sub-populations (Engine::run's ownrestart_rng.split(restart_count),engine.rs:215).
golden_values_pinned (rng.rs's own test suite) pins three concrete
u64 draws for RngStream::from_master(123, &[1, 2]) — a regression test
that fails immediately if the implementation ever changes accidentally.
One master seed, many independent streams¶
Engine::run derives a separate RngStream for every role, all from
the same (master_seed, run_id) but at different paths
(engine.rs:91-106):
| Role | Path |
|---|---|
| Initializer | [run_id, 0] |
| Boundary repair | [run_id, 1000] |
Stage i's generator |
[run_id, 1 + 2*i] |
Stage i's replacer |
[run_id, 2 + 2*i] |
Stage i's adapter |
[run_id, 1_000_000 + i] |
| Restart | [run_id, 2_000_000] (children via .split(restart_count)) |
Because every role's path is disjoint, changing one component's behavior
(e.g. swapping a Generator) never perturbs another role's draw sequence
— engine.rs's own test suite verifies this directly
(stage_rng_streams_use_the_documented_per_stage_indices,
absent_adapter_and_restart_change_nothing).
The Python callback bridge¶
A Python-authored Algorithm.generate(pop, ctx) draws from ctx.rng
(_sezgi.PyRng, py-sezgi/src/lib.rs) — but ctx.rng is not a separate,
Python-only RNG. PyGenerator::generate (the Rust struct backing every
engine-hosted Python callback) clones the engine's own stage RngStream
out into an owned PyRng immediately before the call, hands it to Python
via EngineCtx.rng, and writes its final state back into the engine's
own *ctx.rng immediately after the callback returns — before offspring
conversion, so the write-back happens unconditionally. Every
ctx.rng.next_f64()/next_below(n) draw a Python callback makes therefore
flows through exactly the same house stream a built-in Rust Generator
would have used at that call: same seed ⇒ same sequence, regardless of
which language produced the offspring.
One consequence worth stating plainly: a PyRng handle a callback stashes
from a past call stays usable as an ordinary Python object, but drawing
from it no longer affects the engine's own stream — each generate call
hands out a fresh clone and writes back only that call's own handle, so a
stale stashed handle is permanently inert for run determinism (drawing from
it changes nothing but that stale copy).
Seed to draw, as a diagram¶
flowchart TD
SEED["master_seed, run_id\n(RunConfig)"] --> PATHS["RngStream::from_master(seed, [run_id, path])\n-- one call per role, disjoint paths"]
PATHS --> INIT["init stream\npath=[run_id, 0]"]
PATHS --> BOUND["boundary stream\npath=[run_id, 1000]"]
PATHS --> STAGE["stage i generator/replacer streams\npath=[run_id, 1+2i] / [run_id, 2+2i]"]
PATHS --> ADAPT["adapter i stream\npath=[run_id, 1000000+i]"]
PATHS --> REST["restart stream\npath=[run_id, 2000000]"]
STAGE --> RUSTGEN{"Rust Generator\nor Python Algorithm.generate?"}
RUSTGEN -->|Rust| DRAW1["Generator draws directly\nfrom &mut RngStream"]
RUSTGEN -->|Python callback| CLONE["PyGenerator clones the SAME\nRngStream out into a PyRng"]
CLONE --> PYCALL["Python callback draws via\nctx.rng.next_f64() / next_below(n)"]
PYCALL --> WRITEBACK["PyRng's final state written back\ninto the engine's *ctx.rng"]
DRAW1 --> SAME["same seed => byte-identical\nRust or Python-authored run"]
WRITEBACK --> SAME
The Python-callback bridge, call by call¶
sezgi.Algorithm.run() calls _sezgi.solve_with_py_generator(self,
native_problem, budget, master_seed=seed, ...), which registers self as
a synthetic "py/generator" component and runs it through the exact same
Engine::from_spec + engine loop every built-in preset uses. Each time
that stage's Generator::generate is due, here is what actually happens
(PyGenerator::generate, py-sezgi/src/lib.rs):
sequenceDiagram
participant Engine as Engine::run (Rust)
participant Bridge as PyGenerator::generate (Rust)
participant Py as Python Algorithm subclass
Engine->>Bridge: generate(&pop, &mut ctx)
Bridge->>Bridge: Python::with_gil(...)
Bridge->>Bridge: build PopView (individuals, fitness)
Bridge->>Bridge: build space descriptor list
Bridge->>Bridge: clone ctx.rng out into an owned PyRng
Bridge->>Bridge: build EngineCtx { iteration, space, rng }
Bridge->>Py: callback.generate(pop_view, engine_ctx)
Py->>Py: ctx.rng.next_f64() / next_below(n) draws
Py-->>Bridge: return offspring (iterable of x-values)
Bridge->>Bridge: write PyRng's final state back into *ctx.rng
Bridge->>Bridge: genotype_from_py(...) each offspring item
Bridge-->>Engine: Vec<Genotype> offspring
A Python exception raised inside generate/initialize/validate_space
propagates unchanged (via panic::panic_any(PyErr), caught by
run_with_bridge's catch_unwind) — the same propagation pattern sezgi's
other Python-callback bridges (call_callable/call_callable_spaced) use.
Algorithm.initialize follows the identical clone-out/write-back protocol
via a mechanically parallel PyInitializer struct, registered only when a
subclass actually overrides initialize (detected by function-identity
comparison against Algorithm.initialize itself, in algorithm.py's
Algorithm.run).
A runnable determinism check¶
_sezgi.PyRng.from_master is documented as an advanced/testing surface —
exactly the tool needed to demonstrate the claim above directly, without
needing a full run:
from sezgi._sezgi import PyRng
a = PyRng.from_master(master_seed=42, path=[0, 1])
b = PyRng.from_master(master_seed=42, path=[0, 1])
c = PyRng.from_master(master_seed=42, path=[0, 2])
draws_a = [a.next_f64() for _ in range(3)]
draws_b = [b.next_f64() for _ in range(3)]
draws_c = [c.next_f64() for _ in range(3)]
print("same (master_seed, path) reproduces the same draws:", draws_a == draws_b)
print("a different path diverges immediately:", draws_a[0] != draws_c[0])
same (master_seed, path) reproduces the same draws: True a different path diverges immediately: True
Next¶
This is the last page of the concepts and architecture section. See the API reference for the full generated Python surface, or return to the Learn track.