Technoeconomic Analysis#
This document provides comprehensive documentation for the technoeconomic analysis (TEA) capabilities in Difflow, including capital costs, operating costs, utility costs, and profitability metrics.
Table of Contents#
Overview#
The Difflow economics module provides comprehensive technoeconomic analysis capabilities:
difflow/economics/
├── capital.py # Equipment cost correlations
├── utilities.py # Utility cost models
├── opex.py # Operating cost calculations
├── profitability.py # Financial metrics (NPV, IRR, MSP)
├── indices.py # Cost index escalation (CEPCI)
└── insulation.py # Pipe insulation cost and economic thickness
Key Features:
All functions are JAX-differentiable for optimization
CEPCI cost escalation for 2000-2026
Comprehensive equipment cost database
Industry-standard financial metrics
Capital Costs#
Location: difflow/economics/capital.py
Equipment Cost Correlations#
Each table entry is a CostParams in one of two published forms. Turton et al.
(Table A.1, 2001 dollars) use
and Towler & Sinnott (Table 7.2, 2010 dollars) and Seider et al. (2013 dollars) use
Here \(C\) is the purchased cost of carbon-steel, near-ambient-pressure equipment
in the entry’s base_year dollars, and \(S\) is its size. equipment_cost
evaluates whichever form the entry names and escalates by CEPCI. Both forms are
differentiable in \(S\). Every entry stores the size range its source fitted
(S_min..S_max, in S_units). For a concrete size outside that range,
equipment_cost (and so compressor_cost, vessel_cost, …) raises
difflow.economics.CostRangeWarning and still returns the extrapolated cost.
Sizes traced under jit/grad are not checked.
from typing import NamedTuple
from difflow.economics.capital import CostParams
class CostParams(NamedTuple):
a: float # Fixed cost ($), or K1
b: float # Scaling coefficient, or K2
n: float # Scaling exponent, or K3
S_min: float # Minimum valid size
S_max: float # Maximum valid size
S_units: str # Size units
base_year: int # Cost basis year
form: str = "power_law" # or "log_quadratic"
source: str = "" # citation
CostParams.base_cost(S) gives the cost in base-year dollars.
CostParams.exponent(S) gives the local scaling exponent \(d\ln C/d\ln S\).
Equipment Cost Databases#
The coefficients below are generated from difflow.economics.capital. Some
entries borrow the nearest published row, and their source says so. For
example, pfr_tube uses Turton’s jacketed non-agitated reactor and
centrifugal_multistage uses the single centrifugal-pump row.
packing_structured is Seider’s installed cost.
Turton’s floating-head exchanger has \(K_2 < 0\). Its cost therefore has a minimum
at \(S = 10^{-K_2/2K_3} \approx 21.6\) m², inside the 10–1000 m² range, and
falls as area grows below that point. The coefficients are kept as published.
An area optimizer working below about 22 m² will move toward the minimum, and
validate_cost_params flags this region through its negative local exponent.
Table |
Entry |
Form |
a/K1, b/K2, n/K3 |
Valid size |
Base year |
Source |
|---|---|---|---|---|---|---|
|
|
log_quadratic |
4.1052, 0.532, -0.0005 |
0.1–35 m³ |
2001 |
Turton et al. 5e Table A.1: jacketed agitated reactor |
|
|
log_quadratic |
4.1052, 0.532, -0.0005 |
0.1–35 m³ |
2001 |
Turton et al. 5e Table A.1: jacketed agitated reactor (no separate coil row) |
|
|
log_quadratic |
3.3496, 0.7235, 0.0025 |
5–45 m³ |
2001 |
Turton et al. 5e Table A.1: jacketed non-agitated reactor (proxy for a tubular reactor) |
|
|
log_quadratic |
4.5587, 0.2986, 0.002 |
1–15 m³ |
2001 |
Turton et al. 5e Table A.1: autoclave |
|
|
log_quadratic |
3.4974, 0.4485, 0.1074 |
0.3–520 m³ |
2001 |
Turton et al. 5e Table A.1: vertical process vessel |
|
|
log_quadratic |
3.5565, 0.3776, 0.0905 |
0.1–628 m³ |
2001 |
Turton et al. 5e Table A.1: horizontal process vessel |
|
|
log_quadratic |
4.8509, -0.3973, 0.1445 |
90–30000 m³ |
2001 |
Turton et al. 5e Table A.1: API fixed-roof tank |
|
|
log_quadratic |
3.4974, 0.4485, 0.1074 |
0.3–520 m³ |
2001 |
Turton et al. 5e Table A.1: vertical process vessel |
|
|
log_quadratic |
4.8306, -0.8509, 0.3187 |
10–1000 m² |
2001 |
Turton et al. 5e Table A.1: floating head |
|
|
log_quadratic |
4.3247, -0.303, 0.1634 |
10–1000 m² |
2001 |
Turton et al. 5e Table A.1: fixed tube sheet |
|
|
log_quadratic |
4.1884, -0.2503, 0.1974 |
10–1000 m² |
2001 |
Turton et al. 5e Table A.1: U-tube |
|
|
log_quadratic |
3.3444, 0.2745, -0.0472 |
1–10 m² |
2001 |
Turton et al. 5e Table A.1: double pipe |
|
|
log_quadratic |
4.6656, -0.1557, 0.1547 |
10–1000 m² |
2001 |
Turton et al. 5e Table A.1: flat plate |
|
|
log_quadratic |
4.0336, 0.2341, 0.0497 |
10–10000 m² |
2001 |
Turton et al. 5e Table A.1: air cooler |
|
|
log_quadratic |
4.6561, -0.2947, 0.2207 |
1–100 m² |
2001 |
Turton et al. 5e Table A.1: spiral plate |
|
|
power_law |
29000, 400, 0.9 |
10–500 m² |
2010 |
Towler & Sinnott 2e Table 7.2: U-tube kettle reboiler |
|
|
log_quadratic |
3.4974, 0.4485, 0.1074 |
0.3–520 m³ |
2001 |
Turton et al. 5e Table A.1: tower (tray and packed) |
|
|
log_quadratic |
3.4974, 0.4485, 0.1074 |
0.3–520 m³ |
2001 |
Turton et al. 5e Table A.1: tower (tray and packed) |
|
|
log_quadratic |
2.9949, 0.4465, 0.3961 |
0.07–12.3 m² (per tray) |
2001 |
Turton et al. 5e Table A.1: sieve trays |
|
|
log_quadratic |
3.3322, 0.4838, 0.3434 |
0.7–10.5 m² (per tray) |
2001 |
Turton et al. 5e Table A.1: valve trays |
|
|
log_quadratic |
2.4493, 0.9744, 0.0055 |
0.03–628 m³ |
2001 |
Turton et al. 5e Table A.1: loose packing |
|
|
power_law |
0, 10065, 1 |
0.1–100 m³ |
2013 |
Seider et al. 4e Sec. 16.5: corrugated-sheet stainless, $285/ft³ INSTALLED, very approximate |
|
|
log_quadratic |
3.3892, 0.0536, 0.1538 |
1–300 kW |
2001 |
Turton et al. 5e Table A.1: centrifugal pump |
|
|
log_quadratic |
3.3892, 0.0536, 0.1538 |
1–300 kW |
2001 |
Turton et al. 5e Table A.1: centrifugal pump (one row covers all centrifugal pumps) |
|
|
log_quadratic |
3.8696, 0.3161, 0.122 |
0.1–200 kW |
2001 |
Turton et al. 5e Table A.1: reciprocating pump |
|
|
log_quadratic |
3.4771, 0.135, 0.1438 |
1–100 kW |
2001 |
Turton et al. 5e Table A.1: positive displacement pump |
|
|
power_law |
580000, 20000, 0.6 |
75–30000 kW |
2010 |
Towler & Sinnott 2e Table 7.2: centrifugal compressor, driver power |
|
|
power_law |
260000, 2700, 0.75 |
93–16800 kW |
2010 |
Towler & Sinnott 2e Table 7.2: reciprocating compressor, driver power |
|
|
log_quadratic |
5.0355, -1.8002, 0.8253 |
18–950 kW |
2001 |
Turton et al. 5e Table A.1: rotary compressor, fluid power |
|
|
log_quadratic |
4.7116, 0.4479, 0.0004 |
0.04–60 m³ |
2001 |
Turton et al. 5e Table A.1: mixer/settler |
|
|
power_law |
57000, 480000, 0.7 |
0.26–0.49 m (bowl diameter) |
2010 |
Towler & Sinnott 2e Table 7.2: high-speed disk centrifuge |
|
|
log_quadratic |
4.8123, 0.2858, 0.042 |
0.9–300 m² |
2001 |
Turton et al. 5e Table A.1: disc and drum filter |
|
|
log_quadratic |
3.4974, 0.4485, 0.1074 |
0.3–520 m³ |
2001 |
Turton et al. 5e Table A.1: tower shell (column internals not included) |
|
|
log_quadratic |
5.0238, 0.3475, 0.0703 |
5–1000 m² |
2001 |
Turton et al. 5e Table A.1: evaporator, forced circulation (pumped) |
|
|
log_quadratic |
3.9119, 0.8627, -0.0088 |
50–500 m² |
2001 |
Turton et al. 5e Table A.1: evaporator, falling film |
|
|
log_quadratic |
5.0, 0.149, -0.0134 |
0.5–5 m² |
2001 |
Turton et al. 5e Table A.1: evaporator, agitated film (scraped wall) |
|
|
log_quadratic |
5.2366, -0.6572, 0.35 |
10–100 m² |
2001 |
Turton et al. 5e Table A.1: evaporator, short tube |
|
|
log_quadratic |
4.642, 0.3698, 0.0025 |
100–10000 m² |
2001 |
Turton et al. 5e Table A.1: evaporator, long tube |
|
|
log_quadratic |
4.2756, 0.352, 0.0714 |
0.5–80 m² |
2001 |
Turton et al. 5e Table A.1: plate and frame filter |
|
|
log_quadratic |
3.8187, 0.6235, 0.0176 |
0.6–235 m² |
2001 |
Turton et al. 5e Table A.1: leaf filter |
|
|
log_quadratic |
3.2107, 0.7597, 0.0027 |
15–200 m² |
2001 |
Turton et al. 5e Table A.1: cartridge filter |
|
|
log_quadratic |
4.2756, 0.352, 0.0714 |
0.5–80 m² |
2001 |
Turton et al. 5e Table A.1: gravity filter |
|
|
log_quadratic |
4.8123, 0.2858, 0.042 |
0.9–300 m² |
2001 |
Turton et al. 5e Table A.1: pan filter |
|
|
log_quadratic |
5.1055, 0.4999, 0.0001 |
0.9–115 m² |
2001 |
Turton et al. 5e Table A.1: table filter |
|
|
log_quadratic |
5.1055, 0.4999, 0.0001 |
0.9–115 m² |
2001 |
Turton et al. 5e Table A.1: tube filter |
|
|
log_quadratic |
3.5645, 1.1118, -0.0777 |
5–100 m² |
2001 |
Turton et al. 5e Table A.1: rotary dryer, gas fired (direct heat) |
|
|
log_quadratic |
4.5472, 0.2731, 0.134 |
0.5–50 m² |
2001 |
Turton et al. 5e Table A.1: drum dryer |
|
|
log_quadratic |
3.6951, 0.5442, -0.1248 |
1.8–20 m² |
2001 |
Turton et al. 5e Table A.1: tray dryer |
|
|
power_law |
410000, 2200, 0.7 |
400–4000 kg/h (water evaporated) |
2010 |
Towler & Sinnott 2e Table 7.2: spray dryer, evaporation rate |
|
|
power_law |
0, 41793.7, 0.56 |
9.07185–907.185 t/day (crystals) |
2013 |
Seider et al. 4e Table 16.32: forced-circulation continuous evaporative crystallizer |
|
|
power_law |
0, 34004, 0.63 |
9.07185–226.796 t/day (crystals) |
2013 |
Seider et al. 4e Table 16.32: draft-tube-baffled continuous evaporative crystallizer |
|
|
log_quadratic |
4.5097, 0.1731, 0.1344 |
1.5–30 m³ |
2001 |
Turton et al. 5e Table A.1: batch crystallizer |
|
|
power_law |
170000, 1500, 0.9 |
100–10000 L/s (circulating water) |
2010 |
Towler & Sinnott 2e Table 7.2: cooling tower and pumps, field assembly |
Outside the valid size range a correlation is an extrapolation.
validate_cost_params(params, size) flags that case. It also flags a local
exponent outside 0.3–1.2.
from difflow.economics.capital import reactor_cost, heat_exchanger_cost
cost = reactor_cost(volume=10.0, reactor_type='cstr_jacketed')
# ~$90,000 for a 10 m³ jacketed agitated reactor (escalated to the default target year)
cost = heat_exchanger_cost(area=50.0, hx_type='shell_tube_floating')
# ~$42,000 for a 50 m² floating-head exchanger
Pressure Adjustment (vessels):
Where:
\(P\): Design pressure (barg)
\(D\): Diameter
\(t\): Wall thickness
\(S\): Allowable stress
\(E\): Weld efficiency
from difflow.economics.capital import pressure_factor_vessel, vessel_cost
F_p = pressure_factor_vessel(pressure=20.0, diameter=2.0)
base_cost = vessel_cost(volume=5.0, vessel_type='pressure_vessel_vertical')
cost_adj = base_cost * F_p
Column shells and their internals are costed separately and added:
import math
from difflow.economics.capital import column_cost
# Column shell + internals: 2 m diameter x 20 m tall, 30 sieve trays
D, H, n_trays = 2.0, 20.0, 30
shell_volume = math.pi * D**2 / 4 * H
tray_area = math.pi * D**2 / 4
cost = (column_cost(volume=shell_volume, column_type='tray_column_shell')
+ n_trays * column_cost(volume=tray_area, column_type='sieve_tray'))
from difflow.economics.capital import pump_cost, compressor_cost
pump = pump_cost(power=10.0, pump_type='centrifugal_single')
comp = compressor_cost(power=500.0, compressor_type='centrifugal')
Dryers, crystallizers, filter presses and cooling towers (issue #451):
from difflow.economics import (cooling_tower_cost, crystallizer_cost,
dryer_cost, separator_cost)
dryer = dryer_cost(20.0, dryer_type='drum') # m² of drum area
spray = dryer_cost(1000.0, dryer_type='spray') # kg/h water evaporated
fc = crystallizer_cost(100.0, crystallizer_type='forced_circulation') # t crystals/day
batch = crystallizer_cost(10.0, crystallizer_type='batch') # m³
press = separator_cost(20.0, separator_type='filter_press') # m² filter area
tower = cooling_tower_cost(1000.0) # L/s circulating water
dryer_cost(size, dryer_type="rotary_direct", target_year):rotary_direct(gas fired, direct heat, 5–100 m²),drum(0.5–50 m²) andtray(1.8–20 m²) are Turton et al. 5e Table A.1 (2001, CEPCI 397);sprayis Towler & Sinnott 2e Table 7.2 (January 2010), \(C = 410{,}000 + 2{,}200\,S^{0.7}\) with \(S\) the water evaporation rate, 400–4000 kg/h.crystallizer_cost(size, crystallizer_type="forced_circulation", target_year):forced_circulationanddraft_tube_baffled(continuous evaporative) are Seider et al. 4e Table 16.32 (2013, CE 567), \(C = 39{,}575\,S^{0.56}\) (10–1000) and \(C = 31{,}980\,S^{0.63}\) (10–250) with \(S\) in short tons of crystals per day; difflow takes \(S\) in metric t/day and converts the coefficient.batchis Turton Table A.1, volume 1.5–30 m³.separator_cost(area, "filter_press"): Turton Table A.1 plate-and-frame filter, 0.5–80 m² of filter area. The same table givesfilter_leaf,filter_cartridge,filter_gravity,filter_pan,filter_tableandfilter_tube. Turton prints the same constants for plate-and-frame and gravity filters, for disc/drum and pan filters, and for table and tube filters.cooling_tower_cost(water_flow, tower_type="field_assembled", target_year): Towler & Sinnott 2e Table 7.2 “cooling tower & pumps”, field assembly (January 2010), \(C = 170{,}000 + 1{,}500\,S^{0.9}\) with \(S\) the circulating-water flow, 100–10,000 L/s. The cost includes the pumps.
All of these were read from secondary transcriptions of the textbook tables:
the openpytea 3.2.0 correlation database (openpytea/data/cost_correlations.csv,
https://pypi.org/project/openpytea/) and, for the Turton rows and the Seider
crystallizer exponents and ranges, ecoana 0.0.1 (https://pypi.org/project/ecoana/).
The Towler spray-dryer and cooling-tower rows have only the openpytea
transcription. Each function raises ValueError for an unknown type and
CostRangeWarning for a concrete size outside the valid range.
Installation Factors#
Equipment purchase cost must be multiplied by installation factors to get installed cost.
InstallationFactors is a dataclass whose fields are fractions of the
purchased cost; the installed cost is purchased * (1 + sum of fractions)
(total_factor).
from difflow.economics.capital import InstallationFactors
# Defaults shown; every field can be overridden.
InstallationFactors(
piping=0.35,
instrumentation=0.20,
electrical=0.12,
buildings=0.15,
yard_improvements=0.05,
service_facilities=0.15,
engineering=0.10,
construction=0.10,
contingency=0.15,
)
Typical Installation Factors by Equipment Type:
Equipment |
Bare Module Factor |
|---|---|
Heat exchangers |
3.0-3.5 |
Pumps |
3.0-4.0 |
Vessels |
3.5-4.5 |
Columns |
4.0-5.0 |
Reactors |
3.5-4.5 |
Compressors |
2.5-3.5 |
from difflow.economics.capital import installed_cost, installed_cost_detailed
# Quick calculation with typical factor
installed = installed_cost(purchased_cost=100000, lang_factor=3.5) # $350,000
# Detailed breakdown
detailed = installed_cost_detailed(
purchased_cost=100000,
factors=InstallationFactors(
piping=0.45,
instrumentation=0.20,
electrical=0.15,
buildings=0.10,
yard_improvements=0.05,
service_facilities=0.10,
)
)
Piping Cost per Metre#
For the economic-pipe-diameter problem (pipe capital against pumping energy),
pipe_cost_per_length(D, C_ref, D_ref, base_year, target_year) scales an
installed carbon-steel pipe cost per metre with the inside diameter as a
piecewise power law,
the exponents behind the classical optimum-diameter formulas (Genereaux, 1937,
Ind. Eng. Chem. 29, 385; Peters, Timmerhaus and West, Plant Design and
Economics for Chemical Engineers, 5e, Ch. 12, whose turbulent optimum
\(D_\mathrm{opt}\propto q^{0.45}\rho^{0.13}\) follows from \(n = 1.5\);
PIPE_COST_EXPONENTS). pipe_cost_nps(nps, schedule, C_ref=..., nps_ref=..., base_year=...) does the same for ASME B36.10M sizes and adds the schedule
by metal mass: the cost of a heavier wall at the same NPS is the reference
schedule’s cost times pipe_weight_per_length(nps, schedule) / pipe_weight_per_length(nps, schedule_ref) (B36.10M plain-end weight,
\(W = 0.0246615\,(OD-t)\,t\) kg/m with OD and t in mm). That treats all of the
installed cost as proportional to the steel; erection labour grows more slowly
with wall thickness, so heavy schedules come out somewhat high.
The absolute cost level is an input. C_ref is the installed \(/m of one
reference size in `base_year` dollars, from a quote, a company database or a
textbook table; difflow does not ship a \)/m figure because none could be
checked against its source. Only the size scaling and the CEPCI escalation
come from the library.
import jax, math
import difflow.economics as econ
from difflow import Pipe, PipeParams
def annual_cost(D, Q=0.02, L=1000.0):
capital = 0.20 * L * econ.pipe_cost_per_length(D, C_ref=150.0, D_ref=0.0525,
base_year=2019) # your quote
pipe = Pipe(PipeParams(L=L, D=D, rho=998.2, mu=1.002e-3))
dP = pipe.hydraulics_at_flow(Q)["dP"]
return capital + dP * Q / 0.7 / 1000.0 * 8000.0 * 0.10 # $0.10/kWh, 8000 h
jax.grad(annual_cost)(0.15) # d(annual cost)/dD: zero at the economic diameter
econ.pipe_cost_nps(4, "80", C_ref=150.0, nps_ref=2, base_year=2019, length=120.0)
When the model was written, the weights were checked against B36.10M (2-in Sch 40 5.44 kg/m, 4-in Sch 80 22.32 kg/m), along with the exponents, the escalation, and that the optimum of this cost scales as \(Q^{0.40\text{-}0.48}\).
Total Capital Investment#
Total Capital Investment (TCI) includes all costs to build a functioning plant.
from difflow.economics.capital import (
total_capital_investment,
total_capital_investment_detailed,
CapitalInvestment
)
Capital Investment Breakdown:
Direct Costs (DC):
├── Equipment Purchase (ISBL)
├── Installation
├── Piping
├── Instrumentation
├── Electrical
├── Buildings
├── Site Development
└── Auxiliary Facilities
Indirect Costs (IC):
├── Engineering & Supervision (10-15% DC)
├── Construction & Contractor's Fee (5-15% DC)
└── Contingency (10-20% DC)
Fixed Capital Investment (FCI) = DC + IC
Working Capital (WC) = 10-20% FCI
Total Capital Investment (TCI) = FCI + WC
# Quick estimate from equipment costs
equipment_costs = {
'reactor': 500000,
'column': 800000,
'heat_exchangers': 300000,
'pumps': 100000
}
tci = total_capital_investment(sum(equipment_costs.values()))
# Lang factor approach: FCI = 4.74 x equipment cost, TCI = FCI + 15% working capital
# Detailed breakdown
result = total_capital_investment_detailed(
equipment_costs=equipment_costs,
factors=InstallationFactors(), # piping, instrumentation, ... as fractions
working_capital_fraction=0.15
)
print(f"Direct costs: ${result.total_direct_costs:,.0f}")
print(f"Indirect costs: ${result.total_indirect_costs:,.0f}")
print(f"Fixed capital: ${result.fixed_capital_investment:,.0f}")
print(f"Working capital: ${result.working_capital:,.0f}")
print(f"Total capital: ${result.total_capital_investment:,.0f}")
Pipe Insulation#
Location: difflow/economics/insulation.py
insulation_installed_cost(D_o, thickness, cost_per_m3, cost_per_m2=0, cost_per_m=0, base_year=None, target_year=None) is the installed cost per metre
of pipe, linear in the insulated volume \(\pi t(D_o+t)\) with optional cladding-area
and per-metre terms, escalated by CEPCI when both years are given.
economic_insulation_thickness(...) minimises the annualised insulation cost
(capital recovery factor) plus the cost of the heat lost
(heat_transfer.insulated_pipe_heat_loss), and is differentiable in every input.
The unit cost is a required input with no default: no published coefficient
could be checked (see Economic insulation thickness).
Utility Costs#
Location: difflow/economics/utilities.py
Utility Prices#
from difflow.economics.utilities import UtilityPrices, DEFAULT_PRICES
# UtilityPrices is a dataclass; heat utilities are priced in $/GJ.
UtilityPrices(
steam_high_pressure=14.05, # $/GJ, 4.1 MPa
steam_medium_pressure=11.80, # $/GJ, 1.1 MPa
steam_low_pressure=9.50, # $/GJ, 0.34 MPa
cooling_water=0.35, # $/GJ
chilled_water=4.50, # $/GJ
refrigeration_moderate=8.00, # $/GJ, -20 C
refrigeration_low=13.50, # $/GJ, -50 C
cryogenic=35.00, # $/GJ, below -100 C
electricity=0.07, # $/kWh
natural_gas=4.50, # $/GJ
fuel_oil=6.00, # $/GJ
coal=2.50, # $/GJ
process_water=0.50, # $/m³
boiler_feed_water=2.50, # $/m³
wastewater_treatment=0.80, # $/m³
compressed_air=0.03, # $/Nm³
nitrogen=0.08, # $/Nm³
oxygen=0.12, # $/Nm³
)
# Regional presets: 'us_gulf_coast', 'us_midwest', 'europe_west',
# 'asia_pacific', 'china'
prices_eu = UtilityPrices.from_region('europe_west')
DEFAULT_PRICES is the US Gulf Coast instance. All cost functions return
rates in $/s (multiply by 3600 * hours_per_year for an annual figure).
Steam#
Steam is the primary heating utility in chemical processes.
Steam Properties:
Type |
Pressure |
Temperature |
Latent Heat |
|---|---|---|---|
LP |
150 psig (10 barg) |
185°C |
~2200 kJ/kg |
MP |
400 psig (28 barg) |
250°C |
~1800 kJ/kg |
HP |
600 psig (41 barg) |
280°C |
~1500 kJ/kg |
from difflow.economics.utilities import (
steam_cost_from_duty,
steam_flowrate_from_duty
)
# Calculate steam cost from heat duty
Q = 1e6 # 1 MW heating
cost_per_s = steam_cost_from_duty(Q, steam_level='low_pressure') # $/s
annual_cost = cost_per_s * 8000 * 3600 # $/year
# Calculate steam flowrate
steam_rate = steam_flowrate_from_duty(Q, steam_level='low_pressure') # kg/s
Equations:
with the steam price \(P_{steam}\) in \(/GJ and the duty \)Q$ in GJ/s.
Cooling Water#
from difflow.economics.utilities import (
cooling_water_cost,
cooling_water_flowrate
)
# Cooling water for 500 kW duty with 10°C rise
Q = 500000 # W
cost_per_s = cooling_water_cost(Q, delta_T=10.0) # $/s
annual_cost = cost_per_s * 8000 * 3600 # $/year
# Required flowrate
cw_rate = cooling_water_flowrate(Q, delta_T=10.0) # kg/s
Equations:
Assuming \(C_p = 4.18\) kJ/kg/K for water.
Electricity#
from difflow.economics.utilities import (
electricity_cost,
electricity_cost_per_second,
pump_electricity_cost,
compressor_electricity_cost
)
# Cost rate from power in kW ($/h)
cost_per_hour = electricity_cost(power=1000.0) # $70/h at $0.07/kWh
# Cost rate from continuous power in W ($/s)
cost_rate = electricity_cost_per_second(power=100000)
# Pump electricity (includes efficiency), $/s
pump_cost_rate = pump_electricity_cost(
flowrate=0.01, # m³/s
head=50.0, # m
efficiency=0.75,
)
# Compressor electricity (isentropic), $/s
comp_cost_rate = compressor_electricity_cost(
flowrate=10.0, # mol/s
pressure_ratio=3.0,
efficiency=0.80,
)
Pump Power:
Compressor Power (isentropic):
Refrigeration#
For sub-ambient cooling:
from difflow.economics.utilities import (
refrigeration_cost,
refrigeration_cost_continuous
)
# Cost depends on temperature level ($/s)
cost = refrigeration_cost(
100000, # 100 kW cooling
temperature_level=-20.0, # °C
)
# Smooth, differentiable version of the temperature dependence
cost_c = refrigeration_cost_continuous(100000, temperature=-20.0)
Refrigeration Cost Factor:
Lower temperatures require more compression work, increasing cost. The price per GJ of duty steps up with the temperature level:
Temperature level |
Price field |
Default ($/GJ) |
|---|---|---|
5°C and above |
|
4.50 |
-20°C to 5°C |
|
8.00 |
-50°C to -20°C |
|
13.50 |
below -50°C |
|
35.00 |
Combined Utility Costs#
from difflow.economics.utilities import (
utility_cost_from_heat_duties,
total_utility_cost,
UtilityConsumption,
)
# Total utility cost from the summed heating and cooling duties
heating = 500000 + 300000 # W, sum of the heaters
cooling = 400000 + 600000 # W, sum of the coolers (positive)
cost_per_s = utility_cost_from_heat_duties(
heating_duty=heating,
cooling_duty=cooling,
steam_level='medium_pressure',
prices=DEFAULT_PRICES,
)
print(f"Utilities: ${float(cost_per_s) * 8000 * 3600:,.0f}/year")
# Or describe all consumption in one container
consumption = UtilityConsumption(
heating_duty=heating, cooling_duty=cooling, electricity=200.0, # kW
)
total = total_utility_cost(consumption, DEFAULT_PRICES) # $/s
print(f"Total utilities: ${total * 8000 * 3600:,.0f}/year")
Operating Costs#
Location: difflow/economics/opex.py
Raw Materials#
from difflow.economics.opex import (
RawMaterial,
raw_material_cost,
total_raw_material_cost,
annual_raw_material_cost,
)
# RawMaterial is a dataclass:
# RawMaterial(name, price [$/kg], consumption_rate [kg/s], molecular_weight [g/mol])
methanol = RawMaterial('methanol', price=0.40, consumption_rate=0.3, molecular_weight=32.04)
# Define raw materials as name -> (flowrate [kg/s], price [$/kg])
materials = {
'methanol': (0.30, 0.40),
'oxygen': (0.15, 0.05),
'catalyst': (3e-5, 50.0),
}
rm_rate = total_raw_material_cost(materials) # $/s
total_rm_cost = annual_raw_material_cost(materials) # $/year (default schedule)
print(f"Annual raw material cost: ${total_rm_cost:,.0f}")
From Molar Quantities:
from difflow.economics.opex import raw_material_cost_molar
# Cost from molar flow rates
cost_per_s = raw_material_cost_molar(
molar_flowrate=100.0, # mol/s
price=0.40, # $/kg
molecular_weight=32.04, # g/mol
)
annual = cost_per_s * 8000 * 3600 # $/year
Labor#
from difflow.economics.opex import (
LaborRates,
DEFAULT_LABOR_RATES,
operating_labor_cost,
labor_cost_from_equipment
)
# LaborRates is a dataclass (defaults shown)
LaborRates(
operator_salary=65000.0, # $/year per operator
supervisor_salary=90000.0, # $/year per supervisor
overhead_factor=1.4, # benefits, taxes, etc.
supervisor_ratio=0.2, # supervisors per operator
)
Labor Estimation Methods:
Direct calculation:
labor_cost = operating_labor_cost(
n_operators_per_shift=6,
n_shifts=3, # 3 shifts/day; uses a 4.5x coverage factor for 24/7
rates=DEFAULT_LABOR_RATES,
)
From equipment count (correlation):
# Rough estimate: 1 operator per 2-4 major equipment items
labor_cost = labor_cost_from_equipment(
n_major_equipment=20,
process_complexity='medium', # 'simple', 'medium' or 'complex'
rates=DEFAULT_LABOR_RATES,
)
Overhead and Maintenance#
from difflow.economics.opex import (
OverheadFactors,
maintenance_cost,
insurance_taxes_cost,
plant_overhead_cost
)
# OverheadFactors is a dataclass of fractions (defaults shown):
# maintenance=0.04, property_taxes=0.02, insurance=0.01 (of FCI)
# supervision=0.25, laboratory=0.10, plant_overhead=0.60 (of operating labor)
# general_admin=0.05, distribution_selling=0.05, research_dev=0.03
# (of manufacturing cost)
OverheadFactors()
Typical Factors:
Item |
Basis |
Default |
|---|---|---|
Maintenance |
FCI |
4% |
Insurance |
FCI |
1% |
Property taxes |
FCI |
2% |
Supervision + laboratory + plant overhead |
operating labor |
95% |
FCI = 50e6 # $50M fixed capital
maintenance = maintenance_cost(FCI, OverheadFactors(maintenance=0.03)) # 3% of FCI = $1.5M
taxes_ins = insurance_taxes_cost(FCI) # (2% property tax + 1% insurance) of FCI
overhead = plant_overhead_cost(500000) # supervision + lab + overhead on $500k labor
Total Operating Cost#
from difflow.economics.opex import (
calculate_opex,
simple_opex,
OperatingCostBreakdown,
com_from_correlations
)
# Detailed OPEX calculation; maintenance, taxes/insurance and overhead
# are derived from the fixed capital and labor cost
opex = calculate_opex(
raw_material_cost=4e6, # $/year
utility_cost=2e6, # $/year
fixed_capital=FCI, # $
operating_labor=1e6, # $/year (omit to estimate from operators/shift)
)
print(f"Total manufacturing cost: ${opex.manufacturing_cost:,.0f}/year")
print(f"Total production cost: ${opex.total_production_cost:,.0f}/year")
# Quick estimate: variable costs + fractions of FCI
simple = simple_opex(raw_materials=4e6, utilities=2e6, fixed_capital=FCI)
print(f"Estimated OPEX: ${simple:,.0f}/year")
Cost of Manufacturing (COM) correlation:
Where:
\(FCI\): Fixed capital investment
\(C_{OL}\): Operating labor cost
\(C_{UT}\): Utility cost
\(C_{RM}\): Raw material cost
com = com_from_correlations(
fixed_capital=50e6,
utility_cost=2e6,
raw_material_cost=4e6,
n_operators_per_shift=4, # sets the operating labor term
)
Operating Schedule#
from difflow.economics.opex import (
OperatingSchedule, DEFAULT_SCHEDULE, HOURS_PER_YEAR, SECONDS_PER_YEAR
)
# OperatingSchedule is a dataclass (defaults shown)
OperatingSchedule(
hours_per_year=8000.0, # ~91% availability
shifts_per_day=3,
days_per_week=7.0,
weeks_per_year=50.0,
stream_factor=0.91,
)
# Constants: HOURS_PER_YEAR = 8000, SECONDS_PER_YEAR = 28_800_000
Profitability Analysis#
Location: difflow/economics/profitability.py
Financial Parameters#
from difflow.economics.profitability import FinancialParams
# FinancialParams is a dataclass (defaults shown)
FinancialParams(
discount_rate=0.10, # 10% WACC
tax_rate=0.21, # 21% corporate tax
depreciation_years=10, # MACRS 10-year
plant_life=20, # 20-year project life
construction_years=2, # 2-year construction
salvage_fraction=0.05, # 5% salvage value
working_capital_fraction=0.15, # 15% of FCI
inflation_rate=0.02, # 2% annual inflation
)
Time Value of Money#
from difflow.economics.profitability import (
present_value,
future_value,
discount_factor,
capital_recovery_factor,
present_value_factor
)
# Present value of future cash
PV = present_value(future_value=1e6, rate=0.10, years=10) # $385,543
# Future value of present cash
FV = future_value(present_value=1e6, rate=0.10, years=10) # $2,593,742
# Discount factor
df = discount_factor(rate=0.10, year=10) # 0.3855
# Capital recovery factor (annuity payment per $ of principal)
crf = capital_recovery_factor(rate=0.10, years=20) # 0.1175
# Present value factor (sum of discount factors)
pvf = present_value_factor(rate=0.10, years=20) # 8.514
Equations:
Depreciation#
from difflow.economics.profitability import MACRS_SCHEDULES
# Modified Accelerated Cost Recovery System (US tax code)
# (jnp arrays keyed by recovery period: 5, 7, 10 and 15 years)
print(MACRS_SCHEDULES[5]) # [0.2, 0.32, 0.192, 0.1152, 0.1152, 0.0576]
Annual Depreciation:
Tax Savings from Depreciation:
Where \(\tau\) is the tax rate.
Net Present Value (NPV)#
from difflow.economics.profitability import (
npv,
npv_with_construction
)
import jax.numpy as jnp
# Simple NPV: cash flows for years 1..N, investment at year 0
cash_flows = jnp.array([10e6, 12e6, 14e6, 14e6, 14e6])
npv_value = npv(cash_flows, discount_rate=0.10, initial_investment=50e6)
# NPV with construction period
npv_value = npv_with_construction(
operating_cash_flows=jnp.full(20, 10e6), # 20 operating years
capital_investment=50e6,
discount_rate=0.10,
construction_years=2,
)
Equation:
Decision Criteria:
NPV > 0: Accept project
NPV < 0: Reject project
NPV = 0: Indifferent (earns exactly the required return)
Internal Rate of Return (IRR)#
from difflow.economics.profitability import irr, irr_approx
# IRR calculation (Newton solve via optimistix)
cash_flows = jnp.array([10e6, 12e6, 14e6, 14e6, 14e6])
irr_value = irr(cash_flows, initial_investment=50e6)
# Quick approximation (linear interpolation between two trial rates)
irr_approx_value = irr_approx(jnp.full(20, 10e6), initial_investment=50e6)
Definition: IRR is the discount rate where NPV = 0:
Decision Criteria:
IRR > WACC: Accept project
IRR < WACC: Reject project
Typical IRR Targets:
Risk Level |
Target IRR |
|---|---|
Low (expansion) |
15-20% |
Medium (new product) |
20-30% |
High (new technology) |
30-50% |
Payback Period#
from difflow.economics.profitability import (
simple_payback,
discounted_payback
)
# Simple payback (no discounting)
payback = simple_payback(annual_cash_flow=10e6, initial_investment=50e6) # 5 years
# Discounted payback (accounts for time value)
d_payback = discounted_payback(
cash_flows=jnp.full(20, 10e6),
initial_investment=50e6,
discount_rate=0.10,
) # ~7.3 years
Equations:
Return on Investment (ROI)#
from difflow.economics.profitability import roi, average_roi
# Simple ROI
roi_value = roi(annual_profit=8e6, total_investment=50e6) # 16%
# Average ROI over project life
avg_roi = average_roi(
total_profit=160e6, # Sum over 20 years
total_investment=50e6,
years=20,
)
Equation:
Minimum Selling Price (MSP)#
MSP is the product price required to achieve a target financial return.
from difflow.economics.profitability import (
minimum_selling_price,
msp_with_target_roi,
msp_with_npv_zero
)
# Simple MSP (break-even)
msp = minimum_selling_price(
total_annual_cost=40e6, # $/year
annual_production=1e7 # kg/year
) # $4.00/kg
# MSP for target ROI
msp_roi = msp_with_target_roi(
total_annual_cost=40e6,
annual_production=1e7,
total_investment=50e6,
target_roi=0.20,
) # $5.00/kg
# MSP for NPV = 0 (most rigorous)
msp_npv = msp_with_npv_zero(
opex=35e6,
capex=50e6,
annual_production=1e7,
discount_rate=0.10,
plant_life=20,
)
Equation (NPV = 0):
where \(WC\) is the working capital (a fraction of CAPEX), recovered at the end of the plant life, and \(PVF\) is the present value factor of the annuity. This is a pre-tax breakeven price.
Cash Flow Analysis#
from difflow.economics.profitability import (
FinancialParams,
generate_cash_flows,
full_cash_flow_analysis,
CashFlowResult
)
# Generate year-by-year cash flows
params = FinancialParams(
discount_rate=0.10, tax_rate=0.21, depreciation_years=10, plant_life=20,
)
cash_flows = generate_cash_flows(
capital_investment=50e6,
annual_revenue=45e6,
annual_opex=30e6,
params=params,
)
# Full analysis (adds working capital recovery and salvage value)
result = full_cash_flow_analysis(
capital_investment=50e6,
annual_revenue=45e6,
annual_opex=30e6,
params=params,
)
print(f"NPV: ${result.npv:,.0f}")
print(f"IRR: {result.irr:.1%}")
print(f"Payback: {result.payback:.1f} years")
Cash Flow Components:
Revenue
- Operating Costs
- Depreciation
= Taxable Income
- Taxes (21%)
= Net Income
+ Depreciation (add back)
= Operating Cash Flow
Year 0: -Capital Investment
Years 1-n: Operating Cash Flow
Year n: + Working Capital + Salvage Value
Sensitivity Analysis#
from difflow.economics.profitability import npv_sensitivity
# NPV sensitivity to parameter changes
sensitivities = npv_sensitivity(
base_case={'capital': 50e6, 'revenue': 45e6, 'opex': 30e6, 'discount_rate': 0.10},
parameter_ranges={
'capital': (40e6, 60e6), # +/- 20%
'revenue': (38e6, 52e6), # +/- 15%
'opex': (27e6, 33e6), # +/- 10%
},
n_points=10,
)
# Identify most critical parameters
for param, (values, npvs) in sensitivities.items():
print(f"{param}: NPV ranges from ${npvs.min()/1e6:.1f}M to ${npvs.max()/1e6:.1f}M")
Cost Indices#
Location: difflow/economics/indices.py
CEPCI (Chemical Engineering Plant Cost Index)#
The CEPCI allows escalation of historical equipment costs to current year.
from difflow.economics.indices import (
get_cepci,
escalate_cost,
CEPCI_HISTORICAL
)
# CEPCI_HISTORICAL maps year -> CEPCIData(year, index, ...), 2000-2026
# (2024-2026 are estimates). Selected overall index values:
# 2000: 394.1, 2010: 550.8, 2019: 607.5, 2020: 596.2,
# 2021: 708.0, 2022: 816.0, 2023: 797.9
# Get CEPCI for specific year
cepci_2019 = get_cepci(2019) # 607.5
cepci_2023 = get_cepci(2023) # 797.9
Cost Escalation:
# Escalate 2019 cost to 2023
cost_2019 = 1e6
cost_2023 = escalate_cost(cost_2019, base_year=2019, target_year=2023)
# $1,000,000 × (797.9/607.5) = $1,313,415
Equipment-Specific Escalation#
Different equipment types may have different cost trends:
from difflow.economics.indices import CEPCI_RATIOS
# Pre-computed ratios for common escalations
ratio_2019_to_2024 = CEPCI_RATIOS[(2019, 2024)]
# Apply to equipment cost
old_cost = 1e6
current_cost = old_cost * ratio_2019_to_2024
Inflation Factors#
For general inflation (not CEPCI):
from difflow.economics.indices import (
inflation_factor,
inflation_factor_continuous
)
# Discrete compounding
factor = inflation_factor(base_year=2020, target_year=2025, annual_rate=0.02) # 1.104
# Differentiable (array-valued) form
factor_cont = inflation_factor_continuous(years=5.0, annual_rate=0.02) # 1.104
Examples#
Complete TEA for a Methanol Plant#
import jax.numpy as jnp
from difflow.economics.capital import (
reactor_cost, heat_exchanger_cost, column_cost, compressor_cost,
total_capital_investment
)
from difflow.economics.utilities import utility_cost_from_heat_duties
from difflow.economics.opex import calculate_opex, raw_material_cost_molar
from difflow.economics.profitability import (
FinancialParams, full_cash_flow_analysis, msp_with_npv_zero
)
# Plant specifications
production = 100000 # tonnes/year methanol
hours_per_year = 8000
# Equipment costs
equipment = {
'reactor': reactor_cost(50.0, 'cstr_jacketed'),
'distillation': column_cost(3.14 * 1.5**2 * 30.0, 'tray_column_shell'), # 3 m dia x 30 m
'heat_exchangers': 3 * heat_exchanger_cost(200.0, 'shell_tube_floating'),
'compressor': compressor_cost(2000.0, 'centrifugal'),
}
total_equipment = sum(equipment.values())
# Capital investment
TCI = total_capital_investment(total_equipment)
FCI = TCI / 1.15 # fixed capital: TCI less the 15% working capital
# Utilities (heating and cooling duties summed, W)
heating = 8e6 + 2e6 # reboiler + feed heater
cooling = 5e6 + 6e6 # reactor cooling + condenser
utility_rate = utility_cost_from_heat_duties(heating, cooling) # $/s
utilities = float(utility_rate) * hours_per_year * 3600 # $/year
# Raw materials
syngas_rate = raw_material_cost_molar(
molar_flowrate=production * 1e6 / 32.04 / (hours_per_year * 3600) * 3, # mol/s, 3 mol syngas per mol MeOH
price=0.15, # $/kg
molecular_weight=10.0, # approximate syngas MW, g/mol
)
syngas_cost = float(syngas_rate) * hours_per_year * 3600 # $/year
# Total OPEX (maintenance, taxes, insurance and overhead follow from FCI and labor)
labor = 1.5e6 # $1.5M/year
opex = calculate_opex(
raw_material_cost=syngas_cost,
utility_cost=utilities,
fixed_capital=float(FCI),
operating_labor=labor,
)
# Revenue (at $400/tonne methanol)
revenue = production * 400
# Profitability analysis
result = full_cash_flow_analysis(
capital_investment=float(FCI),
annual_revenue=revenue,
annual_opex=opex.total_production_cost,
params=FinancialParams(discount_rate=0.10, tax_rate=0.21, plant_life=20),
)
print(f"\n=== Methanol Plant TEA ===")
print(f"Production: {production:,} tonnes/year")
print(f"Total Capital Investment: ${TCI/1e6:.1f} M")
print(f"Fixed Capital: ${FCI/1e6:.1f} M")
print(f"Annual OPEX: ${opex.total_production_cost/1e6:.1f} M")
print(f"Annual Revenue: ${revenue/1e6:.1f} M")
print(f"\nProfitability Metrics:")
print(f" NPV (10%): ${result.npv/1e6:.1f} M")
print(f" IRR: {result.irr:.1%}")
print(f" Payback: {result.payback:.1f} years")
# Minimum selling price
msp = msp_with_npv_zero(
opex=opex.total_production_cost,
capex=FCI,
annual_production=production * 1000, # kg/year
discount_rate=0.10,
plant_life=20,
)
print(f" MSP: ${msp:.2f}/kg = ${msp*1000:.0f}/tonne")
Optimization with Gradient-Based Methods#
All TEA functions are JAX-differentiable, enabling gradient-based optimization:
import jax
import jax.numpy as jnp
from difflow.economics.capital import reactor_cost, column_cost, total_capital_investment
from difflow.economics.profitability import npv_objective
def plant_npv(design_params):
"""NPV as function of design parameters."""
reactor_volume, column_volume = design_params
# Equipment costs depend on design
equip_cost = reactor_cost(reactor_volume, 'cstr_jacketed')
equip_cost += column_cost(column_volume, 'tray_column_shell')
FCI = total_capital_investment(equip_cost) / 1.15
# Operating costs depend on design (simplified)
OPEX = 0.15 * FCI # Rough correlation
# Revenue depends on conversion (which depends on reactor size)
conversion = 1 - jnp.exp(-0.1 * reactor_volume) # Simplified kinetics
revenue = conversion * 1e8 # Base revenue
# npv_objective returns the NEGATIVE NPV (for minimization)
return -npv_objective(revenue, OPEX, FCI, discount_rate=0.10, plant_life=20)
# Gradient of NPV w.r.t. design parameters
grad_npv = jax.grad(plant_npv)
# Optimization loop
design = jnp.array([10.0, 20.0]) # Initial guess
step = 0.5 # step length in design units
for i in range(100):
grad = grad_npv(design)
# Normalized gradient ascent (maximize NPV); NPV is in $, so scale the step
design = design + step * grad / (jnp.linalg.norm(grad) + 1e-12)
design = jnp.maximum(design, 0.5) # stay inside the correlation range
if i % 20 == 0:
print(f"Iteration {i}: NPV = ${plant_npv(design)/1e6:.2f}M")
Summary Tables#
Typical Equipment Costs (purchased, default target year)#
Equipment |
Size |
Approximate Cost |
|---|---|---|
CSTR (jacketed, agitated) |
10 m³ |
$90,000 |
PFR (jacketed vessel proxy) |
5 m³ |
$15,000 |
Shell-tube HX (floating head) |
50 m² |
$42,000 |
Distillation column, 30 sieve trays |
2 m dia × 20 m |
$220,000 |
Centrifugal pump |
10 kW |
$8,000 |
Centrifugal compressor |
500 kW driver |
$2,100,000 |
Utility Cost Summary (2019 basis)#
Utility |
Unit Cost |
Typical Usage |
|---|---|---|
LP Steam |
$0.015/kg |
Heating to 185°C |
HP Steam |
$0.030/kg |
Heating to 280°C |
Cooling Water |
$0.05/m³ |
Cooling above 30°C |
Electricity |
$0.07/kWh |
Pumps, compressors |
Refrigeration (-20°C) |
2× CW cost |
Sub-ambient cooling |
Financial Parameters Summary#
Parameter |
Typical Value |
|---|---|
Discount rate (WACC) |
8-12% |
Corporate tax rate |
21% (US) |
MACRS depreciation |
7-10 years |
Plant life |
15-25 years |
Construction period |
2-3 years |
Working capital |
10-20% of FCI |
Contingency |
10-20% of DC |