SPT simulations and convergence¶
The repository-only SPT workflow catalogues supplied XDMF/HDF results, reconstructs lobules, serializes quantitative analysis, and generates numerical and geometry figures. It requires access to study data, but does not need FEBio, VTK rendering, or a graphics session. The reusable library remains independent of these study workflows. This analyzes existing solver results; it does not run new simulations.
Input and output directories¶
Keep the network dataset separate from local outputs. The current dataset is
available at /data/koeniglab/spt, with this structure:
spt/
├── simulations_sixth/
│ ├── material/
│ │ ├── feb_manipulation_v3.py
│ │ └── lobule_zonation_pattern_bc.feb.template
│ └── xdmf/
│ ├── sim001.xdmf
│ ├── sim001.h5
│ ├── sim001_cells.nc
│ ├── sim001_points.nc
│ └── ...
└── convergence_sixth/
├── material/
│ └── *convergence_*.feb
└── xdmf/
├── cbc_convergence_06250.xdmf
├── cbc_convergence_06250.h5
└── ...
Both names carry the study design. The generation script is versioned, the
simulation template names the boundary conditions it was built for, and a
convergence run is prefixed with the variant it belongs to. The workflow
discovers the script and the template rather than expecting fixed names, and
identifies a convergence mesh by the resolution after its last convergence_
marker, so a renamed study is read without changing code.
Use the development environment, or sync just
research dependencies with uv sync --locked --group research --no-dev.
Commands run from the checkout root:
To run the complete project from catalogue through analysis, figures and manuscript figures, use:
export POROUS_MEDIA_SPT_DATA=/data/koeniglab/spt
export MPLBACKEND=Agg PYVISTA_OFF_SCREEN=true
uv run --no-sync python -m research.spt.pipeline
The complete workflow below describes resuming, inspecting stages, and the results layout.
On PowerShell, set $env:POROUS_MEDIA_SPT_DATA and $env:MPLBACKEND instead.
Every command also accepts --input-root /data/koeniglab/spt explicitly. The input
default is POROUS_MEDIA_SPT_DATA, falling back to the checkout's data/spt.
The output default is POROUS_MEDIA_SPT_RESULTS, falling back to results/
inside the input root, for example /data/koeniglab/spt/results. Results never
live inside the repository checkout. Outputs inside a supplied study directory,
outputs containing the input root and symlinked output subdirectories are
rejected. No command changes the supplied study directories.
Results layout¶
The output root separates deliverables from intermediate data. Every report
refresh also writes this overview as README.md:
results/
├── index.html offline report; open this first
├── README.md folder guide
├── manuscript/
│ ├── exports/ stable figure-04.svg ... figure-s1-convergence.svg, tissue panels
│ └── <figure>/ versioned generation of each manuscript figure
├── analysis/<study>/ quantitative tables, NetCDF, figures and provenance
├── figures/
│ ├── runs/<study>/<run_id>/ geometry time course of one history
│ └── overviews/<study>/ final-state overview of a study
├── meshes/<study>/<run_id>/ reconstructed lobules and mixed-sector histories
├── catalog/ runs.tsv, optional runs.xlsx, condition figures
├── logs/ pipeline.json and per-stage reports and catalogues
└── cache/
├── cells/<study>/<run_id>/ numerical cell, point and summary caches
├── analysis/<study>/<run_id>/ per-run analysis results
├── plots/<study>/<run_id>/ sampled plot geometry and values
├── depth/<study>/<run_id>/ native cap heights for tilted views
└── previews/ report thumbnails
Each artifact directory holds a manifest.json naming its current
generation-<id>/ folder; previous generations are removed after a successful
update. Unused report previews are pruned whenever the report is rebuilt.
Storage profile¶
A complete results tree of 48 simulations and 7 convergence meshes is about 7 GB. The equivalent tree written before these settings was 97 GB. Each setting and its measured effect:
| Artifact | Setting | Measured effect |
|---|---|---|
meshes/ |
only the fields the pipeline reads back | one reconstructed lobule fell from 863 MB to 122 MB |
meshes/ |
each attribute stacked into one dataset, referenced per step by an XDMF HyperSlab | that same lobule fell further to 26 MB, 34x in total; the tree falls from 71 GB to about 2 GB |
cache/cells/, cache/analysis/, analysis/*.nc |
NetCDF4 with zlib level 4 and the shuffle filter, instead of uncompressed NetCDF3 | one sixth cache fell from 70 MB to 13 MB and one lobule cache from 419 MB to 36 MB; the tree falls from 21 GB to about 4 GB |
figures/, analysis/ |
PNG previews by default | per-run SVG averaged 32 MB against 1.0 MB for the same figure as PNG; SVG was 88% of all figure storage |
meshes/ HDF arrays |
gzip level 4 with shuffle | level 4 gains 3% over level 1 and levels above 6 under 0.3% |
cache/plots/ |
deflated .npz sample archives |
repetitive face, sector and cell indices compress well |
Stacking is what makes the compression effective rather than nominal. Written one
step at a time, each array is a chunk of its own and the compressor sees a single
frame; a chunk spanning consecutive steps exposes how little changes between
them. meshio cannot read HyperSlab references, so these sidecars are read with
porous_media.data.xdmf_tools.XDMFTimeSeriesReader, which resolves them and
otherwise behaves exactly like meshio's reader. ParaView reads them directly.
The cell histories are stored as float32 by the source solver, so nothing here
reduces numerical precision. Everything under cache/ is rebuilt by rerunning
the pipeline; meshes/, analysis/, figures/ and manuscript/ are the
deliverables. Reconstructing with --all-fields, or requesting SVG or PDF
figures, raises these numbers; --all-fields multiplies mesh storage by about
five.
Compression and the mesh field selection are recorded in each artifact's
manifest.json, and both participate in the stale check: changing either
rebuilds the artifacts that depend on it rather than mixing layouts within one
tree.
Inspect conditions and available runs¶
information.py reads descriptor and model metadata without loading HDF arrays.
After processing, it reuses catalogue metadata when source fingerprints match.
Use --refresh to reread descriptors explicitly.
It discovers actual convergence IDs and records cell/point counts, available
scalar fields, time ranges in seconds, and completion relative to the model's
explicit control duration. Models with ambiguous or absent duration metadata
have an unknown completion status.
Simulation IDs follow the pattern/flow ordering in the supplied generator. The command parses literal assignments without executing the archived Python script. If no generator is supplied, it labels the repository defaults as unverified. Flow units remain unspecified when the source does not declare them. Convergence filename tokens remain identifiers; no mesh-length units are inferred from them.
uv run --no-sync python -m research.spt.information --dry-run
uv run --no-sync python -m research.spt.information --plots --excel
The first command writes nothing. The second writes catalog/runs.tsv, optional
catalog/runs.xlsx, and the condition figures catalog/boundary-flows.png and
catalog/condition-colors.png. Figures are saved
without opening windows and are closed after use. Metadata failures and missing
expected simulations are reported and produce a nonzero exit status.
The dataset inspected on 2026-11-12 contains 56 simulation runs, seven zonation
patterns by eight boundary flows, and six convergence runs.
cbc_convergence_06250 ends at 53,027.1 s, earlier than the model duration of
86,400 s, and its own model declares no duration, so it is catalogued with an
unknown complete. Comparisons must choose a shared time window or explicitly
handle different end times.
Pattern 6 is the CYP2E1 zonation pattern. It rises from periportal to
perivenous like the linear pattern, but smoothly: measured on sim049 at
t = 0, relative protein runs from 0.013 to 0.996 of its range, is flat
periportally, steepest at about 0.6 of the position and saturated perivenously.
It carries the YlOrBr palette, the row label G CYP2E1 and a sigmoidal
profile icon. A pattern index this repository does not name at all is still
catalogued and plotted, as pattern <index>, rather than dropped or given an
invented label.
A lobule is six rotations of one source sector, so the mixed-pattern comparison
view holds six sources and no more. It compares every pattern beside CYP2E1,
which is the measured profile rather than one of the synthetic ones. Every
pattern still has its own lobule and its own row in the study and manuscript
figures. A study that leaves more than six patterns after that exclusion fails
rather than silently dropping one, because the sector count fixes how many fit.
--no-combinations skips the mixed views entirely.
Build or resume local caches¶
data_processing.py uses existing XDMF/HDF results directly. Its default cache
step reads the source sixths. Explicit reconstruction steps prepare full lobules
for subsequent analysis, as described below. VTK conversion remains separate.
uv run --no-sync python -m research.spt.data_processing --status
uv run --no-sync python -m research.spt.data_processing --workers 2
uv run --no-sync python -m research.spt.data_processing \
--study convergence --runs cbc_convergence_06250 --workers 1
--status and --dry-run inspect metadata and cache state without writing.
--study accepts both, simulations, or convergence. --runs takes explicit
run IDs; unknown selections and missing/empty study directories fail clearly.
A complete simulation selection requires every expected run. Use --runs for
an intentionally partial study.
The default analysis profile retains the five analysis scalars, cell volumes,
position, fluid/solid volumes, pressure, zonation pattern, and cell type. It omits
unused point arrays. --profile full retains every scalar cell and point field.
Stored field precision is preserved, with type promotion where needed.
First-time adoption of the supplied NetCDF caches validates time coordinates, field dimensions, and first/middle/last HDF frames for every selected field. The manifest labels this as sample validation, not a complete comparison of all values. Choose a stronger policy when needed:
# Compare every selected cached value against HDF before adoption.
uv run --no-sync python -m research.spt.data_processing --legacy-cache full
# Read selected fields directly from HDF, bypassing supplied NetCDF caches.
uv run --no-sync python -m research.spt.data_processing --legacy-cache ignore
# Rebuild a selected local cache from HDF even if its fingerprints match.
uv run --no-sync python -m research.spt.data_processing --runs sim001 --force
Rejected legacy caches fall back to HDF and the report records the reason.
Subsequent runs check input sizes and nanosecond modification times, processing
options, conditions, and output fingerprints. Unchanged runs do not reopen XDMF
or numerical arrays. Changed sources rebuild from HDF, avoiding reuse of stale
legacy caches. Changes that deliberately preserve both file size and modification
time require --force; metadata fingerprints are not content hashes.
Two spawned workers are the default. --workers 1 runs serially. Each worker
owns one run's files and memory. Tune the worker count for the network share
rather than the machine's CPU count. An unchanged batch avoids worker startup.
Outputs and failure recovery¶
Each run publishes a complete generation under:
results/cache/cells/<study>/<run_id>/
├── manifest.json
└── generation-<identifier>/
├── cells.nc
├── points.nc
├── summary.nc
└── limits.json
The manifest identifies the active generation and records provenance and the legacy validation method. Scalar limits are calculated from the selected data without rereading the source. An OS lock prevents concurrent readers/writers from replacing a run during a load. Partial writes never replace the active manifest. A failed run leaves previously completed outputs intact, and the next invocation resumes other completed runs. Source changes during processing cause failure and require a retry after the upload completes.
logs/processing.json records all selected successes and failures, while
logs/processing-runs.tsv records the selected successful metadata. These describe
the latest invocation and do not overwrite the separate information catalogue.
Any failed run gives the processing command a nonzero exit status. Short but
readable histories are reported separately from processing failures.
Cache APIs load data into memory and close file handles:
from pathlib import Path
from research.spt.cache import load_processed_run, load_summary
from research.spt.catalog import discover_runs
source = discover_runs(Path("/data/koeniglab/spt"), run_ids=("sim001",))[0]
output = Path("/data/koeniglab/spt/results")
summary = load_summary(source, output) # Reads only the compact summary.
cells, points, summary = load_processed_run(source, output)
Summaries contain native-time volume-weighted population means and standard deviations, accumulated in float64. Invalid volumes and nonfinite analysis values fail explicitly. Per-cell arrays retain the original time axis for spatial analysis and interpolation. Interpolating a summary is not generally equivalent to interpolating cell values and changing volumes before reduction.
Further analysis and visualization¶
plots_simulations.py and plots_convergence.py are entry points to the shared
quantitative analysis below.
They use actual run IDs and local homogeneous-lobule caches. Geometry plotting
uses its own shared renderer. Numerical cache generation does not invoke either
plotting workflow or import PyVista.
Lobule reconstruction is an explicit processing step described below. VTK conversion remains a separate helper for studies without XDMF results. PyVista snapshots and video export require a working rendering setup; the sector comparison figures generated below use Matplotlib and run headlessly. For a portable numerical example, see the library quickstart.
Reconstruct lobules and sector comparisons¶
Create the modified geometries after inspecting the supplied sixths. This step reconstructs existing simulation results by symmetry; it does not run FEBio. The complete workflow then builds numerical caches from the homogeneous lobules:
Individual steps can be run or resumed independently:
# Reconstruct every simulation and convergence mesh, preserving native times.
uv run --no-sync python -m research.spt.data_processing --step lobulus
# Compose the mixed convergence view and eight comparisons of zonation patterns.
# This also saves a PNG comparison figure alongside the time series.
uv run --no-sync python -m research.spt.data_processing --step combinations
# Build both sets of geometries and figures, without numerical cache creation.
uv run --no-sync python -m research.spt.data_processing --step geometry
# Analyze reconstructed lobules using the same scalar profiles and cache policy.
uv run --no-sync python -m research.spt.data_processing \
--step cache --geometry lobulus
# Inspect reconstruction status without writing or starting workers.
uv run --no-sync python -m research.spt.data_processing --step geometry --status
The existing default, --step cache --geometry sixth, remains available for
inspecting source-sector results. Subsequent full-lobule analysis should use
--geometry lobulus or --step all. Mixed comparisons are excluded from this
quantitative analysis path. A selected reconstruction can use --step lobulus
--runs sim001; combinations require complete studies and reject --runs.
--study simulations and --study convergence work for every step.
The reconstruction writes:
| Study | Contents | Purpose |
|---|---|---|
simulations_lobulus |
All 48 simulation lobules | Quantitative analysis and visualization |
convergence_lobulus |
All seven convergence lobules | Quantitative analysis and visualization |
simulations_combinations |
dose_01 to dose_08, one sector per pattern |
Visualization |
convergence_combinations |
convergence_mixed, one sector per resolution |
Visualization |
Each lobule places six copies of its source sector counterclockwise around the
origin, reflecting alternating sectors to pair matching radial boundaries.
Connectivity permutations preserve each source cell's orientation. Scalar fields
are copied, Cartesian vectors are transformed with Q, and full stress/strain
tensors with Q T Q.T. The Cartesian scalar fields position_x, position_y, and
position_z transform together. Unsupported component layouts fail explicitly.
Cell volume is preserved per cell; a homogeneous lobule has six times its
source's volume and the same volume-weighted means and standard deviations.
Seam points remain separate. This preserves different values on adjoining
patterns and permits different convergence resolutions without inventing node
correspondence or averaging discontinuities. sector_id identifies sectors
0 through 5, and source_cell_id preserves the original cell identity within
each source. These are reconstructed visualization/analysis geometries, not
conforming meshes intended as new solver inputs.
The mixed convergence view uses 06250, 09375, 12500, 18750, 25000, and
37250 in that order. 50000 still receives its own homogeneous lobule. The
finest input ends at 36,590 s, so the mixed history ends there. Native output times
are offset between resolutions. The mixed history uses the first sector's
native time grid inside the common interval, including the common endpoints.
Continuous scalar/vector/tensor fields interpolate linearly. Integer fields,
cell_type, rr_zonation_pattern, rr_necrosis, and gauss_points_TPM retain
their previous sampled value. No field is extrapolated. Homogeneous lobules
retain every original time sample and do not interpolate any field.
Each dose comparison combines patterns 0 through 5 from the same boundary-flow
condition. For example, dose 1 uses sim001, sim009, sim017, sim025,
sim033, and sim041. Dose numbers identify the generator's boundary-flow
ordering; flow units remain unspecified. Each comparison includes the complete
common history. Its figure shows initial protein zonation and final necrosis,
with numbered sectors and a pattern legend. The convergence figure shows the
final toxicant values of the six numbered resolution sectors. Both figures draw
sector outlines without internal mesh edges. Figures render headlessly with Matplotlib,
without requiring a VTK display backend.
Geometry generations are stored separately from numerical cache generations:
results/meshes/<study>/<run_id>/
├── manifest.json
└── generation-<identifier>/
├── <run_id>.xdmf
├── <run_id>.h5
├── <run_id>_limits.json
├── provenance.json
├── model.feb # Homogeneous lobules, when a source model exists.
└── <run_id>.png # Mixed comparisons only.
XDMF references its colocated HDF sidecar by relative filename. Copying the pair
is sufficient to open it elsewhere; it does not depend on network HDF files.
Each worker stages its input descriptors and HDF sidecars on local storage with
sequential copies, avoiding repeated small reads over the network filesystem.
Temporary copies are removed after readers close, including on failure. Allow
space for one source history per lobule worker or six per mixed-view worker,
in addition to generated outputs. Original inputs remain the provenance source
and are checked for changes before publication.
The HDF writer uses gzip level 4 with the byte-shuffle filter and stacks each
attribute into one dataset spanning the series, which is what collapses repeated
scalar and time-invariant storage. A sidecar keeps only the fields the pipeline reads back, listed as
GEOMETRY_CELL_FIELDS in research/spt/fields.py: the display layers, the
analysis fields and the categorical fields. A source run also carries mechanical
vectors and 3x3 tensors that nothing downstream reads; measured on one 1.76 GB
sidecar, stress, Lagrange_strain and solid_stress_TPM alone were 45% of its
storage and the vector fields another 16%. Pass --all-fields to reconstruct the
complete mechanical state instead, which multiplies mesh storage by about five. Source fingerprints,
conditions, field-transform policy, and temporal alignment are recorded in the
manifest and provenance file. Only complete generations become visible through
the manifest. Failed reconstruction or figure generation preserves the previous
generation, and unchanged runs skip array reads and worker startup.
logs/geometry.json describes the latest selected reconstruction invocation.
logs/geometry-runs.tsv lists successful outputs, including descriptor and figure
paths, so readers do not need to navigate generation directories manually.
Resolve published paths rather than globbing temporary or retired generations:
from pathlib import Path
from research.spt.cache import load_summary
from research.spt.geometry import geometry_path, lobulus_sources
raw = Path("/data/koeniglab/spt")
output = raw / "results"
source = lobulus_sources(raw, output, run_ids=("sim001",))[0]
summary = load_summary(source, output)
lobulus_xdmf = source.xdmf
comparison_xdmf = geometry_path(
output / "meshes" / "simulations_combinations" / "dose_01"
)
Numerical lobulus caches use the same cache/cells/<study>/<run_id>/ layout as
source-sector caches, under their respective *_lobulus study names.
Use published lobules for quantitative analysis and sector comparisons for
visualization. Mixed-sector histories never enter whole-lobule statistics.
Plot sixths, lobules and combined geometries¶
After reconstruction, create all geometry figures with:
The input, data and output roots default to the same locations used above.
--data-root selects the published reconstructed histories; --output-root
selects plot storage. Both default to the results directory. The raw share is read
only. Plotting does not change reconstructed histories or numerical caches.
These repository commands also work independently:
uv run --no-sync python -m research.spt.plots_simulations_geometry
uv run --no-sync python -m research.spt.plots_convergence_geometry
# Render a selected source sixth, its lobule and the same-dose comparison.
uv run --no-sync python -m research.spt.plot_geometry \
--study simulations --runs sim048 dose_08 --geometry all
# Inspect pending work without writing files or reading numerical arrays.
uv run --no-sync python -m research.spt.plot_geometry --status
--geometry accepts all, sixth, lobulus, or combined. The complete current
dataset produces 119 time-course figures: 55 source sixths, 55 homogeneous
lobules, eight dose comparisons and one mixed convergence history. Each of the
six study/geometry groups also gets a final-state overview. Simulation overviews
follow the generator's pattern and dose ordering. No figure shows the mesh
itself; only the triangles of manuscript Figure S1 A display cell edges.
Default figures preserve the manuscript's five snapshots: indices 1, 3, 5, 7
and 9 of ten equally spaced times in the common interval. Explicit --times
and other snapshot counts override this selection. The five columns retain
extracellular substrate and product, protein, toxic compound and necrosis.
The canonical layers in research/spt/fields.py preserve magma for
concentrations, hot for protein and binary for necrosis, with the original
labels and horizontal scalar bars in every panel. Scalar panels omit internal
mesh lines and show only the domain outline, including holes. Homogeneous
lobule outlines omit artificial sector seams; independent combined sectors
retain their own outlines. Layers can opt into transparent cell edges with
show_edges=True in research/spt/fields.py.
Boundary-only scalar surfaces use a thin stroke in each cell's own color to close
vector-export hairlines, without adding neutral mesh lines.
Each panel occupies 4 by 4 inches, matching the original 600 pixel panels at
150 DPI. Scalar bars retain five labels, bold text and one decimal place.
Final-state overviews use the same panel sizes and per-panel scalar bars.
Small ranges use scientific notation when fixed decimals would repeat labels,
so nonzero product concentrations do not appear as five identical zeros.
Times share a common interval within each selected study family. Simulation
plots therefore retain 24 h even when convergence ends at 36,590 s. Shared color
ranges use validated local numerical-cache limits, then full-history
_limits.json metadata beside source descriptors, without scanning network
arrays again. Missing or malformed legacy metadata falls back to sampled
ranges when no validated cache is available. Explicit layer limits take precedence. Combined
views identify each sector. These settings are shared with the historical
DataLayer adapter so the two workflows cannot silently choose different colors.
Continuous scalars are linearly interpolated between neighboring native frames. Necrosis, zonation, cell type and other discrete fields retain their previous native value. No extrapolation or averaging between cells is performed. Only requested scalar arrays and neighboring frames are read from HDF; point, vector, tensor and unused scalar arrays are omitted. A compact local sample cache is shared by time courses, overviews and optional videos. Plotting never creates interpolated XDMF/HDF histories.
Choose fields, times and outputs explicitly:
# Final-state figures for only necrosis and toxicant.
uv run --no-sync python -m research.spt.plot_geometry \
--fields rr_necrosis 'rr_(T)' --snapshots 1 --formats png svg
# Explicit times in seconds must lie within every selected history's interval.
uv run --no-sync python -m research.spt.plot_geometry \
--study simulations --times 0 21600 43200 86400
# Preserve outliers while showing smaller concentrations on a symlog color scale.
# Use a separate output root so the linear default figures remain unchanged.
uv run --no-sync python -m research.spt.plot_geometry \
--study convergence --scale symlog --output-root /tmp/spt-convergence-symlog
# Generate only the two study overviews per geometry.
uv run --no-sync python -m research.spt.plot_geometry --only-overviews
# Separate field/time panels are optional, as are videos and GIFs.
uv run --no-sync python -m research.spt.plot_geometry \
--study simulations --runs dose_08 --geometry combined \
--panels --video-frames 200 --fps 12 --gif
All geometry figures share an orthographic camera tilted 20 degrees from above, with an azimuth of -60 degrees. Native cap heights and exterior side faces show the mesh thickness without exaggeration. Caps and walls use their owning cell's unchanged scalar value and the canonical colormap; lighting does not alter the colors. The camera applies consistently to sixths, lobules, combined sectors, manuscript figures and optional videos. Quantitative plots and their statistics are unaffected. Colored convergence boundaries and sector numbers follow the same projection, and cropping includes the visible thickness.
SVG and PDF preserve vector cell geometry and text. PNG previews default to
150 DPI; --dpi also controls optional video resolution. FFmpeg is required only
when videos are requested. Video artists are reused and frames stream directly
to FFmpeg, without writing intermediate PNG panels. GIF conversion is opt-in.
--no-overviews omits study overviews. Bounded spawned workers own their files
and rendering state; --workers 1 runs serially.
Per-run and study figures default to PNG alone because a dense vector time
course dwarfs its raster preview: across one results tree the per-run SVG files
averaged 32 MB against 1.0 MB for the same figures as PNG, and SVG accounted for
88% of all figure storage. Request --formats png svg for the few figures that
need cell-level vector editing, and --formats png pdf when a vector file has to
stay small. Selecting fewer fields or snapshots also reduces rendering time and
storage without changing the underlying histories.
The shared Matplotlib renderer runs headlessly without importing PyVista or
needing a graphics session. It supports planar XY triangles/quads and vertical
wedge/hexahedron extrusions with horizontal caps. Planar elements tilt without
inventing thickness. Other geometries fail clearly instead of producing an
incorrect projection; use the library's 3D visualization helpers for those.
Historical visualize_spt_2d calls remain available, including generator inputs,
custom layer labels, unit conversions and fixed color limits. Separate panels
and videos now require explicit selection in that helper too.
Outputs and preview caches use the following layout:
results/
├── cache/plots/<study>/<run_id>/manifest.json
├── cache/depth/<study>/<run_id>/manifest.json
├── cache/previews/<fingerprint>.webp
├── figures/runs/<study>/<run_id>/manifest.json
├── figures/overviews/<study>/manifest.json
├── logs/plots-both.tsv
└── logs/plots-both.json
Each manifest points to a complete generation. A failed replacement preserves
the previous published figures. Source and output fingerprints enable unchanged
runs to skip XML parsing, numerical reads, rendering and worker startup. Changing
only a rendering option reuses the existing sampled arrays. New sample caches
retain native cap heights. Older planar caches are adopted without resampling
scalar histories: one geometry-only read stores the heights separately in
cache/depth/. Later rendering changes reuse this small local cache. Mesh-view
settings are included in figure provenance and invalidate geometry figure caches.
Missing artifacts
are regenerated; --force explicitly resamples and rerenders. Fingerprints use
size and modification time, so changes preserving both require --force.
The catalogue gives the current paths of figures and any requested videos; the
final HTML report links them. The plot report records errors, sampled color
ranges, elapsed time and the number of selected HDF field arrays read. Files
are named by the selected study (both, simulations, or convergence) and
describe the latest invocation, including an intentionally partial selection.
--scale symlog is an explicit alternative for concentration ranges containing
large outliers. It preserves every value, with a linear region within one
millionth of the maximum absolute display limit and logarithmic spacing outside
that region. Figures label the transformed scale; necrosis stays linear.
The current convergence_50000 has sampled substrate values above 2,000 mM,
whereas the six finer histories stay near 1 mM or below. Its product values
likewise stretch the shared linear range. Keep those source values visible;
use a separate symlog output root when inspecting finer spatial differences.
Time courses, spatial profiles and convergence¶
After numerical cache generation, run both study analyses or select one:
uv run --no-sync python -m research.spt.analysis
uv run --no-sync python -m research.spt.plots_simulations
uv run --no-sync python -m research.spt.plots_convergence
# Change spatial resolution or explicit comparison times, in seconds.
uv run --no-sync python -m research.spt.analysis \
--bins 30 --spatial-times 0 21600 36000 --time-points 101
# Analyze an explicit subset, retaining its source-derived condition labels.
uv run --no-sync python -m research.spt.analysis \
--study simulations --runs sim001 sim009 --workers 1
The default --geometry lobulus analyzes every simulation lobule and every
convergence lobule. --geometry sixth explicitly analyzes source-sector
numerical caches instead. Mixed geometries are excluded. --data-root selects
the numerical-cache root; --output-root selects local analysis storage.
Neither the raw share nor reconstructed simulation histories are modified.
Manuscript time-course figures use matched-time cell-volume-weighted means.
Simulation panels retain the pattern palettes (Greys, Blues, Purples,
Reds, Oranges, Greens, YlOrBr), dose shades, black-edged circular markers and
population SD error bars. --no-spread explicitly omits the bars; --spread
also enables them for convergence. SD describes cell variation, not uncertainty
in a mean or a confidence interval. Convergence retains a coarse-to-fine
viridis palette, plain lines, the 1.5 h reference line and the legend above
the panels. Actual mesh IDs replace obsolete IDs from the archived scripts.
Colors remain stable when selecting a subset of doses or meshes.
Panels retain bold 12 point labels, 2.7 by 2.5 inch dimensions, the original
axis labels and PP/PV ticks, and 300 DPI PNG exports. The native histories,
including every original output and each run's actual endpoint, remain available
in native-timecourses.tsv and the per-run native.nc caches.
For comparisons, 51 equally spaced times cover the common interval separately within each selected study family. Continuous cell values and cell volumes are interpolated before weighted reduction. This differs from interpolating summary means when volumes change. Discrete fields retain their previous native value, and no time is extrapolated. Source necrosis fractions are preserved; figures display their volume-weighted percentage while serialized values remain fractions. The cell data are read selectively from local NetCDF caches, without loading complete histories, point arrays, or network HDF data.
Spatial plots default to the first common time, the historical reference time
(10 h for simulations and 1.5 h for convergence), and the last common time.
If the reference lies outside the common interval, its midpoint is used instead.
Figures show individual cell scatters rather than binned curves, including
valid fractional necrosis. For quantitative comparisons, twenty equal bins cover the source's normalized rr_position, from
periportal 0 to perivenous 1. Each bin stores volume-weighted mean, population
SD, minimum, maximum, cell count, and total volume. Position 1 belongs to the
last bin. Empty or zero-volume bins retain missing statistics and zero volume;
they are never filled by spatial interpolation. Raw cell snapshots are also
serialized so later analysis can choose a different reduction.
The initial-properties figure always uses each run's first native frame,
even when explicit spatial comparison times start later. Simulation diagnostics
show the highest selected dose per pattern; convergence diagnostics show every
mesh. Diagnostics retain the manuscript's ten-bin position count histogram and
individual cell scatters. Display conversions follow the archived script:
mesh cell volume uses m³ to nL (1e12), and fluid/solid volume uses L to nL
(1e9). Pressure retains the original Pressure [?] label. Serialized arrays
remain in source units; caches do not provide independent unit metadata.
The former use of the last frame for initial diagnostics remains corrected.
Simulation figures compare doses by pattern and include matched-time dose responses. Convergence figures use the actual mesh IDs and lobulus cell counts. Resolution panels retain a linear element-count axis, black connecting line and mesh-colored markers at 1.5 h. If that time is absent from explicitly selected snapshots, the middle selected snapshot is used and the report names its time. The mesh with the largest cell count supplies the reference. Exported tables report absolute and relative differences of whole-lobule means at matched times. A zero reference makes relative differences undefined, stored as missing values. Spatial comparisons report RMS differences of bin means, weighted by reference bin volume, with the fraction of reference volume covered by mutually populated bins. These are differences from the finest available mesh, not estimates of error against a continuum solution or an inferred convergence order.
Convergence includes supplemental symlog figures to expose smaller values beside
the current coarsest mesh's large substrate/product outliers. --linear-only
omits those supplements. All values remain in the serialized results. Default
exports are PNG; --formats, --dpi, and --fields select figure output.
Reusable analysis results¶
Each run publishes a compact numerical generation under
cache/analysis/<study>/<run_id>/:
| File | Contents |
|---|---|
native.nc |
Native-time mean and population SD for selected scalars |
aligned.nc |
Matched-time statistics, reduced after cell interpolation |
profiles.nc |
Spatial statistics, counts, bin boundaries, and volumes |
cells.nc |
Selected cell fields at requested spatial times |
initial.nc |
Binned diagnostics from the first native frame |
Each study publishes an independent generation under analysis/<study>/,
containing analysis.nc, native/matched time-course TSV tables, spatial-profile
TSV tables, figures, a local HTML gallery, and provenance. Convergence also
includes convergence-means.tsv and convergence-profiles.tsv. The study NetCDF
combines matched statistics and profiles with run, dose, pattern, cell-count,
and native-end-time coordinates; native histories with differing time axes are
preserved in the native TSV table and per-run files. No pickle files are used.
The study tables and NetCDF files can be moved and read without the raw share.
The figures appear in the analysis section of the offline report.
logs/analysis-<study>.tsv supplies current result paths, and
logs/analysis-<study>.json records errors, common windows, elapsed time, and
the number of native frames read. These describe the latest selection. Unchanged
invocations inspect fingerprints without opening numerical arrays or starting
workers. Plot styling or format changes reuse numerical reductions. Missing
exports rebuild, failed publication retains previous figures, and --force
explicitly regenerates the selected analysis. --status writes nothing.
Load a validated per-run result from a checkout:
from pathlib import Path
from research.spt.analysis_data import load_analysis
from research.spt.plot_data import plot_sources
raw = Path("/data/koeniglab/spt")
output = raw / "results"
source = plot_sources(
raw, output, study="simulations", geometry="lobulus", run_ids=("sim001",)
)[0]
with load_analysis(source, output, "profiles") as profiles:
print(profiles.sel(field="rr_necrosis"))
Run and verify the complete project¶
uv run --no-sync python -m research.spt.pipeline --status
uv run --no-sync python -m research.spt.pipeline --workers 2
The pipeline runs these stages in order:
- Catalogue actual inputs and plot simulation conditions.
- Reconstruct homogeneous lobules and mixed visualization geometries.
- Build or resume the homogeneous-lobule numerical caches.
- Serialize quantitative analysis and create its figures.
- Create or resume sixth, lobulus, and combined geometry figures.
- Create the manuscript figures and the tissue panels.
It checks every selected result, stops dependent work after a failure, and writes
logs/pipeline.json plus the final index.html report at the output root. The JSON names
each stage and records its outcome and elapsed time. Repeating the command is a
supported resume operation. Existing numerical and geometry outputs are reused
only when their provenance and output fingerprints match; they are not rebuilt
merely to verify the pipeline.
Short source histories are listed separately in truncated_runs; a successful
pipeline does not imply that every source solver run reached its model duration.
--study selects a study, and --runs selects an explicit subset. A subset
omits mixed comparisons and manuscript figures, which require complete studies.
--no-combinations skips the mixed comparison lobules, which place one source
per sector and therefore need exactly six zonation patterns or six resolutions;
use it for a study whose sources do not fill one lobule.
--no-geometry-figures skips the last two stages when only numerical results
are needed. --formats selects the per-run and study figure formats and
--manuscript-formats those of the manuscript deliverables, while
--geometry-snapshots changes the geometry figure panel count. Inspecting a fresh output root reports downstream stages as pending
without creating directories or loading HDF arrays.
Portable regressions run every stage from fresh synthetic raw files, including
mixed convergence meshes, installed-library imports, serialized results and
figures. Run them with uv run --no-sync pytest -q research/tests. Validation of
the private current dataset uses the same pipeline against the read-only share.
Shared workflow code¶
SPT modules share field definitions in fields.py, filesystem publication and
locking in artifacts.py, and bounded job execution in execution.py. Numerical
caches, reconstructed geometries, analysis, and plotting use those same helpers.
Serial and parallel batches isolate job failures consistently, and empty batches
or a single pending history do not start worker processes. Field ordering,
manifest schemas, existing cache generations, and command names remain compatible.
The four plots_* command modules delegate to the shared analysis or geometry
workflow. Historical plotting adapters still serve other research projects and
delegate to the current renderer. These helpers remain repository-only research
code, outside the public library and release artifacts.
Manuscript figures¶
The complete pipeline creates the updated Figures 4-7 of the SPT manuscript, a convergence supplement and the tissue panels in its last stage. Recreate only the manuscript figures after the pipeline has finished with:
This step reads validated local NetCDF summaries, selected cell frames and cached
projected meshes. It does not require the raw network drive, rerun reductions or
alter existing plots. Unchanged figures use metadata-only cache checks. Missing
exports are repaired; --force regenerates selected manuscript figures.
| Figure | Data and layout | Restored annotations |
|---|---|---|
| 4 | sim014, linear increase, D6; five field rows by five time columns |
Pattern/dose heading, field labels, horizontal time arrow and column labels; one vertical colorbar per field row |
| 5 | One run per zonation pattern at D6, in pattern blocks | Each block has protein, toxic compound and necrosis rows, five time columns, pattern labels and icons; three shared bottom colorbars |
| 6 | Every homogeneous lobule; one pattern row per pattern and five fields | Consistent pattern labels, field headers and D1-D8 legend |
| 7 | Necrosis at 24 h; pattern rows by eight doses | Consistent pattern labels and icons, D1-D8 columns, inflow arrow and one shared binary legend on the right; dashed placeholders name runs that stop early |
| S1 | Six source-sector meshes, five common-time mixed-sixth readouts and one row of native lobule time courses | Mesh IDs and cell counts, matched boundary/curve colors, A-C panels, exact map time, linear axes, common-window boundary and truncated-history markers |
Figures 4 and 5 explicitly sample 0, 6, 12, 18 and 24 h, independently of the historical overview snapshot selection. Labels are derived from the actual times; a nonzero overview frame is never relabeled as zero. Continuous cell fields are interpolated locally and necrosis holds its preceding native frame. Shared scalar limits use full histories of the entire simulation cohort, preserving the mesh palettes. Necrosis is a cell state, so every map shows it on the physical 0-1 scale and float32 round-off just below zero never reaches a colorbar label. Maps of Figures 4, 5 and 7 show lobule outlines without internal mesh edges; outline widths scale with panel size. Only the source meshes in Figure S1 A show their triangles. Figure 6 reuses the existing volume-weighted means, population SD, markers and pattern/dose colors. SD represents spatial heterogeneity, rather than uncertainty across replicate simulations.
Time runs left to right in Figures 4 and 5, with columns for 0, 6, 12, 18 and 24 h. Figure 4 retains all five fields as rows, each with one vertical colorbar on the right. Figure 5 shows only protein, toxic compound and necrosis. Its three bottom colorbars serve every pattern block. Figure 7 uses two swatches beside the complete grid: black means necrosis and white means no necrosis. Its binary legend has no intermediate values. Individual map panels have no colorbars. A simulation whose solver stops before 24 h keeps its place in the pattern-by-dose grid as a dashed panel naming the run and its actual end time; nothing is extrapolated, and the caption and manifest list the affected runs, so a completed rerun invalidates the cached figure. The field palettes and full-cohort limits remain unchanged. Geometry axes use their cropped domain extents, including white living tissue; smaller canvases and closely packed grids reduce whitespace around the meshes.
All manuscript figures use dose labels D1-D8. The representative dose in
Figures 4 and 5 is D6. Pattern names and lettering match across Figures 4-7:
A Constant, B Linear increase, C Sharp perivenous, D Linear decrease,
E Sharp periportal, F Random and G CYP2E1. The row letters and the
cohort size follow PATTERN_NAMES in research/spt/manuscript_data.py, so
naming a further pattern extends every figure rather than one of them. Figures 6 and 7 share the same row-label
format and typography; Figure 5 uses those labels as its pattern-block headings.
Every pattern label also carries the same small square black-and-white vector profile
panel, including the Figure 4 heading. The horizontal direction runs from
periportal (left) to perivenous (right); the vertical direction shows relative
initial protein abundance. Flat, rising, falling, sharp-step, sigmoidal and
random profiles distinguish the patterns. These schematics
explain pattern identity; the random profile is illustrative.
Figures 4, 5, 7 and S1 use the same slightly tilted orthographic geometry view as the study figures, including native thickness and exterior side faces. Figure 6 retains its time-course layout and styling. Captions and provenance record the view, and manuscript scalar ranges, binary necrosis states, pattern icons and dose labels stay unchanged.
The convergence supplement retains every available native time point and actual
end time. The 06250 and 09375 histories stop at 10.16 and 21.58 h; no curve is
extended to 24 h. The common comparison window ends at 10.16 h. A single row of
linear time courses uses the six approved meshes 37250, 25000, 18750,
12500, 09375 and 06250, ordered coarse to fine. Each
resolution has a distinct qualitative color, shared by its time courses,
source-mesh boundary and mixed-sector boundaries. Source sixths are selected from
local lobule meshes, so producing this figure does not reopen the original network
meshes. The mixed view numbers those same six resolutions clockwise from upper
left, coarse to fine. This display rotation preserves cell values and the
archived composition; it applies to the Panel A mesh view and all five readouts.
Panel B compares substrate S, product P, protein, toxic compound T and necrosis
at 5400 s (1.5 h), the existing convergence reference time. All six sectors
use exactly this time, native-frame continuous interpolation and preceding-frame
necrosis states. The scalar values come from local homogeneous-lobule cell caches,
with composition order and cell counts checked before assembly. These maps show
sector outlines without internal mesh edges. Each field uses the canonical palette
and full-history limits shared by the six meshes; necrosis uses the fixed range
0-1. Use
--comparison-time SECONDS to select another time within every native history;
times outside the common interval are rejected. Dashed time-course lines mark
the map time; dotted lines mark the common interval end.
Stable manuscript filenames are available under manuscript/exports/, for
example figure-04.svg through figure-07.svg and figure-s1-convergence.svg.
A format is exported only when it was rendered, so requesting --formats pdf
adds the matching .pdf names.
These files share storage with validated generations. Regenerating a figure
refreshes its stable exports, caption and provenance from the complete generation.
Outputs live under manuscript/<figure>/manifest.json in complete generations.
Each generation contains the rendered exports, SVG and PNG by default,
caption.md and provenance.json, including run conditions, snapshot times and
native end times.
Dense map panels are rasterized at the chosen export DPI to keep the composite
files practical; annotations and time-course curves stay vector in SVG/PDF.
Each map is rendered as an independent small tile so PDF export never retains
one full-composite raster buffer per panel.
All manuscript formats are cropped to displayed content with a one-point safety
border. Cropping retains labels, arrows, legends and white living tissue, and
preserves the spacing between panels. Hidden map-axis rectangles do not add
empty margins; embedded map tiles are cropped geometrically, independently of
their scalar colors. SVG/PDF text and time courses remain vector.
The default is 300 DPI. Select --dpi 600 when higher-resolution maps are needed.
For example, regenerate only Figures 4 and 7 as SVG and PNG:
The default representative dose is index 6, matching the original manuscript.
--dose changes that selection for Figures 4 and 5 and records the choice in
their provenance and captions. Keep the default when reproducing the manuscript.
The final inspection report includes these outputs in its Manuscript
figures section and uses the same small, cached previews as other figures.
Flat tissue cutouts for linear increase¶
Create additional top-down CYP2E1, toxic-compound and necrosis panels by repeating the linear-increase lobule on a hexagonal lattice:
The defaults use D6 (sim014) at 0, 6, 12, 18 and 24 h, with canonical
palettes and full-cohort scalar limits. CYP2E1 displays the model's rr_protein
field. Continuous fields use native-time interpolation; necrosis retains the
preceding native state. The new workflow reads local processed cells and cached
XY geometry, without reopening raw HDF histories.
Each panel measures 2 native flat-to-flat lobule widths per side and samples
an independent random XY position within a 4 by 4 lobule-width tissue square.
Crop centers are sampled uniformly wherever the complete cutout fits. Each
field/time panel shows its own tissue location, including in the composite.
The default --seed 0 reproduces the crop positions; change --seed to sample
another set. Exact centers and the PCG64 generator are recorded in provenance.
The hexagonal lattice covers the entire square and its corners, with no exterior
margins. The number of intersecting lobules depends on the crop. Border cells are
clipped exactly; they keep their original cell values. Native central and portal
vessel openings remain visible. Copies repeat the same simulation; this is a tissue
visualization rather than a new multi-lobule transport solve.
These panels use a direct XY projection with zero tilt and no visible thickness,
independently of the camera used by other geometry figures. New tissue panels
omit the internal cell grid by default; --mesh enables the canonical transparent
mesh lines. --vessel-color lightgray distinguishes the native lumens from white
living tissue without assigning scalar values to the vessels.
Each standalone panel is an exact, margin-free square, 900 by 900 pixels at 300 DPI with the default settings. SVG and PDF versions contain the same bounded map image. An additional annotated composite arranges fields in rows and time from left to right, with shared scalar bars, the pattern icon and a binary necrosis legend. For a final-time selection or a wider square:
uv run --no-sync python -m research.spt.tissue_panels \
--dose 6 --times-hours 24 --width-lobules 2.5
Widths smaller than 2, negative seeds, invalid dose indices and times outside the
source history are rejected. --formats, --dpi, --mesh and --vessel-color
control only these new outputs. Existing manuscript figures and numerical
results are preserved.
The default stable exports include linear-increase-tissue-d6.svg and individual
files such as linear-increase-tissue-d6-necrosis-t86400s.png, under
manuscript/exports/. Exact fractional seconds use p instead of a decimal point
in filenames. Captions and provenance record the source run, time selection,
scalar ranges, lattice spacing, vessel coverage and clipping convention.
Outputs publish through one atomic generation under
manuscript/linear-increase-tissue-d6/manifest.json. An unchanged invocation checks
metadata without opening numerical arrays or rendering. Missing stable exports
are restored from that generation. The HTML report lists the composite and each
field/time panel separately, with small previews and descriptive titles.
Final inspection report¶
Open index.html in the results directory in a browser after the pipeline
finishes. The offline report gathers all current published figures: manuscript
figures, numerical analysis, geometry overviews, individual sixths and lobules,
mixed sectors, and simulation conditions. Exports of the same figure share one
card. Previously generated figures remain included when the latest pipeline
invocation selects a subset of the data. README.md beside it explains the
folder layout.
Search by run, pattern, dose, or title, and combine section, study family, geometry, pattern, dose, and scale filters. Click a preview for the zoom viewer. Left and right arrow keys move through the filtered selection; Escape closes the viewer. SVG and PDF links open the original exports. Data and provenance links provide the numerical tables, NetCDF results, generation metadata, and execution records. Aggregate figures match every pattern and dose they contain.
Refresh only the report after generating additional figures:
This reads local manifests and output metadata without accessing the raw share,
opening numerical arrays, or rerendering figures. It lists missing, modified, or
invalid generations as omissions instead of linking incomplete outputs. The
pipeline status refers to the latest pipeline invocation; the report does not
independently validate raw-source freshness. Condition PNGs are included when
present. The gallery uses lazily loaded, lossless WebP previews bounded by
768 by 768 pixels. Opening a figure loads its original resolution; PNG, SVG and
PDF downloads remain unchanged. Previews are cached under cache/previews/
using relative source paths and file metadata. An unchanged report refresh reads
only metadata and reuses previews without decoding the original images. Missing
or changed previews are regenerated locally without rereading simulation data,
and previews no longer referenced by the report are removed.
The HTML embeds its styling and interaction code and needs no web server or
internet connection. Images and data retain relative links. Move or share the
whole results directory, including its generation and cache folders,
to keep those links working. The report generator also recognizes relocated
generation files from their manifests. Generated reports and private results stay
outside the repository.