Tutorial 3: Sensitivity Analysis¶
Find out how strongly the fit depends on each parameter, with a local study around the optimum, simple parameter sweeps, and a global study over the full parameter ranges.
This tutorial walks through the three scripts in examples/sensitivityAnalysis/. Every code block below is taken from those scripts.
Estimated time: 15-20 minutes
What You'll Learn¶
- Run a local sensitivity study around the optimum
- Run one-parameter and two-parameter sweeps
- Run a global (Sobol) sensitivity study
- Decide which of the three fits your question
Prerequisites¶
- Completed Tutorial 1, so that the run
run1exists inprojects/exampleProject - Tutorial 2 is helpful, since the parameter sweeps use the
CustomEvaluatorintroduced there
Which Study for Which Question?¶
| Study | Question it answers | Cost in this example |
|---|---|---|
| Local sensitivity | How does the fit change when I move one parameter away from the optimum? | 14 evaluations |
| Parameter sweeps | What does the objective look like along one or two parameters I choose? | 6 + 9 evaluations |
| Global sensitivity | Over the whole allowed range, which parameters drive the result? | 128 evaluations |
All three vary neutralRate and ionRate and keep the other parameters at their optimum.
Common Setup¶
The two sensitivity studies start by loading the project, the process sequence of run1 and its optimum:
import viennafit as fit
import viennaps as vps
import json
import os
# Load the project
p1 = fit.Project()
projectToLoad = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"../projects/exampleProject",
)
p1.load(projectToLoad)
# Use the process sequence and the optimum from a previous optimization run
runDir = os.path.abspath(os.path.join(p1.projectPath, "optimizationRuns", "run1"))
processPath = os.path.join(runDir, "run1-processSequence.py")
with open(os.path.join(runDir, "run1-final-results.json")) as f:
optimum = json.load(f)["bestParameters"]
Every optimization run keeps a copy of its process sequence (run1-processSequence.py), so a study can reuse exactly the model that was fitted.
Part 1: Local Sensitivity¶
Script: examples/sensitivityAnalysis/localSensitivityStudy.py
A local study moves one parameter at a time through a range while all others stay put.
lss1 = fit.LocalSensitivityStudy("lss1", p1)
lss1.loadProcessSequence(processPath)
# Declare all parameters used by the process sequence
lss1.setParameterNames(
["neutralStickP", "ionPowerCosine", "neutralRate", "ionRate", "ionEnergy"]
)
# Keep the remaining parameters fixed at their optimal values
lss1.setFixedParameters(
{name: optimum[name] for name in ["ionEnergy", "neutralStickP", "ionPowerCosine"]}
)
# Set variable parameters as (lowerBound, pointOfInterest, upperBound):
# each one is varied on its own around the optimum, within the optimization bounds
lss1.setParameterSensitivityRanges(
{
"neutralRate": (1.0, optimum["neutralRate"], 150.0),
"ionRate": (0.1, optimum["ionRate"], 10.0),
}
)
# Same distance metric as used in the optimization
lss1.setDistanceMetric("CCH")
lss1.validate()
lss1.apply()
The points to note:
setParameterSensitivityRangestakes(lowerBound, pointOfInterest, upperBound)per parameter. The point of interest is the optimum; the bounds are the ones used in the optimization.- By default each parameter is evaluated at 7 points across its range. Pass
nEval=(n1, n2)with one entry per variable parameter to change that. validate()checks the configuration before any simulation is started.
Run it:
The 14 evaluations take under a minute. The output goes to projects/exampleProject/locSensStudies/lss1/, with the individual evaluations in evaluations/ and the summary in sensitivity_results.json.
Reading the result¶
For each parameter you get the objective value along its range. Look at the shape:
- A sharp minimum at the optimum: the parameter is well determined by the target.
- A flat curve: the target says little about this parameter. Its fitted value should not be over-interpreted, and it is a candidate for being fixed.
- A minimum away from the point of interest: the optimization had not converged for this parameter.
Keep in mind that a local study only moves along the axes. The trade-off between neutralRate and ionRate seen in the grid of Tutorial 2 runs diagonally and does not show up here.
Part 2: Parameter Sweeps¶
Script: examples/sensitivityAnalysis/parameterStudies.py
Sometimes a plain sweep with hand-picked values is all you need. This script uses the CustomEvaluator for two such studies.
One parameter¶
evaluator = fit.CustomEvaluator(p1)
# Load optimization results from a previous run
evaluator.loadOptimizationRun("run1")
evaluator.setDistanceMetric("CCH")
# Define a simple parameter sweep for one parameter
singleParamValues = {
"neutralRate": [40.0, 60.0, 80.0, 100.0, 120.0, 140.0],
}
evaluator.setVariableValues(singleParamValues)
results = evaluator.apply(evaluationName="singleParameterSweep", saveComparison=False)
print(f"Evaluated {len(results)} parameter combinations")
bestResult = evaluator.getBestResult()
if bestResult:
print(f"Best objective value: {bestResult['objectiveValue']:.6f}")
print(f"Best neutralRate: {bestResult['parameters']['neutralRate']:.2f}")
Two parameters¶
evaluator2 = fit.CustomEvaluator(p1)
evaluator2.loadOptimizationRun("run1")
evaluator2.setDistanceMetric("CCH")
# Study interaction between two parameters around their optimal values (±20%)
optimalParams = evaluator2.getOptimalParameters()
factors = [0.8, 1.0, 1.2]
twoParamValues = {
"neutralRate": [optimalParams["neutralRate"] * f for f in factors],
"ionRate": [optimalParams["ionRate"] * f for f in factors],
}
evaluator2.setVariableValues(twoParamValues)
results2 = evaluator2.apply(evaluationName="twoParameterStudy", saveComparison=False)
With saveComparison=False only the numbers are kept, not the surfaces.
Both studies together take under a minute. The output goes to customEvaluations/singleParameterSweep/ and customEvaluations/twoParameterStudy/.
Unlike the local study, the 3 x 3 grid includes the corners, where both parameters are changed together. Comparing the corner (0.8, 1.2) with the corner (1.2, 1.2) shows the interaction between the two rates directly.
Part 3: Global Sensitivity¶
Script: examples/sensitivityAnalysis/globalSensitivityStudy.py
A global study samples the whole parameter range and computes Sobol indices: the share of the variance of the objective that each parameter is responsible for.
gss = fit.GlobalSensitivityStudy("gss1", p1)
gss.loadProcessSequence(processPath)
# Declare all parameters used by the process sequence
gss.setParameterNames(
["neutralStickP", "ionPowerCosine", "neutralRate", "ionRate", "ionEnergy"]
)
# Keep the remaining parameters fixed at their optimal values
gss.setFixedParameters(
{name: optimum[name] for name in ["ionEnergy", "neutralStickP", "ionPowerCosine"]}
)
# Set variable parameters with ranges
gss.setVariableParameters(
{
"neutralRate": (1.0, 150.0),
"ionRate": (0.1, 10.0),
}
)
# Same distance metric as used in the optimization
gss.setDistanceMetric("CCH")
# First-order and total-order indices only: numSamples * (numParams + 2) = 128
# evaluations. Second-order indices (secondOrder=True) need at least 500 samples
# per variable parameter, i.e. 1000 * (2 * 2 + 2) = 6000 evaluations here.
gss.setSamplingOptions(numSamples=32, secondOrder=False)
# Run the sensitivity analysis
gss.apply(saveComparison=False)
The number of evaluations is set by setSamplingOptions:
| Setting | Evaluations | Here |
|---|---|---|
secondOrder=False |
numSamples * (numParams + 2) |
32 * 4 = 128 |
secondOrder=True |
numSamples * (2 * numParams + 2) |
needs far more samples, 6000 evaluations |
The 128 evaluations take about 5-10 minutes. The output goes to projects/exampleProject/globSensStudies/gss1/:
| File | Content |
|---|---|
parameter_samples.csv |
The sampled parameter sets |
objective_values.csv |
The objective value of each sample |
evaluation_results.json |
Detailed result of each evaluation |
sensitivity_results.json |
The Sobol indices |
Reading the result¶
Two indices are reported per parameter, each with a confidence interval (firstOrderConf, totalOrderConf):
- First-order index S1 (
firstOrder): the share of the variance explained by this parameter alone. - Total-order index ST (
totalOrder): the share explained by this parameter including its interactions with the others.
How to read them:
- ST close to zero: the parameter does not matter over this range and can be fixed.
- ST clearly larger than S1: the parameter acts mainly through interactions. For the two rates here that is expected, because of the trade-off between them.
- Indices that are noisy or slightly negative: the sample is too small. 32 samples are enough for a ranking, not for precise values; increase
numSamplesfor those.
Global is not local
A global study describes the whole range inside the bounds, where most parameter sets fit badly. It tells you which parameters decide between a bad and a reasonable fit. How well a parameter is pinned down near the optimum is a question for the local study.
Key Takeaways¶
- All studies reuse the process sequence and the optimum stored in the optimization run.
- A local study is cheap and shows how well each parameter is determined at the optimum, one axis at a time.
- Sweeps with the
CustomEvaluatorare the quickest way to look at specific values or at two parameters together. - A global study ranks parameters over their full range and exposes interactions, at a much higher cost.
Next Steps¶
- Tutorial 4: Multi-Domain Optimization - fit one parameter set to several geometries
- Core Concepts - background on the study types