Core concepts
Five ideas. Read this once and the rest of the documentation is obvious.
Project
A directory containing a .sillon/ folder. That is the whole definition — there
is no registration step, no config to write. The first script that logs
something creates it. (sillon init exists for when you want to name a project
before it has runs, or put its heavy data on another disk, but you never have
to call it.)
Everything inside that directory belongs to the project: sillon searches
upward for the .sillon/, like git. So a script in scripts/sweeps/ and a
notebook in analysis/ are both working on the project at the root, and
projects never nest.
Projects are independent: one database, one set of runs, one place to back up.
sillon projects lists every project on your machine and where it is, and
sillon export / sillon import move runs between them.
Run
One execution of your script. A run holds:
| parameters | the inputs you chose — log_param |
| results | the outputs you got — log_result |
| artifacts | files you produced — log_result(path=...) |
| figures | plots, with a record of what drew them — log_figure |
| metadata | context: host, author, source code, timing — mostly automatic |
| tags / notes | your own labels — add_tag, add_note |
Runs are never overwritten. Log my_fit twice and you get my_fit and
my_fit_2.
Parameters vs results
The distinction is intent, not type: a parameter is something you decided, a
result is something the run produced. Keeping them apart is what makes
query(parameters={...}) answer "which settings did I try?" and
sort_by("rmse") answer "which worked best?".
Code version
Every run records a hash of the code that produced it — the entry script and your own imported modules. It is computed from the parsed structure of the code with literal values normalised away, which means:
| Change | New version? |
|---|---|
N = 5 → N = 4 |
no — a tuned constant is not a code change |
| adding a comment, reformatting | no |
kinetic - potential → kinetic + potential |
yes |
| editing a function in your own library | yes |
That distinction is the whole point: it lets you ask "which of my runs used the code from before I fixed that bug?" and get a trustworthy answer.
sillon versions
project.query(fields={"logic_version": "c8d012…"})
Run status
Every run records how it ended, and this is meant to be trusted:
| Status | Meaning |
|---|---|
SUCCESS |
the script finished normally |
CRASHED |
it raised an uncaught exception, or was killed |
RUNNING |
still in flight |
A run that died is never recorded as a success. When it crashed with an
exception, the type and message are stored too (sillon.error.type,
sillon.error.message).
Light values and heavy values
You log both the same way; sillon decides where they go.
- Small, JSON-friendly values (numbers, strings, short lists) are stored inline in SQLite, so filtering on them is fast.
- Large arrays are written to the run's HDF5 store and the database keeps a pointer, its shape and its dtype.
This is why log_result("field", a_huge_array) needs no special handling, and
why filtering on a parameter stays fast even when the results are gigabytes.
Two-phase queries
project.query(...) is deliberately split:
- Cheap phase — filters that only need the database (parameters, tags, status, dates) run first, in memory, over one bulk fetch.
- Heavy phase — filters that need the array store run only on the runs that survived phase one.
So this is fast even in a large project, because the results filter only ever
touches a handful of files:
project.query(
parameters={"degree": lambda d: d == 3}, # cheap, narrows to a few runs
results={"rmse": lambda v: v < 0.1}, # heavy, runs only on those
)
Order your filters cheapest-first and you get this for free.
The daemon
Your script does not write to the database. It sends what you log to a small background process — one per project — which owns the writes.
You never start or stop it. It appears on the first log call and exits on its
own after five minutes idle (SILLON_IDLE_TIMEOUT to change that). Several
scripts can log to the same project at once; the daemon serialises them.
Two consequences worth knowing:
- If your script is killed outright, the daemon notices the dropped connection
and marks that run
CRASHEDrather than losing it. - Its log is at
.sillon/daemon.log. That is the first place to look when something is wrong.
Content hashing
Every logged value is hashed. Two runs that produced identical data have the
same hash, which is what makes sillon trace <file> able to tell you which run
a file on disk came from.
Lineage
A run can derive from another, and usually you do not have to say so: if your script loads an earlier run's data through sillonlab before logging a new one, sillon records the link itself, naming the items that passed between them.
state = sl.load_project().get("equilibrated").load_result("final_state")
with sp.track_run(): # parent edge recorded automatically
...
Declare it with inherit= when the two halves happen in different processes.
Either way nothing is copied — it is an edge you can query later with
run.parents(), run.children(), or sillon lineage <run>.