Skip to content

Workflows with simrun

The tutorial runs every analysis of the HCTZ model in one python session: a dose, a parameter scan, a sensitivity analysis, a fit and an uncertainty analysis. This page runs the same analyses as one workflow of simrun, on your laptop with --local or on a cluster with the same file, and shows how to bring an analysis of your own there.

A workflow is a YAML file of tasks. Every task is an analysis of a project, run by the run layer of sbmlsim, and simrun schedules the tasks, splits the large ones into units (the repeats of a fit, the points of a scan, the draws of an uncertainty analysis), runs them on all cores or servers, merges the units, and keeps the outputs, the logs and the provenance of every run. A web app shows the runs live.

From the tutorial to a project

The interactive code of the tutorial builds its objects in a session and writes nothing. A workflow needs every analysis by name, so the analyses become entries of a Project, a description of the model package. The HCTZ example has one, examples/hctz_fitting/project.py:

tutorial project kind of the run layer task type of simrun
Simulate a dose, the simulation experiments of the studies experiments simulate sbmlsim.simulate
Scan parameters scans, a ScanAnalysis scan sbmlsim.scan
Sensitivity analysis scans, a ScanAnalysis with indices sensitivity sbmlsim.sensitivity
Fit the model to data fits, a FitDefinition per subset fit, identifiability, report sbmlsim.fit, sbmlsim.identifiability, sbmlsim.report
Uncertainty analysis uncertainties, an UncertaintyAnalysis uncertainty sbmlsim.uncertainty

python -m sbmlsim.run list shows what a project offers; run it from the root of the repository, which python -m puts on the path:

python -m sbmlsim.run list examples.hctz_fitting.project:PROJECT
kind             name   description
simulate         all    the simulation experiments of the project
fit              PK     fit of the problem 'PK'
identifiability  PK     profile likelihoods of the fit of the problem 'PK'
...
scan             dose   oral doses of 12.5, 25 and 50 mg over one day
scan             sobol  Sobol design of the dissolution and the renal excretion of a 25 mg oral dose
sensitivity      sobol  sobol indices of the scan 'sobol'
uncertainty      cl     the AUC of a 25 mg oral dose under a renal excretion uncertain by 30 %

Converting an analysis

Take the dose scan of the tutorial. As a script it builds the scan, runs it and saves the result:

simulator = Simulator()
model = simulator.load(HCTZ_SBML)
dose = Simulation(
    time_unit="hr", end=24, changes=[Change(0, {"PODOSE_hctz": Q(25, "mg")})]
)
scan = Scan(dose, [Dimension("dose", values={"PODOSE_hctz": Q([12.5, 25, 50], "mg")})])
observables = [
    Formula("cp", "[Cve_hctz] * Mr_hctz", unit="ng/ml"),
    PK("pk", "cp", dose="PODOSE_hctz", route="oral"),
]
res = simulator.run(model, scan, observables, time=np.linspace(0, 24 * 60, 49))
res.to_netcdf("dose.nc")

In a project the same analysis is a set of functions which build the objects, and an entry which names them:

def oral_dose() -> Simulation:
    return Simulation(
        time_unit="hr", end=24, changes=[Change(0, {"PODOSE_hctz": Q(25, "mg")})]
    )


def dose_scan() -> Scan:
    return Scan(
        oral_dose(),
        [
            Dimension(
                "dose",
                values={"PODOSE_hctz": Q([12.5, 25, 50], "mg")},
                labels=["12.5 mg", "25 mg", "50 mg"],
            )
        ],
    )


def observables() -> list[Observable]:
    return [
        Formula("cp", "[Cve_hctz] * Mr_hctz", unit="ng/ml"),
        PK("pk", "cp", dose="PODOSE_hctz", route="oral"),
    ]


PROJECT = Project(
    name="hctz",
    root=HCTZ_PATH,
    scans={
        "dose": ScanAnalysis(
            model=MODEL_PATH,
            scan=dose_scan,
            observables=observables,
            time=DOSE_TIMES,
            description="oral doses of 12.5, 25 and 50 mg over one day",
        ),
    },
)

