Checkpoints and migration
Saving and resuming
Checkpoints keep the full Rust state needed to resume without replaying old contests. Save after a batch and load before the next one:
from pathlib import Path
from elo_mmr_py import rate_latest
path = Path('ratings.json')
rate_latest(first_batch, save_checkpoint=path)
ratings = rate_latest(
second_batch,
load_checkpoint=path,
save_checkpoint=path,
)
For a long-running service, Rater keeps the same state in Rust and avoids a
checkpoint read and write between ordinary updates. Its tested example lives in
the API reference.
The save is written to a temporary file in the destination directory, flushed,
and atomically renamed over the target. On Unix, the parent directory is also
synchronized after the rename so the directory entry survives a power loss.
On Unix, the replacement copies an existing regular file's POSIX mode bits; a
new checkpoint is owner-only (0600). It does not copy ACLs, extended
attributes, or ownership. No promise is made to retain custom Windows DACLs;
protect the destination directory with the access policy required by your
deployment.
Saving to a symbolic-link path replaces the link itself with a new regular checkpoint and leaves the former link target untouched. Pass the resolved target path instead if that target is the file you intend to replace.
A write or rename failure raises OSError and never exposes a partially
serialized target file. If the final parent-directory synchronization fails,
the complete replacement may already be visible even though OSError is
raised; callers should inspect or retry the checkpoint before continuing.
Version 2 schema
The top-level JSON object is:
{
"format_version": 2,
"package_version": "<installed package version>",
"system": "mmr",
"system_config": {
"weight_limit": 0.2,
"noob_delay": [],
"sig_limit": 80.0,
"drift_per_day": 0.0,
"split_ties": false,
"subsample_size": null,
"subsample_bucket": 0.00001
},
"mu_noob": 1500.0,
"sig_noob": 350.0,
"contests_processed": 42,
"last_time_seconds": 1700000000,
"players": {}
}
players contains the serialized upstream state. Treat it as implementation
data. The system, resolved configuration, initial parameters, timeline, and
player state are validated before calculation begins. package_version records
which package wrote the file; it is metadata, not a compatibility check.
Checkpoints are compact JSON terminated by one newline. system_config is the
fully resolved Elo-MMR configuration, or null for other system families.
With the same package version and inputs, continuing from a canonical v2
checkpoint preserves finite floating-point state bit-for-bit and produces the
same result as processing the contests in one uninterrupted run.
The v2 envelope is intentionally strict: missing required fields and unknown
fields are rejected instead of being silently ignored. A future schema change
that old readers cannot validate safely must increment format_version; no
forward-compatibility promise is made from an older package to a newer format.
contests_processed is the next global event offset. Ignored outcome-free
contests count toward it, so later PlayerEvent.contest_index values remain
stable across batches. last_time_seconds enforces chronology across calls.
Migrating version 1.x
Version 1.x wrote only the player map. Version 2 recognizes that shape, infers
the offset as max(contest_index) + 1, and infers the last timestamp from player
state. The next successful save writes the v2 envelope automatically.
Because a legacy file has no system or initial-parameter metadata, those values
cannot be verified while reading it. Once migrated, every continuation must use
the stored system, mu_noob, and sig_noob. Packages from the 1.x line are not
expected to read v2 checkpoints.
Rater.load() intentionally accepts canonical v2 only. Use rate() or
rate_latest() once to load and resave a legacy file before opening it through
Rater.
Exception categories for malformed data and filesystem failures are defined in the Errors reference.