The run registry¶
cn.runs records the computations you may want to find, compare, cite or re-run later, with
their numerics, code and machine.
It needs no service: an SQLite index plus one directory per run (a JSON record, an HDF5 file for series and fields, solver checkpoints) under a root you declare. Tutorial: 11. Record, query, reproduce: the run registry.
Where runs go¶
Set $CN_RUNS to the directory you choose (or pass root=); without it, a scratch directory
of the project is used. A default that resolves under your home directory is refused: runs
belong on scratch storage. cn.runs.explain() prints the root and every declared constant.
Recording¶
import numpy as np
import conatus as cn
def heat(kappa):
"""u_t = kappa u_xx, u(x, 0) = sin(3x): the amplitude decays as exp(-9 kappa t)."""
m = cn.Model(cn.Box(x=cn.periodic(2 * np.pi)), name="heat", kappa=kappa)
m.scalar("u")
m.equations("dt(u) = kappa*lap(u)")
return m.simulate(N=32, scheme="RK443", dt=1e-3).initial(u=lambda x: np.sin(3 * x))
def decay_rate(sim):
g = sim.grid("u")
x = np.linspace(0, 2 * np.pi, g.size, endpoint=False)
return -np.log(2 * np.mean(g * np.sin(3 * x))) / sim.t
sim = heat(0.2)
with cn.runs.track("heat_k0.2", tags=["decay"], params={"note": "first try"}) as run:
sim.solver.run(200, record=run) # problem hash, numerics, explain(), start/end state
rate = decay_rate(sim)
run.log(t=sim.t, rate=rate) # a row of series (in memory, written at the end)
run.result(rate=rate, rel_error=abs(rate - 1.8) / 1.8) # scalar results, queryable
run.save_field("u", sim.grid("u"), compression="gzip") # compression has no default
run.certify("decay rate = 9 kappa (closed form)", evidence={"rel_error": run.results["rel_error"]})
Other ways in:
Call |
Records |
|---|---|
|
one run per call: numerics, problem hash, parameters, code, hardware, Slurm job, start/end state; returns the run |
|
the same, through the native simulation; returns the run |
|
a finished object in one call; an |
|
one run per call: arguments as parameters, the return value as results / fields |
What is recorded, always: the structural hash of the problem, the values of its parameters, the
full numerical configuration and explain() text, the executed code (commit, dirty paths, sha256 of
the imported tree and of the compiled core), versions, host, GPU, batch job, the command line. Nothing is guessed: an unreadable field is recorded as such.
Recording never blocks or breaks the computation. Scalars and series are kept in memory; fields,
files and states are copied and written by a background thread through a bounded queue (a full
queue drops and counts, never waits); a disk error is recorded and reported by a
RegistryWriteWarning when the run closes, and the numbers are bit-for-bit those of an unrecorded
run (tested with an unwritable root).
Statuses: provisional (default), certified, retracted. Changing one needs a reason (and
optional evidence such as residuals); the history is kept, a retracted run is never deleted and never
reused. A run failing inside a with block is recorded as failed and the exception propagates.
Querying¶
import numpy as np
import conatus as cn
def heat(kappa):
"""u_t = kappa u_xx, u(x, 0) = sin(3x): the amplitude decays as exp(-9 kappa t)."""
m = cn.Model(cn.Box(x=cn.periodic(2 * np.pi)), name="heat", kappa=kappa)
m.scalar("u")
m.equations("dt(u) = kappa*lap(u)")
return m.simulate(N=32, scheme="RK443", dt=1e-3).initial(u=lambda x: np.sin(3 * x))
def decay_rate(sim):
g = sim.grid("u")
x = np.linspace(0, 2 * np.pi, g.size, endpoint=False)
return -np.log(2 * np.mean(g * np.sin(3 * x))) / sim.t
import socket
for kappa in (0.2, 0.4): # two recorded runs to query
sim = heat(kappa)
with cn.runs.track(f"heat_k{kappa}", tags=["decay"]) as run:
sim.solver.run(100, record=run)
run.log(t=sim.t, rate=decay_rate(sim))
run.result(rate=decay_rate(sim))
run.certify("decay rate = 9 kappa", evidence={"rate": run.results["rate"]})
print(cn.runs.find(problem=sim.solver.plan.problem, kappa__gt=0.15, status="certified",
tags=["decay"]))
print(cn.runs.find(rate__ge=1.0, host__contains=socket.gethostname())) # results, index columns
a = cn.runs.find(name="heat_k0.2")[0] # or cn.runs.get(id or unique id prefix)
b = cn.runs.find(name="heat_k0.4")[0]
print(cn.runs.compare(a, b).explain()) # parameters, numerics, results, code
cn.runs.compare(a, b).plot("rate", x="t") # both curves, each on its own abscissae
print(cn.runs.lineage(a).explain()) # parents, forks, reproductions, cache reuse
cn.runs.to_dataframe(tags="decay") gives the same rows as a pandas table (pandas is optional).
Filters are key=value or key__op=value with op in eq, ne, gt, ge, lt, le, in, contains. A key
is an index column (host, gpu, git_head, slurm_job, telemetry_id, …), else a parameter,
else a result. A key that is both a parameter and a result is refused (write param_Re or
result_Re), and an unknown key raises with a suggestion: a typo never returns an empty list.
Reproducing¶
import numpy as np
import conatus as cn
def heat(kappa):
"""u_t = kappa u_xx, u(x, 0) = sin(3x): the amplitude decays as exp(-9 kappa t)."""
m = cn.Model(cn.Box(x=cn.periodic(2 * np.pi)), name="heat", kappa=kappa)
m.scalar("u")
m.equations("dt(u) = kappa*lap(u)")
return m.simulate(N=32, scheme="RK443", dt=1e-3).initial(u=lambda x: np.sin(3 * x))
def decay_rate(sim):
g = sim.grid("u")
x = np.linspace(0, 2 * np.pi, g.size, endpoint=False)
return -np.log(2 * np.mean(g * np.sin(3 * x))) / sim.t
run = heat(0.2).run(steps=200, record="heat_k0.2") # a recorded run
solver = heat(0.2).solver # solver built by your script for the same problem
report = run.reproduce(solver)
print(report.explain()) # bit-for-bit identical? code identical?
The solver must have the same problem hash, parameter values and numerics; any difference is
refused with the list, never adapted. The recorded start state is loaded, the same number of steps
is taken in a new run (relation="reproduce"), and the whole end state (the state, every multistep
history array, the time and the iteration) is compared bit for bit: same dtype and same bytes, so
-0.0 differs from 0.0. A run recorded by 0.1.0 does not hold the given-field data, the user
functions and six numerical choices that 0.1.1 records: an IdentityWarning names them (they are
not compared) and the end state is still compared bit for bit. If the
executed code changed since the run, a CodeChangedWarning says how (require_same_code=True
makes it an error). A decorated function is re-imported and called with the recorded arguments.
The cache: only when you ask¶
import numpy as np
import conatus as cn
def heat(kappa):
"""u_t = kappa u_xx, u(x, 0) = sin(3x): the amplitude decays as exp(-9 kappa t)."""
m = cn.Model(cn.Box(x=cn.periodic(2 * np.pi)), name="heat", kappa=kappa)
m.scalar("u")
m.equations("dt(u) = kappa*lap(u)")
return m.simulate(N=32, scheme="RK443", dt=1e-3).initial(u=lambda x: np.sin(3 * x))
def decay_rate(sim):
g = sim.grid("u")
x = np.linspace(0, 2 * np.pi, g.size, endpoint=False)
return -np.log(2 * np.mean(g * np.sin(3 * x))) / sim.t
heat(0.4).solver.run(200, record=True) # computed and recorded
again = heat(0.4).solver.run(200, record=True, cache=True) # read back: CacheReuseWarning
print(again.cache_report["hit"])
With cache=True, an identical computation already in the registry is read instead of computed,
and a CacheReuseWarning says which run was reused and what was read (run.cache_report has the
key and its parts). Identical means the same identity of the computation (the one the checkpoint
guards and the HDF5 append also compare: problem hash, parameter values, the data of every given
field by content, user functions by code, every numerical choice including time_parameter,
bc_time and f_backend), exact start state (and multistep history), number of steps, the
series functions and series_every (a hit returns the source run’s series), and executed
code (tree and compiled core hashes, Python/NumPy/SciPy versions, architecture). For a decorated
function, identical also covers what it reads besides its arguments: its closure values and the
module globals its code names (by content; library modules, functions and classes by name and
version; the attributes it names of a user module or a user class, such as cfg.A or Cfg.A, by
content), followed into the user functions it calls, so two closures make(1) and make(2) of
one factory, or one function under @scale(2) and @scale(3), are two computations (your own
decorators under @track are part of what is called and of the key). The cache is off by default; it needs record=; it is refused
with a callback (a hit would never call it); retracted runs are never reused; an argument, or a
value read by the function or by a user pointwise function, without a content hash makes the
call uncachable (said with a CacheReuseWarning naming it, then computed). A hit returns values of
the recorded type (a NumPy scalar keeps its dtype, the string "nan" stays a string), and its
series and fields are read from the source run through the new run (run.series(...)).
Studio and export¶
The The Studio lists, filters and compares registry runs (read-only), and a live run links to its registry record when it was recorded with
cn.runs.track(..., telemetry=tel).cn.runs.export(dir)writes runs as small JSON files (filters as infind);cn.runs.export_mlflow(runs)copies runs to MLflow if you have it (never installed by Conatus).
Reference¶
- conatus.runs.track(name=None, *, tags=(), params=None, obj=None, parent=None, relation=None, status='provisional', root=None, telemetry=None, capacity=256, notes=None, cache=False, compression=None)[source]¶
Record a block (
with track("rb", tags=[""]) as run: ...) or every call of a function (@track("eig", cache=True)).compressionapplies to arrays returned by a decorated function (None, ‘gzip’, ‘lzf’; all lossless).- Parameters:
name (str | None)
tags (Iterable[str])
params (Mapping[str, Any] | None)
obj (Any)
parent (Any)
relation (str | None)
status (str)
root (Any)
telemetry (Any)
capacity (int)
notes (str | None)
cache (bool)
compression (str | None)
- Return type:
Tracker
- conatus.runs.record(obj=None, *, name=None, params=None, results=None, series=None, fields=None, files=(), compression=REQUIRED, tags=(), status='provisional', parent=None, relation=None, root=None, telemetry=None, notes=None)[source]¶
Record one finished computation in one call and return the closed run.
obj: a Problem, Plan or solver (numerics, problem hash,explain()), anEVPResult(eigenvalues and residuals stored as fields,n_modes,growth_max,residual_maxas results), or any object withexplain().series: {name: 1-D sequence}, all the same length.fields: {name: array}, which requirescompression(None, ‘gzip’, ‘lzf’).- Parameters:
obj (Any)
name (str | None)
params (Mapping[str, Any] | None)
results (Mapping[str, Any] | None)
series (Mapping[str, Any] | None)
fields (Mapping[str, Any] | None)
files (Iterable[Any])
compression (Any)
tags (Iterable[str])
status (str)
parent (Any)
relation (str | None)
root (Any)
telemetry (Any)
notes (str | None)
- Return type:
- conatus.runs.find(problem=None, *, name=None, status=None, state=None, tags=None, parent=None, since=None, until=None, limit=None, order='-created', root=None, **filters)[source]¶
Runs matching every criterion, newest first.
problem: a Problem / Plan / solver (its structural hash), a hash prefix (>= 8 hex digits) or a problem name.filters:key=valueorkey__op=valuewith op in eq, ne, gt, ge, lt, le, in, contains. A key is an index column (host, gpu, git_head, slurm_job, …), else a parameter, else a result; a key that is both a parameter and a result is refused (writeparam_<key>orresult_<key>), and an unknown key is an error (typos never return an empty list silently), except in an empty registry.- Parameters:
problem (Any)
name (str | None)
status (str | Iterable[str] | None)
state (str | Iterable[str] | None)
tags (str | Iterable[str] | None)
parent (Any)
since (Any)
until (Any)
limit (int | None)
order (str)
root (Any)
filters (Any)
- Return type:
- conatus.runs.get(run_id, *, root=None)[source]¶
One run by id or unique id prefix.
- Parameters:
run_id (Any)
root (Any)
- Return type:
- conatus.runs.compare(a, b, *, root=None)[source]¶
Differences between two runs (ids or Run objects): parameters, numerics, results, code.
- Parameters:
a (Any)
b (Any)
root (Any)
- Return type:
Comparison
- conatus.runs.lineage(run, *, root=None)[source]¶
Parent chain and descendants (fork, restart, reproduce, cache reuse) of a run.
- Parameters:
run (Any)
root (Any)
- Return type:
Lineage
- conatus.runs.reproduce(run, solver=None, *, fn=None, record=True, require_same_code=False)[source]¶
Run the recorded computation again and compare with the recorded output.
IVP runs: pass a
solverbuilt for the same problem and numerics (checked: problem hash, parameters, bases, scheme, dt… ; any difference is refused, never adapted); the recorded start state is loaded, the same number of steps is taken, the end state is compared bit for bit. Decorated calls: the function is re-imported (orfn=) and called with the recorded arguments. If the executed code differs from the recorded one, aCodeChangedWarningsays how (or an error withrequire_same_code=True).- Parameters:
run (Run)
solver (Any)
fn (Callable | None)
record (bool)
require_same_code (bool)
- Return type:
ReproduceReport
- conatus.runs.export_mlflow(runs, *, experiment=None, tracking_uri=None)[source]¶
Copy runs to MLflow (optional;
mlflowimported here only, never installed by Conatus): parameters, numeric results as metrics, series as metric histories, tags, status, and the full record as an artifact. Returns {conatus id: mlflow run id}.- Parameters:
runs (Iterable[Run])
experiment (str | None)
tracking_uri (str | None)
- Return type:
dict[str, str]
- class conatus.runs.Run(registry, name, *, tags=(), params=None, obj=None, parent=None, relation=None, status='provisional', telemetry=None, capacity=256, notes=None, key_parts=None, _record=None)[source]¶
Bases:
objectA registry run. Created live by
conatus.runs.track()/conatus.runs.record()/solver.run(record=...), or read back byconatus.runs.get()/find.Live methods:
log()(series),result()(scalar results),save_field(),save_file(),attach()(a Problem / Plan / solver: numerics andexplain()),certify()/retract(),close(). Read methods:params,results,series(),load_field(),file_path(),explain(),reproduce(),lineage().- Parameters:
registry (Registry)
name (str)
tags (Iterable[str])
params (Mapping[str, Any] | None)
obj (Any)
parent (Any)
relation (str | None)
status (str)
telemetry (Any)
capacity (int)
notes (str | None)
key_parts (dict | None)
_record (dict | None)
- property name: str¶
- property status: str¶
- property state: str¶
- property params: dict¶
- property results: dict¶
- property tags: list[str]¶
- property parent: str | None¶
- property path: Path¶
- property cache_report: dict | None¶
- attach(obj)[source]¶
Record the problem hash, the numerical configuration (
conatus.provenance.numerics) and theexplain()text of a Problem, Plan or solver. The problem’s parameter values (IRParametervalues, overridden bydiscretize(params=...)) join the run parameters; a parameter declared twice with different values is refused.- Parameters:
obj (Any)
- Return type:
- log(step=None, **scalars)[source]¶
Append one row of series values (
run.log(t=t, energy=E));stepis optional (the row number is always kept, so series logged together can be plotted against each other). In-memory only; written todata.h5at close.- Parameters:
step (int | None)
scalars (Any)
- Return type:
None
- result(**values)[source]¶
Scalar results (numbers, strings, booleans, complex): stored in
run.jsonand indexed, sofind(Nu__gt=...)works.- Parameters:
values (Any)
- Return type:
None
- save_field(name, array, *, compression, level=None, t=None, step=None)[source]¶
Queue a copy of
arrayfordata.h5(fields/<name>/<k>).compressionhas no default (None, ‘gzip’ or ‘lzf’, all lossless;level0–9 for gzip) and is written in the dataset’s attributes. Returns False if the queue was full (dropped, counted).- Parameters:
name (str)
array (Any)
compression (str | None)
level (int | None)
t (float | None)
step (int | None)
- Return type:
bool
- save_file(path, name=None)[source]¶
Queue a copy of a file into
files/(sha256 recorded). The copy is taken by the writer thread: a file changed right after this call may be copied changed.- Parameters:
path (str | PathLike)
name (str | None)
- Return type:
bool
- set_status(status, reason, evidence=None)[source]¶
Change the status (history kept, never rewritten).
reasonis required.- Parameters:
status (str)
reason (str)
evidence (Any)
- Return type:
- certify(reason, evidence=None)[source]¶
Mark the run
certified(reason and optional evidence, e.g. residuals, recorded).- Parameters:
reason (str)
evidence (Any)
- Return type:
- retract(reason)[source]¶
Mark the run
retracted(kept, never deleted; never reused by the cache).- Parameters:
reason (str)
- Return type:
- close(state='completed', error=None, timeout=600.0)[source]¶
Finish the run: series to
data.h5, finalrun.json, index. Waits for the writer at mosttimeouts. Idempotent. Warns (RegistryWriteWarning) if anything was lost.- Parameters:
state (str)
error (str | None)
timeout (float)
- Return type:
- series(name, x=None)[source]¶
{“row”, “step”, “value”} arrays of one series; with
x(another series logged in the same rows, e.g. “t”), also “x” aligned on the rows both have (no interpolation).- Parameters:
name (str)
x (str | None)
- Return type:
dict
- load_field(name, index=-1)[source]¶
One saved snapshot of a field (default: the last one).
- Parameters:
name (str)
index (int)
- Return type:
ndarray
- file_path(name)[source]¶
The attached file
nameof this run (a plain file name: no directory part, never “.” or “..”; security name=”../../x” read outside the run).- Parameters:
name (str)
- Return type:
Path
- reproduce(solver=None, **kw)[source]¶
Run this computation again and compare (see
conatus.runs.reproduce()).- Parameters:
solver (Any)
kw (Any)