What changes, and why:

  • Functions instead of objects. Every unit of a split analysis runs in its own process, possibly on another server, and builds the scan again from the project. A function of the module builds it the same way everywhere; the run layer checks that every unit built the same scan (scan_hash).
  • Deterministic. A design, a sample or a draw has a fixed seed (sobol(..., seed=1), UncertaintyAnalysis(seed=1)), so a unit on one server draws the same points as on another. The plan draws a seed for a fit or a propagation without one and records it.
  • Paths of the package. The model, the data and the records are paths relative to the package (MODEL_PATH, DATA_PATH), never to the current directory, which differs between a laptop, a worker process and a pod.
  • No output code. The run layer writes the result (result.nc, the indices, the reports) and its provenance into the output of the task, so the script code which saved or plotted the result is gone; read the result afterwards, see Results.
  • Importable. The project is a module attribute, examples.hctz_fitting.project:PROJECT, which the client of simrun imports to plan the workflow and every task imports to run it. A model package installs its project with the package, see Projects.

Check the converted analysis on its own before it goes into a workflow, with a spec which names it:

dose.yaml
project: examples.hctz_fitting.project:PROJECT
analysis: dose
python -m sbmlsim.run plan scan dose.yaml          # the planned spec, units and hash
python -m sbmlsim.run scan dose.yaml --out results/dose

The other analyses convert the same way: an experiment class into experiments, a scan with a sensitivity design into a ScanAnalysis with indices="sobol", a fit into a FitDefinition of fits, a propagation into an UncertaintyAnalysis. Analyses lists the settings of every kind.

The workflow

examples/hctz_fitting/workflows/tutorial.yaml runs the analyses of the tutorial. A task names its type and the spec of its kind as settings; inputs connect a task to the output out of another one, here the fit to its profiles and the profiles to the report:

examples/hctz_fitting/workflows/tutorial.yaml
name: hctz-tutorial
tasks:
  - id: simulate
    type: sbmlsim.simulate
    settings:
      project: examples.hctz_fitting.project:PROJECT
  - id: dose
    type: sbmlsim.scan
    settings:
      project: examples.hctz_fitting.project:PROJECT
      analysis: dose
  - id: sobol
    type: sbmlsim.sensitivity
    settings:
      project: examples.hctz_fitting.project:PROJECT
      analysis: sobol
  - id: cl
    type: sbmlsim.uncertainty
    settings:
      project: examples.hctz_fitting.project:PROJECT
      analysis: cl
  - id: fit
    type: sbmlsim.fit
    settings:
      project: examples.hctz_fitting.project:PROJECT
      subset: PK
      repeats: 8
      seed: 1
  - id: profiles
    type: sbmlsim.identifiability
    settings:
      project: examples.hctz_fitting.project:PROJECT
      subset: PK
    inputs:
      fit: fit.out
  - id: report
    type: sbmlsim.report
    settings:
      project: examples.hctz_fitting.project:PROJECT
      subset: PK
    inputs:
      fit: profiles.out

The tasks without inputs run side by side as soon as cores are free, profiles waits for fit and report for profiles.

Run it on your laptop

Install sbmlsim with the extra simrun, which installs simrun and registers the task types sbmlsim.<kind>:

pip install "sbmlsim[simrun]"

From the root of the repository, --local runs the workflow with the local service of simrun, which starts in the background and keeps its runs in .simrun/ of the current directory. The HCTZ project is in examples/, which is not part of the package sbmlsim; a task imports from the root of the source of the model first, as python -m in the checkout, so it finds examples without further settings:

simrun estimate examples/hctz_fitting/workflows/tutorial.yaml --source sbmlsim=. --local
simrun submit examples/hctz_fitting/workflows/tutorial.yaml --source sbmlsim=. --local

estimate shows the plan of every task: its processes, the cores and the memory it asks for, and the units of a split task (3 points of the dose scan, 32 blocks of the Sobol design and of the draws, 8 repeats of the fit, a profile per parameter). submit plans the tasks, records the provenance of the sources and follows the run to its end with a line per task; on 20 idle cores the workflow takes about a minute:

simrun status <run> --local
┏━━━━━━━━━━┳━━━━━━━┳━━━━━━┳━━━━━━━┓
┃ task     ┃ state ┃ done ┃ total ┃
┡━━━━━━━━━━╇━━━━━━━╇━━━━━━╇━━━━━━━┩
│ simulate │ done  │      │       │
│ dose     │ done  │ 3    │ 3     │
│ sobol    │ done  │ 32   │ 32    │
│ cl       │ done  │ 32   │ 32    │
│ fit      │ done  │ 8    │ 8     │
│ profiles │ done  │ 3    │ 3     │
│ report   │ done  │      │       │
└──────────┴───────┴──────┴───────┘

