Skip to content

terraflow.sensitivity

Sobol' first-order / total-order indices and Morris elementary effects for ModelParams weights. Invoked from the CLI as terraflow sensitivity -c config.yml; results land in sensitivity_report.json in the run directory.

API Reference

sensitivity

Sensitivity analysis module -- Sobol' and Morris methods via SALib.

run_sensitivity(config_path)

Run sensitivity analysis and write sensitivity_report.json.

Loads config from config_path, validates the sensitivity: section exists, runs Sobol' and/or Morris analysis per the method setting, writes sensitivity_report.json atomically to output_dir, and prints ranked parameter tables to stdout.

Parameters:

Name Type Description Default
config_path Path

Path to a TerraFlow YAML config file that includes a sensitivity: section with weight bounds and n_samples.

required

Returns:

Name Type Description
Path Path

Absolute path to the written sensitivity_report.json.

Raises:

Type Description
ValueError:

If the config has no sensitivity: section.

FileNotFoundError:

If config_path does not exist.

Source code in terraflow/sensitivity.py
def run_sensitivity(config_path: Path) -> Path:
    """Run sensitivity analysis and write sensitivity_report.json.

    Loads config from *config_path*, validates the ``sensitivity:`` section
    exists, runs Sobol' and/or Morris analysis per the ``method`` setting,
    writes ``sensitivity_report.json`` atomically to ``output_dir``, and
    prints ranked parameter tables to stdout.

    Parameters
    ----------
    config_path:
        Path to a TerraFlow YAML config file that includes a ``sensitivity:``
        section with weight bounds and ``n_samples``.

    Returns
    -------
    Path:
        Absolute path to the written ``sensitivity_report.json``.

    Raises
    ------
    ValueError:
        If the config has no ``sensitivity:`` section.
    FileNotFoundError:
        If *config_path* does not exist.
    """
    data = load_config_dict(config_path)
    cfg = build_config(data)
    # Resolve output_dir relative to the config file (mirroring run_pipeline)
    # so `output_dir: ../outputs/demo_run` lands under the config's parent
    # directory regardless of the caller's working directory.
    config_dir = Path(config_path).resolve().parent
    if not cfg.output_dir.is_absolute():
        cfg.output_dir = (config_dir / cfg.output_dir).resolve()

    if cfg.sensitivity is None:
        raise ValueError(
            "Config file has no 'sensitivity:' section. "
            "Add a sensitivity: block with w_v, w_t, w_r bounds and n_samples. "
            "See terraflow documentation for config format."
        )

    sens_cfg = cfg.sensitivity
    problem = _build_problem(sens_cfg)
    method = sens_cfg.method
    n_samples = sens_cfg.n_samples

    report: Dict[str, Any] = {
        "schema_version": "1",
        "method": method,
        "n_samples": n_samples,
        "parameters": problem["names"],
        "bounds": {
            "w_v": {"low": sens_cfg.w_v.low, "high": sens_cfg.w_v.high},
            "w_t": {"low": sens_cfg.w_t.low, "high": sens_cfg.w_t.high},
            "w_r": {"low": sens_cfg.w_r.low, "high": sens_cfg.w_r.high},
        },
        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }

    if method in ("sobol", "both"):
        sobol_result = _run_sobol(problem, cfg, n_samples)
        report["sobol"] = sobol_result
        _print_sobol_table(sobol_result)

    if method in ("morris", "both"):
        morris_result = _run_morris(problem, cfg, n_samples)
        report["morris"] = morris_result
        _print_morris_table(morris_result)

    # Write report atomically to output_dir
    output_dir = ensure_dir(cfg.output_dir)
    report_path = output_dir / "sensitivity_report.json"
    _atomic_write_json(report_path, report)
    logger.info(f"Sensitivity report written to {report_path}")

    return report_path