Source code for modelly.sensitivity.spec

"""Read a sensitivity analysis from a TOML file (what ``modelly sensitivity spec.toml`` runs).

The file reuses the experiment format; ``[sensitivity]`` picks the method::

    [experiment]
    model = "model.py:SIR"          # a Model class, or module:function of the parameters
    steps = 60
    replicates = 2
    seed = 1
    outputs = ["infected"]          # the first output is analysed
    store = "runs/sensitivity"      # optional

    [space]
    beta = { min = 0.1, max = 0.8 }
    gamma = { min = 0.05, max = 0.3 }
    population = 1000

    [sensitivity]
    method = "morris"               # oat, morris, sobol, pawn
    statistic = "max"               # last, max, min, mean, sum
    trajectories = 20               # the method's settings

    [run]
    executor = "processes"
"""

from __future__ import annotations

import inspect
from collections.abc import Callable
from pathlib import Path
from typing import Any

from modelly.experiments.spec import (
    SpecError,
    model_reference,
    read_document,
    read_run,
    read_space,
    relative_path,
)
from modelly.experiments.tasks import resolve
from modelly.sensitivity.pawn import pawn
from modelly.sensitivity.screening import morris, oat
from modelly.sensitivity.study import is_model
from modelly.sensitivity.variance import sobol

METHODS: dict[str, Callable[..., Any]] = {
    "oat": oat,
    "morris": morris,
    "sobol": sobol,
    "pawn": pawn,
}
EXPERIMENT_KEYS = ("steps", "replicates", "seed")
STUDY_KEYS = {"statistic", "output"}


def _method(section: dict[str, Any], override: str | None) -> Callable[..., Any]:
    """The method ``[sensitivity]`` (or ``override``) names; its settings are checked."""
    name = section.pop("method", "morris")
    name = name if override is None else override
    function = METHODS.get(name)
    if function is None:
        raise SpecError(
            f"sensitivity.method must be one of {sorted(METHODS)}, got {name!r}"
        )
    accepted = set(inspect.signature(function).parameters) | STUDY_KEYS
    unknown = set(section) - accepted
    if unknown:
        raise SpecError(f"{name} takes no setting(s) {sorted(unknown)}")
    return function


[docs] def load_sensitivity( path: str | Path, method: str | None = None ) -> tuple[Callable[..., Any], dict[str, Any]]: """The method a TOML file (or ``method``) names and every keyword argument for it.""" document, base = read_document(path) table = document.get("experiment", {}) if "model" not in table: raise SpecError(f"{path}: [experiment] needs model = 'path/model.py:Class'") section = dict(document.get("sensitivity", {})) function = _method(section, method) reference = model_reference(table["model"], base) model = reference if is_model(resolve(reference)) else resolve(reference) outputs = table.get("outputs", []) output = section.pop("output", outputs[0] if outputs else None) arguments = {"model": model, "space": read_space(document), "output": output} arguments |= section | {key: table[key] for key in EXPERIMENT_KEYS if key in table} if "store" in table: arguments["store"] = relative_path(table["store"], base) run = read_run(document, "serial") arguments |= {"executor": run.executor, "max_parallel": run.max_parallel} return function, arguments | {"options": run.options}