Sensitivity Analysis - Nouman090/ThermoSim GitHub Wiki

The SensitivityAnalyzer lets you study how cycle performance changes when you vary one or more design parameters. It also supports analyzing existing CSV data files.

Key Concept: Build Function

You write a build function that takes a dictionary of parameters and returns a fully-solved model. The analyzer calls this function many times with different parameter values.

def build_my_cycle(params):
    """Build and solve a cycle from parameters."""
    M = ThermodynamicModel()
    M.set_dead_state()
    
    T1 = params['T1']
    P1 = params['P1']
    eta_t = params['eta_t']
    # ... define state points and components ...
    
    
    M.Solve()
    return M   # return the solved model

Setup

from ThermoSim.analysis import SensitivityAnalyzer
import numpy as np

# Base parameter values
base_params = {
    'T1': 753.15,
    'P1': 8e6,
    'P_cond': 0.008e6,
    'eta_t': 0.85,
    'eta_p': 0.85,
}

# For parametric sweeps
sa = SensitivityAnalyzer(build_my_cycle, base_params)

# For CSV-only analysis (no build function needed)
sa_csv = SensitivityAnalyzer()

1. Single-Parameter Sweep

Vary one parameter while keeping everything else at base values.

df = sa.single_sweep(
    param_name='T1',                                    # which parameter
    values=np.arange(623.15, 873.15, 25),              # values to try
    outputs={                                           # what to measure
        'Efficiency (%)':   lambda m: m.Efficiency,
        'Net Power (kW)':   lambda m: m.Net_power / 1e3,
    },
    x_label='Turbine Inlet Temperature [K]',
    save_to='sweep_T1.csv'                             # optional: save results
)

print(df)  # results as a DataFrame

2. Multi-Output Sweep

Plot multiple outputs on one chart with separate y-axes.

df = sa.multi_output_sweep(
    param_name='P1',
    values=np.arange(4e6, 16e6, 1e6),
    outputs={
        'Efficiency (%)':    lambda m: m.Efficiency,
        'Net Power (kW)':    lambda m: m.Net_power / 1e3,
        'Turbine Work (kW)': lambda m: m.Component['Turbine'].work / 1e3,
    },
    x_label='Boiler Pressure [Pa]',
    save_to='sweep_P1_multi.csv'                       # optional: save results
)

3. Two-Parameter Contour Plot

Vary two parameters simultaneously and create a contour/heatmap.

df = sa.double_sweep(
    param1_name='T1',
    param1_values=np.arange(623.15, 873.15, 50),
    param2_name='P1',
    param2_values=np.arange(4e6, 14e6, 2e6),
    output_name='Efficiency (%)',
    output_func=lambda m: m.Efficiency,
    save_to='sweep_2D.csv'                             # optional: save results
)

4. Analyzing Existing CSV Data

Single-Parameter Analysis from CSV

# Analyze previously saved results
df = sa_csv.analyze_csv(
    filename='sweep_T1.csv',
    param_name='T1',
    output_names=['Efficiency (%)', 'Net Power (kW)'],
    x_label='Turbine Inlet Temperature [K]'
)

Two-Parameter Analysis from CSV

# Create contour plot from CSV data
pivot = sa_csv.analyze_csv_double(
    filename='sweep_2D.csv',
    param1_name='T1',
    param2_name='P1',
    output_name='Efficiency (%)'
)

Compare Two CSV Files

from ThermoSim.analysis import compare_csv_files

# Compare baseline vs. optimized design
compare_csv_files(
    file1='baseline_sweep.csv',
    file2='optimized_sweep.csv',
    param_name='T1',
    output_names=['Efficiency (%)', 'Net Power (kW)'],
    labels=['Baseline', 'Optimized']
)

5. Sensitivity Indices

Compute statistical sensitivity metrics from sweep results.

