Analysis

The openpytea.analysis module provides tools for understanding cost structure and how uncertain inputs affect financial outcomes:

  • Cost breakdowns — prepare equipment-level and plant-level CAPEX/OPEX data

  • One-way sensitivity — vary one parameter across a range and observe the metric

  • Tornado diagram — rank all parameters by their ±impact on a single metric

  • Monte Carlo simulation — propagate all uncertainties simultaneously

All analysis functions accept a configured and calculated Plant object. Visualization of the results is handled separately by Plotting.

To see the outputs of all code examples below, refer to the walkthrough notebook.

from openpytea.analysis import (
    direct_costs_data, fixed_capital_data,
    fixed_opex_data, variable_opex_data,
    sensitivity_data, tornado_data, monte_carlo,
)

CAPEX and OPEX breakdowns

Cost breakdowns are produced in two steps: the analysis functions prepare structured data, and the plotting functions render it. This separation lets you reuse the data in custom visualizations or export it directly.

The four data-preparation functions and their outputs:

Function

Output

direct_costs_data(plants)

Equipment-level purchased and direct costs.

fixed_capital_data(plants)

ISBL, OSBL, D&E, contingency (and optional additional CAPEX).

fixed_opex_data(plants)

Each fixed OPEX component (absolute or as % of total).

variable_opex_data(plants)

Each variable OPEX item.

Basic usage (single plant):

# Equipment-level CAPEX
direct_costs = direct_costs_data(plants=plant)

# Fixed capital breakdown (include additional CAPEX events)
fixed_capital = fixed_capital_data(plants=plant, additional_capex=True)

# Fixed OPEX as percentage of total
fixed_opex = fixed_opex_data(plants=plant, pct=True)

# Variable OPEX by item
variable_opex = variable_opex_data(plants=plant)

Pass the returned data to plot_stacked_bar() to visualize it — see Plotting for details.

Comparing multiple plants

Pass a list of Plant objects to compare two or more configurations side-by-side:

from copy import deepcopy

plant_b = deepcopy(plant)
plant_b.update_configuration({
    "plant_name": "Scenario B",
    "variable_opex_inputs": {
        "electricity": {"consumption": 0.9e6, "price": 0.05},
    },
})
plant_b.calculate_all()

variable_opex = variable_opex_data(plants=[plant, plant_b])
# pass to plot_stacked_bar() for a side-by-side chart

One-way sensitivity analysis

sensitivity_data() varies a single parameter over a symmetric range while holding everything else constant, then records the selected metric at each point.

# Default metric is LCOP; vary electricity price ±50 %
sens = sensitivity_data(plants=plant, parameter="electricity", plus_minus_value=0.5)

# Specify metric and label explicitly
npv_sens = sensitivity_data(
    plants=plant,
    parameter="methanol",       # product price
    metric="NPV",
    plus_minus_value=0.5,
    label="Project A — NPV [USD]",
)

parameter can be any of:

  • A key from variable_opex_inputs — varies that item’s price

  • A key from plant_products — varies that product’s price

  • "fixed_capital" — scales total installed CAPEX

  • "fixed_opex" — scales total fixed OPEX

  • "interest_rate" — discount rate

  • "project_lifetime" — project duration

  • "operator_hourly_rate" — labor wage

Supported metric values:

Value

Description

"LCOP"

Levelized cost of the primary product (default).

"NPV"

Net Present Value.

"IRR"

Internal Rate of Return.

"ROI"

Return on Investment.

"PBT"

Simple payback time in years.

For metrics that depend on revenue (NPV, ROI, IRR, PBT), product prices are included in the evaluation automatically.

Comparing multiple plants:

pbt_comparison = sensitivity_data(
    plants=[plant, plant_b],
    parameter="electricity",
    metric="PBT",
    plus_minus_value=0.5,
    additional_capex=True,   # account for mid-project CAPEX events
    n_points=50,
)

Pass the result to plot_sensitivity() — see Plotting.

Tornado diagram

A tornado diagram evaluates every variable-cost driver and financial parameter independently at ±``plus_minus_value``, then ranks them by impact on the chosen metric.

