Skip to content

Latest commit

 

History

History
397 lines (309 loc) · 10.2 KB

File metadata and controls

397 lines (309 loc) · 10.2 KB

Uncertainty Analysis Tutorial

Learn how to quantify and analyze uncertainty in OTEC techno-economic assessments using Monte Carlo simulations and sensitivity analysis.

Why Uncertainty Analysis?

OTEC cost estimates involve significant uncertainty in:

  • Thermodynamic parameters: Heat transfer coefficients, efficiencies
  • Economic factors: CAPEX, OPEX, discount rates
  • Operating conditions: Temperature variations, availability

Uncertainty analysis helps you:

  • Quantify confidence intervals for LCOE estimates
  • Identify which parameters matter most
  • Make robust investment decisions
  • Communicate results with appropriate caveats

Available Methods

Method Purpose Speed Requires SALib
Monte Carlo Full uncertainty propagation Slow No
Tornado Quick sensitivity screening Fast No
Sobol Global sensitivity indices Medium Yes

Monte Carlo Analysis

Basic Usage

from otex.analysis import MonteCarloAnalysis, UncertaintyConfig

# Configure the analysis
config = UncertaintyConfig(
    n_samples=1000,    # Number of samples (more = better accuracy)
    seed=42,           # Random seed for reproducibility
    parallel=True      # Use multiple CPU cores
)

# Create and run analysis
mc = MonteCarloAnalysis(
    T_WW=28.0,         # Warm water temperature (°C)
    T_CW=5.0,          # Cold water temperature (°C)
    config=config,
    p_gross=-50000,    # 50 MW plant
    cost_level='low_cost'
)

results = mc.run(show_progress=True)

Interpreting Results

# Get comprehensive statistics
stats = results.compute_statistics()

# LCOE statistics
lcoe = stats['lcoe']
print(f"LCOE Statistics")
print(f"{'='*40}")
print(f"Mean:     {lcoe['lcoe_mean']:.2f} ct/kWh")
print(f"Std Dev:  {lcoe['lcoe_std']:.2f} ct/kWh")
print(f"CV:       {lcoe['lcoe_cv']:.1%}")
print(f"Median:   {lcoe['lcoe_median']:.2f} ct/kWh")
print(f"Min:      {lcoe['lcoe_min']:.2f} ct/kWh")
print(f"Max:      {lcoe['lcoe_max']:.2f} ct/kWh")
print(f"P5:       {lcoe['lcoe_p5']:.2f} ct/kWh")
print(f"P95:      {lcoe['lcoe_p95']:.2f} ct/kWh")

# Confidence interval
low, high = results.get_confidence_interval('lcoe', confidence=0.90)
print(f"\n90% Confidence Interval: [{low:.2f}, {high:.2f}] ct/kWh")

Latin Hypercube Sampling

OTEX uses Latin Hypercube Sampling (LHS) for efficient coverage of the parameter space:

# Access the samples
samples = mc.samples
print(f"Sample shape: {samples.shape}")  # (n_samples, n_parameters)

# View parameter names
print(f"Parameters: {config.parameter_names}")

LHS ensures better coverage than simple random sampling, especially for small sample sizes.

Tornado Analysis

Tornado diagrams show which parameters have the largest impact on results:

from otex.analysis import TornadoAnalysis

tornado = TornadoAnalysis(
    T_WW=28.0,
    T_CW=5.0,
    variation_pct=10.0,   # ±10% variation (if not using bounds)
    p_gross=-50000,
    cost_level='low_cost'
)

# Run analysis
tornado_results = tornado.run(
    output='lcoe',        # Analyze LCOE
    use_bounds=True,      # Use parameter bounds, not percentage
    show_progress=True
)

# View results
print(f"Baseline LCOE: {tornado_results.baseline:.2f} ct/kWh")
print(f"\nParameter Rankings (by swing magnitude):")
for i, (name, swing) in enumerate(tornado_results.get_ranking(), 1):
    print(f"{i:2d}. {name:40s} {swing:+.2f} ct/kWh")

Interpreting Tornado Results

  • Swing: Total change from low to high parameter value
  • Positive swing: Higher parameter value → higher LCOE
  • Negative swing: Higher parameter value → lower LCOE
  • Large swings: Focus on reducing uncertainty in these parameters

Sobol Sensitivity Analysis

Sobol analysis provides rigorous, global sensitivity indices:

from otex.analysis import SobolAnalysis

sobol = SobolAnalysis(
    T_WW=28.0,
    T_CW=5.0,
    n_samples=512,           # Base samples (total = n*(2d+2))
    calc_second_order=False, # Set True for interaction effects
    p_gross=-50000,
    cost_level='low_cost'
)

# Run analysis (requires SALib)
sobol_results = sobol.run(output='lcoe', show_progress=True)

# View results
print("Sobol Sensitivity Indices")
print("="*60)
print(f"{'Parameter':40s} {'S1':>8s} {'ST':>8s}")
print("-"*60)
for name, st in sobol_results.get_ranking('ST'):
    idx = sobol_results.parameter_names.index(name)
    s1 = sobol_results.S1[idx]
    print(f"{name:40s} {s1:8.3f} {st:8.3f}")

Understanding Sobol Indices

Index Name Interpretation
S1 First-order Direct effect of parameter
ST Total-order Total effect including interactions
ST - S1 Interactions Effect through parameter interactions
  • S1 close to ST: Parameter acts independently
  • ST >> S1: Strong interactions with other parameters
  • Sum of S1 ≈ 1: Additive model (no interactions)
  • Sum of ST > 1: Significant parameter interactions