# After running a sweep
df = sa.single_sweep('T1', np.arange(623, 873, 25),
                     {'Efficiency (%)': lambda m: m.Efficiency})

# Compute sensitivity metrics
metrics = sa.compute_sensitivity_indices(df, 'T1', 'Efficiency (%)')

print(f"Sensitivity Index: {metrics['sensitivity_index']:.3f}")
print(f"Correlation: {metrics['correlation']:.3f}")
print(f"Output Range: {metrics['output_range']:.2f} %")
print(f"Min/Max Output: {metrics['output_min']:.2f} / {metrics['output_max']:.2f} %")

6. Save and Load Results

# Manual save
sa.save_results(df, 'my_results.csv', overwrite=False)

# Manual load
df_loaded = sa.load_results('my_results.csv')

Complete Example


import numpy as np
from ThermoSim import (
    ThermodynamicModel, Turbine, Pump, HeatExchanger,
)
from ThermoSim.analysis import SensitivityAnalyzer


def build_rankine(params):
    M = ThermodynamicModel()
    M.set_dead_state()
    
    M.add_point('water', '1', P=params['P1'],
                T=params['T1'], Mass_flowrate=1)
    M.add_point('water', '2', P=params['P_cond'])
    M.add_point('water', '3', P=params['P_cond'], Q=0)
    M.add_point('water', '4', P=params['P1'])
    
    Turbine(M, 'Turbine', '1', '2',
            n_isen=params['eta_t'])
    HeatExchanger(M, 'Condenser', PPT=5, HEX_type='SimpleHEX',
                  HeatAdded=False,
                  Hot_In_state='2', Hot_Out_state='3',
                  Cold_In_state=None, Cold_Out_state=None)
    Pump(M, 'Pump', '3', '4',
         n_isen=params['eta_p'])
    HeatExchanger(M, 'Boiler', PPT=5, HEX_type='SimpleHEX',
                  HeatAdded=True,
                  Hot_In_state=None, Hot_Out_state=None,
                  Cold_In_state='4', Cold_Out_state='1')
    M.Solve(verbose=0)
    return M


base = {
    'T1': 753.15,
    'P1': 8e6,
    'P_cond': 0.008e6,
    'eta_t': 0.85,
    'eta_p': 0.85,
}

sa = SensitivityAnalyzer(build_rankine, base)

# Effect of turbine inlet temperature
df_T1 = sa.single_sweep(
    'T1', 
    np.arange(623, 873, 25),
    {'η (%)': lambda m: m.Efficiency},
    x_label='T_turbine_in [K]',
    save_to='T1_sweep.csv'
)

# Effect of condenser pressure
df_Pc = sa.single_sweep(
    'P_cond', 
    np.arange(0.004e6, 0.02e6, 0.002e6),
    {'η (%)': lambda m: m.Efficiency},
    x_label='P_condenser [Pa]',
    save_to='Pcond_sweep.csv'
)

# Two-parameter study
df_2D = sa.double_sweep(
    'T1', np.arange(623, 873, 50),
    'P1', np.arange(4e6, 14e6, 2e6),
    'Efficiency (%)', 
    lambda m: m.Efficiency,
    save_to='2D_sweep.csv'
)

#Multiple Outputs in single graph
sw_m = sa.multi_output_sweep(
     'T1', 
    np.arange(623, 873, 25),
    {'η (%)': lambda m: m.Efficiency,
     'Turbine Power (kW)': lambda m: m.Component['Turbine'].work / 1e3,
     'Net Power (kW)': lambda m: m.Net_power / 1e3},
    x_label='T_turbine_in [K]',
    save_to='T1_sweep.csv'
)
# Compute sensitivity
metrics_T1 = sa.compute_sensitivity_indices(df_T1, 'T1', 'η (%)')
metrics_Pc = sa.compute_sensitivity_indices(df_Pc, 'P_cond', 'η (%)')