The web app

The local service serves the web app of simrun at /ui/ of the address in .simrun/service.json, which asks for the token stored there. The Executions show the run as a graph of its tasks, with the units of every split task and the cores of the machine, live while it runs:

The Executions of the tutorial workflow: the simulations, the scans and the uncertainty analysis are done, the fit runs, the profiles and the report wait for it

A click on a task opens it in the inspector on the right: its Overview, its Outputs and its Logs. The outputs of the report are the report of the fit with its profiles, metrics and figures:

The outputs of the task report

The logs are the console of the run layer, here the repeats of the fit with their costs; long lines scroll sideways, wrap lines wraps them at the width of the inspector:

The logs of the task fit

Results

simrun outputs lists the files of a task or copies them with --to; every task writes the output of its run into out/, see The output:

simrun outputs <run> dose --local
simrun outputs <run> report --local --to results/report
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ path                      ┃  bytes ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
│ out/blocks/points-0-1.nc  │ 157608 │
│ out/blocks/points-1-2.nc  │ 157608 │
│ out/blocks/points-2-3.nc  │ 157608 │
│ out/progress/unit-0.jsonl │    222 │
│ out/progress/unit-1.jsonl │    222 │
│ out/progress/unit-2.jsonl │    222 │
│ out/progress.jsonl        │      0 │
│ out/result.nc             │ 158845 │
│ out/run.json              │   1614 │
...

A task of simrun runs the command line, which has no terminal: it writes a line of its progress every 30 seconds into the log of the task and the progress events into out/progress.jsonl (out/progress/unit-<index>.jsonl for a unit), so how far a task is and what of its worker it uses are in the log and in the file, see Progress.

The results are the objects of the tutorial, read from their files:

from sbmlsim.result.scan import ScanResult
from sbmlsim.sensitivity import SensitivityResult
from sbmlsim.uncertainty import UncertaintyResult

dose = ScanResult.from_netcdf("results/dose/out/result.nc")
print(dose["pk.cmax"].round(1).to_pandas())

sobol = SensitivityResult.from_netcdf("results/sobol/out/sensitivity.nc")
print(sobol.index("ST").to_pandas().round(2))

cl = UncertaintyResult.from_netcdf("results/cl/out/uncertainty")
print(
    cl.summary(outputs=["pk.auc_inf_obs"])[
        ["output", "n", "mean", "sd", "q0.05", "q0.95"]
    ]
)
dose
12.5 mg     67.3
25 mg      134.6
50 mg      269.2

observable    pk.cmax  pk.auc_inf_obs
parameter
Ka_dis_hctz      0.96            0.00
KI__HCTZEX_k     0.15            0.43

        output  n    mean      sd   q0.05    q0.95
pk.auc_inf_obs 32 77768.0 19271.0 51348.0 113101.0

results/report/out/report/index.html is the report of the fit, as in Parameter fitting, with the profiles of the identifiability analysis:

The report of the fit PK, read from the outputs of the task report

A split result equals the result of the whole analysis on one machine bitwise, so the numbers do not depend on how the run was split, see Units and merges.

The Editor

simrun edit opens the workflow file in the Editor of the web app: the task types of sbmlsim in the palette, the tasks as cards, the settings of the selected task as a form of its spec, without the fields the plan sets, such as the units. A save writes the YAML file, Run workflow submits it:

simrun edit examples/hctz_fitting/workflows/tutorial.yaml --source sbmlsim=. --local

The Editor with the tutorial workflow and the settings of the fit

Run it on a cluster

The same workflow runs on a cluster of simrun without --local, with the address and the token of the service in SIMRUN_URL and SIMRUN_TOKEN. The client takes snapshots of the sources, which the pods install, and the split tasks spread their units over the idle servers:

simrun submit examples/hctz_fitting/workflows/tutorial.yaml --source sbmlsim=.
simrun wait <run>
simrun outputs <run> report --to results/report

A project of a model package outside of sbmlsim names its source first and sbmlsim second, e.g. --source model=~/git/my_model --source sbmlsim=~/git/sbmlsim: the pods install the snapshots, which install the package of the model with its project, and import from the root of the model first, so a project at the root of a checkout, such as the examples of sbmlsim, is found as well. simrun in Analyses describes the plan in the client, the provenance and the task types, the documentation of simrun the service, the cluster and the web app.