Knowing what a run will cost#
explain resolves the whole sweep and stops. It reads the space, applies the
policy, opens the store if there is one, and returns a Plan without calling
the function once.
plan = layer.explain(space, policy=SweepPolicy(store="runs/layer.zarr"))
print(plan)
╭─ layer ──────────────────────────────────────────────────────────────╮
│ loop(tau, ssa) -> reflectance(), transmittance() │
╰──────────────────────────────────────────────────────────────── v0 ──╯
SPACE
tau axis 12
ssa axis 5
points 60
dedup disabled
CALLS
to compute 30
cached 30
skipped 0
total 60
ARGUMENTS
name kind dtype shape
───────────────────────────
tau loop float64
ssa loop float64
RESULT
name dims dtype
────────────────────────────────────────
reflectance tau: 12, ssa: 5 float64
transmittance tau: 12, ssa: 5 float64
STORE
path runs/layer.zarr
chunk tau 12
chunk ssa 5
regions 60
EXECUTOR serial
Estimating the total time#
The plan gives the count; you supply the cost of one call.
start = time.perf_counter()
engine(one_representative_point)
per_call = time.perf_counter() - start
plan = layer.explain(space, policy=policy)
print(
f"{plan.n_to_compute} calls x {per_call:.1f} s = {plan.n_to_compute * per_call / 3600:.1f} h"
)
n_to_compute is the honest number: it excludes what the store already has
and what skip_where removes. n_points is the size of the grid,
n_calls what a full run would take from scratch.
Two caveats worth saying out loud. Per-call cost is rarely uniform across a
parameter space, so time a point near the expensive end rather than the
cheap corner. And with executor="process" the wall clock divides by
something smaller than the worker count, never by the worker count itself.
What else the plan is for#
Catching an accidental product. A point count with five too many digits is the symptom; see semantics.
Seeing what one call will receive. The arguments block gives the kind, dtype and shape of every argument, including the batch length of a
vecvariable. If that shape is not what the callee expects, you know now.Checking the store lines up. The store block shows the chunk grid and region count a run will write into.
Auditing a finished run. Against a complete store,
n_to_computeis zero and every point is cached.
Execution consumes the same plan object that explain returns, which is what
guarantees the report and the run cannot drift apart.
Skipping points#
skip_where takes a predicate on a point’s loop values and excludes it
without ever calling the function:
SweepPolicy(skip_where=lambda point: point["tau"] > 1.5)
Skipped points get a skipped status rather than a value, and a later run
without the predicate will compute them.
Worked in Start here and the store.