print(f"\nT1 Sensitivity: {metrics_T1['sensitivity_index']:.4f}")
print(f"P_cond Sensitivity: {metrics_Pc['sensitivity_index']:.4f}")

Analyzing Saved Data (No Re-computation)

# Later, analyze without re-running simulations
sa_analysis = SensitivityAnalyzer()

# Load and plot
df = sa_analysis.analyze_csv(
    'T1_sweep.csv',
    param_name='T1',
    output_names=['η (%)']
)

# Compare baseline vs. modified design
from ThermoSim.analysis import compare_csv_files

compare_csv_files(
    'baseline_T1_sweep.csv',
    'modified_T1_sweep.csv',
    param_name='T1',
    output_names=['η (%)', 'Net Power (kW)'],
    labels=['Baseline', 'Modified']
)

Output Functions

The outputs dictionary maps display names to functions that extract values from a solved model:

# Cycle-level outputs
lambda m: m.Efficiency                          # thermal efficiency (%)
lambda m: m.Net_power                           # net power (W)
lambda m: m.Q_in                                # heat input (W)
lambda m: m.Q_out                               # heat rejected (W)
lambda m: m.Total_Ex_d                          # total exergy destruction (W)

# Component-level outputs
lambda m: m.Component['Turbine'].work           # turbine work (W)
lambda m: m.Component['Pump'].work              # pump work (W)
lambda m: m.Component['Turbine'].Ex_D           # turbine Ex_D (W)

# State-point outputs
lambda m: m.Point['2'].T - 273.15               # temperature at state 2 (°C)
lambda m: m.Point['2'].Q                        # quality at state 2
lambda m: m.Component['HEX1'].UA                # UA value of a HEX

Best Practices

1. Error Handling

The analyzer automatically catches errors during sweeps and marks failed points as NaN. Check your results:

df = sa.single_sweep(...)
print(f"Failed runs: {df['Efficiency (%)'].isna().sum()}")

2. Save Your Results

Always save parametric study results for later analysis:

df = sa.single_sweep(..., save_to='results.csv')

3. Large Sweeps

For computationally expensive models, start with coarse sweeps:

# Coarse sweep first
values_coarse = np.arange(600, 900, 100)  # 4 points
# Then refine interesting regions
values_fine = np.arange(700, 800, 10)     # 11 points

4. Batch Processing

For very large studies, consider running in batches:

# Batch 1
temps_batch1 = np.arange(623, 723, 25)
df1 = sa.single_sweep('T1', temps_batch1, outputs, save_to='batch1.csv')

# Batch 2
temps_batch2 = np.arange(723, 873, 25)
df2 = sa.single_sweep('T1', temps_batch2, outputs, save_to='batch2.csv')

# Combine later
import pandas as pd
df_combined = pd.concat([df1, df2]).sort_values('T1').reset_index(drop=True)

Troubleshooting

NaN Results

If you get NaN values in your results:

# Check which runs failed
failed_mask = df['Efficiency (%)'].isna()
print(df[failed_mask])

# Common causes:
# - Parameters outside valid range (e.g., P > critical pressure)
# - Non-physical states (e.g., superheated liquid)
# - Convergence failures in components

CSV Column Errors

When analyzing CSV files, ensure column names match:

# Check available columns
df = pd.read_csv('results.csv')
print(df.columns.tolist())

# Use exact column names
sa_csv.analyze_csv('results.csv', 
                   param_name='T1',  # must match CSV exactly
                   output_names=['Efficiency (%)'])

Memory Issues

For very large 2D sweeps:

# Instead of storing all in memory, process in chunks
n_chunks = 5
T1_chunks = np.array_split(np.arange(623, 873, 10), n_chunks)

for i, T1_vals in enumerate(T1_chunks):
    df_chunk = sa.double_sweep(
        'T1', T1_vals,
        'P1', P1_vals,
        'Efficiency (%)', lambda m: m.Efficiency,
        save_to=f'chunk_{i}.csv'
    )