Skip to content

Sensitivity Analysis

The hera.utils.SALibUtils module provides the SALibUtils class, which bridges Hera configurations with the SALib sensitivity analysis library. It handles building properly formatted problem definitions, supporting typed parameters (float, int, log-scale, and categorical), and transforming raw SALib samples back to their actual values.

Importing

from hera.utils.SALibUtils import SALibUtils

The module imports SALib at load time, and SALib is not listed in requirements.txt or setup.py. Install it first:

pip install SALib

Defining a Problem

Use SALibUtils.buildSAProblem to define the parameters and their types. Plain two-element lists are treated as float ranges. Use the typeInt, typeList, and typeLog helpers for other types.

problem_container = SALibUtils.buildSAProblem(
    # Float parameter: uniform between 0.001 and 0.1
    viscosity=[0.001, 0.1],

    # Integer parameter: 1 through 10
    mesh_layers=SALibUtils.typeInt(1, 10),

    # Log-scale parameter: 10^1 through 10^5
    reynolds=SALibUtils.typeLog(1, 5),

    # Categorical parameter: pick from a list
    solver=SALibUtils.typeList(["simpleFoam", "pisoFoam", "pimpleFoam"]),
)

The returned problem_container is a dict with two keys:

  • "problem" -- the SALib problem dict (num_vars, names, bounds), ready to pass to any SALib sampler.
  • "type" -- metadata mapping each parameter name to its type descriptor ({"type": ..., "parameters": ...}), used by transformSample.

Each keyword argument is either a two-element [lower, upper] list or a type descriptor dict produced by the helpers below.

Type Helpers

Helper Description Example
SALibUtils.typeInt(lower, upper) Integer range; the sample is floored to an int typeInt(1, 10)
SALibUtils.typeList(items) Categorical choice from a list typeList(["a", "b", "c"])
SALibUtils.typeLog(lower, upper) Log-scale: sampled value becomes 10**x typeLog(-3, 0)
Plain [lower, upper] list Continuous float range [0.0, 1.0]

Current bounds of typeInt and typeLog

The bounds passed to SALib are wider than the helper arguments suggest. typeInt(lower, upper) stores [lower, upper + 1] and buildSAProblem adds 1 again, so the sampled bounds are [lower, upper + 2]: typeInt(1, 10) produces integers 1 to 11 (and 12 only at the exact upper bound). typeLog(lower, upper) is sampled in [lower, upper + 1]: typeLog(1, 5) gives values from 10 to 10**6, and typeLog(-3, 0) from 0.001 to 10. Check problem_container["problem"]["bounds"] and pass tighter arguments if you need exact ranges.

Sampling

Pass the "problem" dict to any SALib sampling method:

from SALib.sample import saltelli

samples = saltelli.sample(problem_container["problem"], N=256)
print(samples.shape)  # (N * (2D + 2), D) where D is number of parameters

Transforming Samples

Raw SALib samples are always floats in the specified bounds. Use transformSample to convert them to their intended types (rounding integers, indexing into lists, applying log transforms):

transformed = SALibUtils.transformSample(samples, problem_container)

for row in transformed[:3]:
    print(row)
# [0.042, 5, 1000.0, 'pisoFoam']
# [0.087, 2, 31.62, 'simpleFoam']
# ...

transformSample returns a plain Python list of lists (not a NumPy array). Each row is a list of parameter values in the same order as problem_container["problem"]["names"].

Full Workflow Example

from SALib.sample import saltelli
from SALib.analyze import sobol
import numpy as np
from hera.utils.SALibUtils import SALibUtils

# 1. Define the problem
problem_container = SALibUtils.buildSAProblem(
    amplitude=[0.5, 2.0],
    frequency=SALibUtils.typeInt(1, 10),
    phase=SALibUtils.typeList([0, 90, 180, 270]),
)

# 2. Generate samples
raw_samples = saltelli.sample(problem_container["problem"], N=512)

# 3. Transform to actual parameter values
param_sets = SALibUtils.transformSample(raw_samples, problem_container)

# 4. Run the model (replace with your actual model)
Y = np.array([
    row[0] * np.sin(row[1] * 2 * np.pi + np.radians(row[2]))
    for row in param_sets
])

# 5. Analyze sensitivity
Si = sobol.analyze(problem_container["problem"], Y)
print("First-order indices:", Si["S1"])
print("Total-order indices:", Si["ST"])