from openpytea.analysis import tornado_data

# Default metric is LCOP
td = tornado_data(plant=plant, plus_minus_value=0.5)

# Profit-oriented metric — product prices are included automatically
td_roi = tornado_data(plant=plant, plus_minus_value=0.5, metric="ROI")

Pass td to plot_tornado() — see Plotting.

Monte Carlo simulation

Monte Carlo assigns probability distributions to all uncertain inputs and evaluates the plant thousands or millions of times, producing a distribution of outcomes for each financial metric.

Configuring input uncertainties

Variable OPEX and product price uncertainties are defined inline in the existing variable_opex_inputs and plant_products configuration keys by adding std, min, and max fields to each item:

plant.update_configuration({
    "plant_products": {
        "methanol": {
            "production": 150_000,
            "price": 1.75,
            "std": 0.25,    # standard deviation
            "min": 1.25,    # lower truncation bound
            "max": 2.25,    # upper truncation bound
        },
    },
    "operator_hourly_rate": {
        "rate": 38.11,
        "std": 10.0,
        "min": 20.0,
        "max": 60.0,
    },
    "variable_opex_inputs": {
        "electricity": {
            "consumption": 1.4e6,
            "price": 0.10,
            "std": 0.035,
            "min": 0.025,
            "max": 0.175,
        },
        "natural_gas": {
            "consumption": 1.0e5,
            "price": 0.05,
            "std": 0.03,
            "min": 0.001,
            "max": 0.10,
        },
    },
})

Project-level financial uncertainties are set through the project_uncertainties key:

plant.update_configuration({
    "project_uncertainties": {
        "fixed_capital_factor": {"std": 0.30, "min": 0.25, "max": 1.75},
        "fixed_opex_factor":    {"std": 0.30, "min": 0.25, "max": 1.75},
        "project_lifetime":     {"std": 5},     # min/max auto-derived
        "interest_rate":        {"std": 0.03},  # min/max auto-derived
        "plant_utilization":    {"std": 0.05},  # opt-in; default std=0
        "tax_rate":             {"std": 0.10},  # opt-in; default std=0
    }
})

The first four keys are active by default. plant_utilization and tax_rate require an explicit std > 0 to be sampled. When min/ max are omitted they are derived as ±2 × std around the plant baseline. Set std=0 for any key to disable it.

Key

Description

Default std

fixed_capital_factor

Multiplicative factor on total installed CAPEX.

0.30 (30%)

fixed_opex_factor

Multiplicative factor on annual fixed OPEX.

0.30 (30%)

project_lifetime

Economic project life (years).

5 years

interest_rate

Discount / financing rate.

0.03 (3 pp)

plant_utilization

Yearly fraction of operating time.

0 (opt-in)

tax_rate

Corporate tax rate.

0 (opt-in)

Running the simulation

from openpytea.analysis import monte_carlo

mc_results = monte_carlo(
    plant,
    num_samples=1_000_000,   # increase for accuracy, decrease for speed
    batch_size=10_000,       # adjust to available memory
)

Results are stored on the Plant object and returned as a dict keyed by metric name. Each entry contains the raw sample array:

# Access results
print(mc_results["LCOP"])    # array of LCOP samples
print(mc_results["NPV"])     # array of NPV samples
# Available keys: "LCOP", "NPV", "IRR", "ROI", "PBT"

Visualizing results

Pass the plant (or mc_results) to the plotting functions:

from openpytea.plotting import plot_monte_carlo, plot_monte_carlo_inputs

# Distribution of the LCOP
plot_monte_carlo(plant, metric="LCOP", bins=30)

# Verify input distributions (useful for checking std/min/max settings)
plot_monte_carlo_inputs(mc_results, bins=40)

See Plotting for full plotting options.

Comparing multiple plants under uncertainty

from openpytea.plotting import plot_multiple_monte_carlo

mc_b = monte_carlo(plant_b, num_samples=1_000_000, batch_size=10_000)

plot_multiple_monte_carlo(
    data_list=[plant, plant_b],
    metric="LCOP",
    bins=30,
)

See also