Visualization

Histogram with Statistics

from otex.analysis import plot_histogram
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 6))
plot_histogram(results, output='lcoe', ax=ax, bins=50)
plt.savefig('lcoe_histogram.png', dpi=150)
plt.show()

Tornado Diagram

from otex.analysis import plot_tornado

fig, ax = plt.subplots(figsize=(12, 8))
plot_tornado(tornado_results, ax=ax, top_n=10)
plt.savefig('tornado_diagram.png', dpi=150)
plt.show()

Sobol Indices Bar Chart

from otex.analysis import plot_sobol_indices

fig, ax = plt.subplots(figsize=(10, 8))
plot_sobol_indices(sobol_results, ax=ax, top_n=10)
plt.savefig('sobol_indices.png', dpi=150)
plt.show()

Summary Figure

from otex.analysis import create_summary_figure

fig = create_summary_figure(
    mc_results=results,
    tornado_results=tornado_results,
    sobol_results=sobol_results,  # Optional
    output='lcoe'
)
fig.savefig('uncertainty_summary.png', dpi=150)

Scatter Matrix

from otex.analysis import plot_scatter_matrix

fig = plot_scatter_matrix(
    results,
    output='lcoe',
    max_params=5  # Show top 5 correlated parameters
)
fig.savefig('scatter_matrix.png', dpi=150)

Customizing Parameters

Default Uncertain Parameters

from otex.analysis import get_default_parameters

params = get_default_parameters()
for p in params:
    print(f"{p.name}: {p.nominal} ({p.distribution}, {p.bounds})")

Default parameters include:

  • Turbine isentropic efficiency
  • Pump isentropic efficiency
  • Heat transfer coefficients (U_evap, U_cond)
  • CAPEX factors (turbine, HX, pump, structure)
  • OPEX factor
  • Discount rate

Custom Parameters

from otex.analysis import UncertainParameter, UncertaintyConfig

# Define custom parameters
custom_params = [
    UncertainParameter(
        name='discount_rate',
        nominal=0.08,
        distribution='uniform',
        bounds=(0.05, 0.12),
        category='economic'
    ),
    UncertainParameter(
        name='turbine_isentropic_efficiency',
        nominal=0.85,
        distribution='normal',
        bounds=(0.85, 0.03),  # mean, std for normal
        category='efficiency'
    ),
    UncertainParameter(
        name='capex_structure_factor',
        nominal=1.0,
        distribution='triangular',
        bounds=(0.8, 1.8),  # min, max (mode = nominal)
        category='economic'
    ),
]

config = UncertaintyConfig(
    parameters=custom_params,
    n_samples=500,
    seed=42
)

Distribution Types

Type bounds meaning Example
uniform (min, max) (0.8, 1.2)
normal (mean, std) (0.82, 0.04)
triangular (min, max), mode=nominal (0.7, 1.3)

Command Line Interface

After installing OTEX via pip, the otex-uncertainty command is available:

# Tornado analysis (fast)
otex-uncertainty \
    --T_WW 28 --T_CW 5 \
    --method tornado

# Monte Carlo (comprehensive)
otex-uncertainty \
    --T_WW 28 --T_CW 5 \
    --method monte-carlo \
    --samples 1000

# Sobol analysis (requires SALib: pip install otex[uncertainty])
otex-uncertainty \
    --T_WW 28 --T_CW 5 \
    --method sobol \
    --samples 512

# All methods with saved plots
otex-uncertainty \
    --T_WW 28 --T_CW 5 \
    --method all \
    --samples 500 \
    --save-plots \
    --output-dir ./results/

Best Practices

Sample Size Guidelines

Method Minimum Recommended High Accuracy
Monte Carlo 100 1,000 10,000
Tornado 2*n_params 2*n_params 2*n_params
Sobol 64 512 2,048

Convergence Check

# Run with increasing sample sizes
sample_sizes = [100, 500, 1000, 2000]
means = []

for n in sample_sizes:
    config = UncertaintyConfig(n_samples=n, seed=42)
    mc = MonteCarloAnalysis(T_WW=28.0, T_CW=5.0, config=config)
    results = mc.run(show_progress=False)
    means.append(results.compute_statistics()['lcoe']['lcoe_mean'])

# Plot convergence
plt.plot(sample_sizes, means, 'o-')
plt.xlabel('Number of samples')
plt.ylabel('Mean LCOE (ct/kWh)')
plt.title('Convergence Check')

Recommended Workflow

  1. Start with Tornado: Quick identification of important parameters
  2. Run Monte Carlo: Full uncertainty propagation (1000+ samples)
  3. Optional Sobol: If you need rigorous sensitivity indices
  4. Visualize and report: Create summary figures

Exporting Results

import pandas as pd

# Export Monte Carlo samples
df = pd.DataFrame(results.samples, columns=results.parameter_names)
df['lcoe'] = results.lcoe
df['net_power'] = results.net_power
df['capex'] = results.capex
df.to_csv('monte_carlo_results.csv', index=False)

# Export statistics
stats = results.compute_statistics()
stats_df = pd.DataFrame(stats).T
stats_df.to_csv('uncertainty_statistics.csv')

# Export tornado results
tornado_df = pd.DataFrame({
    'parameter': tornado_results.parameter_names,
    'low_value': tornado_results.low_values,
    'high_value': tornado_results.high_values,
    'swing': tornado_results.swings
})
tornado_df.to_csv('tornado_results.csv', index=False)

Next Steps