analysis_io
data.analysis_io
Run a study’s declarative analyses: and persist each as its own container.
A study reports quantities that no simulation produced — a basis decomposition of empirical maps, a spectrum, a permutation test — and reductions that span several experiments. SimulationStudy.analyses declares each as the same FunctionCall a Parameter.producer uses, and this module is what executes one: resolve its arguments (a literal value, or a used: DataRef reading an experiment, another analysis, or a dataset), call it, and write the result to <results_root>/ana-<name>_result.h5 beside its one YAML provenance sidecar.
That path is the container convention every consumer already resolves, so a figure layer binds an analysis exactly as it binds a run. Arrays are written through xarray untouched: an invocation that returns labelled DataArrays keeps its dims and coordinates, which is what lets a grammar panel name them in an encoding.
Which backend renders an analysis is declared, not assumed — Analysis.execution carries the same choice a SimulationExperiment makes. :data:RENDERERS maps a backend name to the renderer that runs it; a backend with no renderer raises rather than being quietly run by another.
The used: edges also order the work. :func:schedule splits the analyses into those that run before the experiments (an experiment may source one through a parameter) and those that run after (they read an experiment’s result), each topologically ordered.
Attributes
| Name | Description |
|---|---|
| RENDERERS | Backend name -> renderer fn(analysis, kwargs) -> produced. |
Functions
| Name | Description |
|---|---|
| analysis_closure | The named analyses plus the upstream ones that have no container yet. |
| analysis_name | The analysis’s declared name — its container key and its identity in a used:. |
| container_path | Path an analysis named name writes, whether or not it exists yet. |
| dependencies | What this analysis consumes, read off its arguments’ used: edges. |
| dependents_of | Every analysis downstream of the given experiments or analyses, in declaration order. |
| render_inprocess | Renderer that performs the declared invocation in this interpreter. |
| render_tvboptim | Renderer that lowers a declarative equation: analysis to JAX and vmaps it. |
| run_analyses | Execute analyses in the given order, returning the containers written. |
| run_analysis | Execute one declared analysis and persist its result. Returns the container path. |
| schedule | Split the analyses into (before_experiments, after_experiments). |
| study_analyses | A study’s declared analyses as a list, whatever spelling the loader produced. |
analysis_closure
data.analysis_io.analysis_closure(analyses, names, exists)The named analyses plus the upstream ones that have no container yet.
The upstream counterpart of :func:dependents_of, and it belongs beside it: both walk the study’s used: edges, and a caller that needs one usually needs the other.
Asking for an analysis by name means asking for it to be RE-derived, so its own inputs are left alone wherever they already exist — re-running them would be the whole study, which is what asking by name is an alternative to. An input that has never been produced is different: it cannot be read, so it is pulled in, and transitively, because the first missing container’s own inputs may be missing too.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| analyses | The study’s declared analyses. | required | |
| names | The names asked for. | required | |
| exists | Predicate saying whether an analysis’s container is already on disk. | required |
analysis_name
data.analysis_io.analysis_name(analysis)The analysis’s declared name — its container key and its identity in a used:.
container_path
data.analysis_io.container_path(name, results_root=None)Path an analysis named name writes, whether or not it exists yet.
The layout itself is defined once, by :func:tvbo.data.dataref.locate_analysis_container; this is its write-side counterpart, which must not raise on a container that has not been produced.
dependencies
data.analysis_io.dependencies(analysis)What this analysis consumes, read off its arguments’ used: edges.
Returns {"experiments": {...}, "analyses": {...}} — the ids/names that must have run before it. A literal argument contributes nothing.
An experiment: edge contributes BOTH the spelling the recipe used and its bare numeric id, because a recipe may write exp-3, exp3 or 3 for the same experiment while the runtime identifies it by {key, name, label, id}. Keeping only the literal string makes every dotted spelling silently match nothing — an empty stale set reads exactly like a clean one.
dependents_of
data.analysis_io.dependents_of(analyses, *, experiments=(), changed_analyses=())Every analysis downstream of the given experiments or analyses, in declaration order.
An analysis container records no link back to the run it was derived from, so re-running part of a study leaves each downstream container holding the PREVIOUS run’s numbers with nothing to raise. This names the set to invalidate, and it must be transitive: the second-order analyses (a landscape built from a per-cell reduction, a correlation built from that) are exactly the ones a hand-written list forgets.
The seed is either kind of node, because both partial-run modes exist: --experiment re-runs a simulation, --analysis re-runs a derivation, and what goes stale downstream is found by the same walk over the study’s own used: edges. Analyses named in changed_analyses come back in the result — a caller that just ran them drops them.
render_inprocess
data.analysis_io.render_inprocess(analysis, kwargs)Renderer that performs the declared invocation in this interpreter.
callable: calls the function with the resolved arguments. class_call: instantiates the class with its constructor_args and calls the instance with the resolved arguments, so an analysis carried by a class (a fitted estimator, a stateful reduction) declares the same way a monitor does. The function: and equation: forms are refused rather than half-executed — they describe a step inside an observation pipeline, whose inputs come from a running solve, not a container.
It imposes no array library: whatever the declared code uses is what runs. The written container is xarray either way.
render_tvboptim
data.analysis_io.render_tvboptim(analysis, kwargs)Renderer that lowers a declarative equation: analysis to JAX and vmaps it.
This is the metadata-native form: instead of pointing at arbitrary code/ Python, the analysis states an expression over its named arguments and the axis to map it over, and tvbo emits the realization — so the framework, not the study, owns the parallelism. apply_on_dimension becomes a jax.vmap over that axis of every argument carrying it (arguments without it are broadcast, not copied per element), and aggregate reduces a named axis of the result.
The expression is ELEMENTWISE over the inputs’ axes; aggregate is what reduces. Output axes are derived from that contract — apply_on_dimension first, then the inputs’ own axes minus aggregate.over — and cross-checked against the result’s rank, never read off its shape. An expression that reshapes or reduces on its own (an outer product, a correlation) fails that check and must declare dims:, which is honoured as written.
How that map is spread is declared too. By default it is one jit-ed vmap on one device; execution.n_workers shards the mapped axis across devices and execution.batch_size bounds how many lanes are live per device, both through the same tvboptim machinery an experiment’s grid uses (see :func:_map_over). execution.accelerator pins the platform when JAX has not yet initialised.
Host-only work stays host-only: a sparse eigensolve, a CIFTI read or a spin-permutation generator is not JAX-expressible and belongs on the inprocess renderer. This renderer refuses a callable:/class_call: analysis rather than silently jitting code that was never written to be traced.
run_analyses
data.analysis_io.run_analyses(
analyses,
results_root=None,
*,
compress=True,
on_start=None,
on_done=None,
)Execute analyses in the given order, returning the containers written.
on_start(name) / on_done(name, path) report progress to a caller’s logger without this module choosing an output style.
Names are checked against each other first: a BIDS entity value keeps only the alphanumeric characters, so calcium_c10 and calciumC10 would write the same container and the second would silently replace the first.
run_analysis
data.analysis_io.run_analysis(analysis, results_root=None, *, compress=True)Execute one declared analysis and persist its result. Returns the container path.
schedule
data.analysis_io.schedule(analyses)Split the analyses into (before_experiments, after_experiments).
An analysis that reads an experiment result runs after the experiments, and so does everything downstream of it. The rest run first, so an experiment’s own parameter can source one through a used:. Each list is topologically ordered.
study_analyses
data.analysis_io.study_analyses(study)A study’s declared analyses as a list, whatever spelling the loader produced.