TEA¶
cheme assess tea evaluates the economics of a plant. Its purpose is a decision, not a
precise number: it states the question the analysis supports and builds the capital cost from
the plant's own estimate. It prices what the plant buys and sells and computes the discounted
cash flow. It then shows where the money goes, which premises dominate the outcome and at which
value of each one the decision would turn. Its four principles:
- comparable: same product specification, boundary, scale, location and time basis;
- traceable: every input with source, year, unit and status;
- uncertainty-aware: ranges and drivers, not one number;
- decision-led: detail in proportion to the stage and its decision.
It covers a wide range of plants:
- continuous and batch, in one or several operating cases;
- material and energy main products (LCOP, cost of production, LCOE/LCOH);
- products, co-products, components of a flow, consumables the flowsheet does not carry, emissions with a carbon price and free allocation;
- replacements, turnarounds and grants as periodic events;
- prices that move over the plant's life.
Where the code and its companions live:
- Code:
domains/assessment/tea/: schemas.py(estimate/tea.yaml);build.py(premises to lines, findings);model.py(the cash flow);uncertainty.py(Monte Carlo, tornado);report.py(results, formats, comparison);gates.py.
The flowsheet's feed and product rates are in domains/assessment/flows.py, shared with the
LCA.
- Template notes: templates/project/estimate/README.md and STAGES.md.
- Skill: tea. It has a complete example.
Inputs¶
| file | kind | what |
|---|---|---|
estimate/tea.yaml |
tea |
the premises, see below |
process/flowsheet.yaml (materials.yaml, reactions.yaml, catalysts.yaml) |
process-flowsheet |
the feeds and products in each case. A material terminal counts in kg/h with its components, an energy terminal (kind: energy streams with duty) in kWh/h |
plant.yaml |
plant |
the stage, which sets the estimate class expected |
estimate/cost-basis.yaml, the equipment list and datasheets |
cost_basis, ... |
isbl.source: equipment: the bare module cost of cheme assess equipment |
estimate/rates.yaml, layout/plant-model.yaml |
rates |
isbl.source: mto: the direct and indirect cost of cheme assess estimate (its contingency is left out, so it is not counted twice) |
lca/lca.yaml |
lca |
operation when tea.yaml gives none (a finding says so; a different one is tea-basis-mismatch); the flows it maps as emissions; allocation.prices (and allocation.properties.<p>.price) checked against the products' prices here: the same plant at other price ratios gives the revenue and the economic allocation different premises (tea-lca-price-mismatch) |
estimate/tea.yaml¶
cheme schema tea prints the JSON Schema; the skill tea has a complete example, marked
example: true (its figures show the shape only). cheme init writes no tea.yaml: the
project writes it when it prices its plant. Every value is a Value: value, status
(ASSUMED or QUOTED), source, year, and optionally uncertainty and swing: {low, high}.
The uncertainty is as in the LCA: lognormal gsd2 or pedigree, normal cv, triangular or
uniform low/high multipliers.
Decision and basis¶
currency,cost_year: every cost is in one currency and year, in real terms.example: truemarks example numbers.decision:question;metric:npvandirrmust be at least the threshold,lcopandcash_costat most;threshold: npv in currency, default 0; irr a fraction, default the discount rate; lcop and cash_cost per the basis unit, required;confidence: the probability of meeting the threshold the decision needs;alternatives.basis:product: the flowsheet product whose levelised cost is computed;unit: what its cost is per. The default is its price's unit, else t for a material and MWh for an energy product. Giveconversionfor a unit of another kind;specification,boundary,scale,location.
Operation¶
case, orcases:[{case, share, prices, source}]. Each case's flows count by their share of the operating hours, and the shares sum to 1. A flow priced differently in a case becomes one line per case,<key>~<case>. These share one rate input and one price input, so the price driver stays one.operation:hours_per_year,life_years;ramp_up: capacity fractions of the first years;utilisation: the load after the ramp-up;source.operation.batch:{cycle_time, basis_hours, trains, batches_per_year, success_rate, source}. The flowsheet's rates are those of one batch overbasis_hours. The operating hours are batches timesbasis_hours, with batches =batches_per_year, else hours x trains / cycle time.success_ratescales the products sold.
Capital¶
capex.isbl:source:equipment,mto, orgivenwithvalue;scaling:{reference, unit, exponent}. For a given value at another capacity: ISBL x (capacity / reference) ** exponent, where the capacity is the design capacity: the main product per year in its largest case, at full load;- conversions to the file's basis:
exchange_rate,escalation,location_factor. The project's own costs already stated in the TEA's currency and year (quotations for this plant) are neither converted nor moved by the location factor; purchased_installation: installed / purchased, for items with a purchased cost only;uncovered_allowance: a fraction of the costed ISBL for what the estimate leaves uncosted (equipment or mto): items no correlation covers, take-off rows without a rate;- a catalyst's first fill is a line of its own, of its price's year:
escalation(from the cost index's year) applies to it only when that is the index's year, and a fill of a third year is reported (tea-cost-year). A fill priced in another currency than the estimate's counts as it is when that is the TEA's currency and cost year, else it is left out (tea-capex-own-cost); - optionally
uncertainty,swingandestimate_class. osbl(x ISBL),owners_cost(x (ISBL + OSBL)),contingency(x (ISBL + OSBL + owner's cost)). Their sum with the ISBL is the fixed capital.
Lines: what the plant buys and sells¶
prices:{key: feed:<id> | product:<id>, component?, value, unit, conversion?, category?, profile? | escalation?, status, source, year, uncertainty, swing}.unitis one of kg, t, lb, st, kWh, MWh, MJ, GJ, MMBtu, therm, Nm3, m3, L, gal, bbl, kmol, ea.- A unit of the flow's kind converts by itself. Any other unit needs
conversion: units per kg or kWh of the flow, with its source, e.g. 0.0455 MMBtu/kg (LHV) for a gas, or Nm3/kg. componentprices only that component's mass,<key>@<component>, for example H2 in an off-gas.- A product with a disposal cost has a negative price.
free:{key, reason}for flows that carry no price (never the main product).- A product with a negative price is carried as a cost line: a material one in category disposal, an energy one (rejected heat) in category utility.
variable_opex:[{id, per, component?, amount, unit, value, category, profile? | escalation?, ...}]. These are consumables, utilities, fees and incentives the flowsheet does not carry, each a linevariable:<id>:peris a flow (amountper kg or kWh of it, or of itscomponent),hour, orbatch;valueis the price perunit;- a negative price is a credit (category
incentivecounts as a subsidy). emissions:{price, profile? | escalation?, sources, allocation?}.- Each source
{id, key, component?, factor, unit, conversion?, captured?}charges t CO2-eq perunitof the flow or component, less what is captured (emission:<id>). allocation: {benchmark, per, unit, fraction, profile?}credits the free allowances (allowance:carbon).- Every emission line takes
price:carbon. - Categories: feed, utility, chemical, catalyst, disposal, emission, incentive, byproduct, other. The default follows the flow: a material feed is a feed, an energy feed a utility, a product a co-product credit, or disposal when its price is negative. They group the cost of production.
- Price paths:
profilegives multipliers by operating year, the last holding after.escalationgives a real change per year, compounding.
Fixed cost and events¶
fixed_opex:maintenanceandinsurance_tax: x fixed capital;labour:operatorsxcost_per_operator, plussupervision;overheads: x (labour + maintenance);items: own annual costs, each with an id and an optional price path.events:[{id, kind, cost | fraction_of_isbl, every_years | every_hours, first_year, downtime_days, treatment, skip_last, source}]. These are replacements (catalyst, membrane, stack), turnarounds, sustaining capital, and one-off costs or grants (a negative cost).every_hourscounts the plant's operating hours at its load, so a stack lasts longer at a lower capacity factor. In batch operation these are batches x cycle time / trains.first_yeardefaults toevery_years, else 1. 0 and below are the last construction years. A one-off after the last operating year is not charged. Withevery_hours, a givenfirst_yearis the year of the first occurrence and the next ones follow everyevery_hoursof running after it; without one, the running hours count from start-up.downtime_dayslowers the load of the year it occurs in, once per occurrence (days of the plant's operating hours).skip_last(default true) leaves a recurring event out of the last operating year; a one-off is never skipped.treatment: capitaldepreciates the cost from start-up until the next occurrence (or over the rest of the life).
Finance and uncertainty¶
finance:discount_rate(real) andtax_rate;losses:credit, offset against the owner's other income, orcarry_forward;depreciation_years(straight line, default the life), ordepreciation, a list of fractions per operating year;construction: fractions per year, summing to 1;working_capital(x fixed capital), orworking_capital_days: days of feed inventory, product inventory, receivables and payables;salvage(x fixed capital).class_by_stage: the AACE 18R-97 class each stage is expected to reach (default concept 5, basic 4, detail 3).uncertainty:iterations,seed;defaultsby status (ASSUMED,QUOTED);ratesby the provenance of the flowsheet values, withrate_mode: independent;- with
rate_mode: throughput, every rate moves together throughthroughput, so the mass balance holds in every sample.
sensitivity.swing is the default tornado swing.
- scenarios: {id, prices: {line or carbon: price}, scale: {input id: multiplier},
source}. A scenario price replaces the line's price, in the baseline price's unit.
Input ids¶
Input ids name what a scenario scales, what the Monte Carlo samples and what the tornado and the break-even move:
price:<line>,rate:<line>(a line isfeed:X,product:Y,product:Y@Corvariable:<id>; a flow's lines per case,feed:X~<case>, share its inputs),rate:<line>only foremission:<id>andallowance:carbon(their price isprice:carbon),price:carbon,conversion:<line>;capex:isbl,capex:osbl,capex:owners_cost,capex:contingency;opex:maintenance,opex:labour,opex:insurance_tax,opex:overheads,opex:<item>,event:<id>;operation:hours_per_year. In batch operation:operation:cycle_timeoroperation:batches_per_year, andoperation:success_rate(with a derived batch count the hours stay an input too);operation:utilisation, andoperation:throughputwithrate_mode: throughput;finance:discount_rate,finance:wc:<days>.
Calculation¶
The cash flow by year t (0 is the first construction year):
- Construction years: the fixed capital times each year's fraction, plus events in those years.
- Working capital and salvage: working capital is spent in the last construction year. It is recovered, and the salvage value received, in the last operating year.
- Operating years, at the year's load (ramp-up, then utilisation, less the downtime of events):
- revenue (product lines) minus the cost lines, each at its price path;
- minus the fixed operating cost and the events expensed that year;
- minus income tax on that result less depreciation.
- Tax losses: a loss is credited, or carried forward to later profits.
- Last year: the book value left is written off and a salvage above it is taxed.
- Discounting: by
(1 + i) ** t.
The results:
- NPV: the sum of the discounted cash flows.
- IRR: found by bisection; none when the cash flow does not change sign.
- Discounted payback: years from start-up.
- LCOP: the main product's price that makes the NPV zero, every other product a credit.
With losses credited the NPV is linear in that price and two evaluations give the LCOP; with
losses carried forward it is found by a bracketed search. With a price path on the main
product, the LCOP is the base price the path multiplies: with
escalationthe price of the first operating year, with aprofilethe price its multiplier 1 stands for (the first year's price is the LCOP x its first multiplier). - Cash cost: (variable cost + fixed cost - co-product revenue) per unit of the main product, first full year.
- Cost of production: per unit of the main product, by category, plus the fixed cost, the
events as an annuity at the discount rate, and capital and tax (the rest of the LCOP). It
sums to the LCOP. With
incentivelines, the LCOP without them is reported too.
Boundary¶
The operating cost and revenue count only the flowsheet's terminals. What the flowsheet implies
but does not declare is not in them: a heater, cooler, compressor, pump, column or reactor with
no energy stream (its steam, cooling, refrigeration or power is priced nowhere), or a product
without category (its disposal or treatment as wastewater, off-gas, solid waste or emission
is not stated). The TEA runs assessment.flows.boundary_gaps on the flowsheet and reports
these as findings cheme.boundary-*, the same check as cheme process energy and the LCA's
boundary_check (the rules are
in the docstring of domains/assessment/flows.py); it does not add or price anything for them.
The check is narrow, and the report says so. It covers steps of the unit kinds compressor, pump,
heater, cooler, shortcut column and the reactors (a step counts as connected when any energy
stream is at its port), terminal categories and utility levels. It does not cover steps of
other kinds (component separators, flash drums, mixers, splitters, unknown classes), whether an
energy stream reaches a utility terminal (a step-to-step stream counts), or whether duties are
given or balance (cheme process energy). A step is exempt only when declared adiabatic:
mode: adiabatic in process/simulation.yaml (a reactor) or heat: adiabatic on its bed, tank
or batch. A simulation.yaml that does not load is cheme.boundary-adiabatic-unread: a
warning naming the unassigned reactors it could have exempted, info when there are none.
The report's boundary (--json, /api/tea) has:
complete: no warning from these rules; not a claim that the inventory is complete;checked(the rules that warn),noted(info only: feed categories, energy terminals' category, utility levels),not_checked: the coverage above, as text;categories,unassigned_duties,gaps(rule, level, ids, message, file, line);excluded_products: products without a category that have no price, are not listed free, have novariable_opexitem of category disposal per them and no emission source on them, and are not the main product, so the operating cost counts them at no cost. A product without a category that is costed in one of these ways is a classification gap only.
estimate/tea.md states gaps under its title and has a section Boundary: what was and was not
checked, what the variable operating cost excludes, and the gaps by rule.
Uncertainty and sensitivity¶
- Break-even: for every input, the value at which the decision's metric meets its threshold. This is the value that would change the decision. It is searched between x0.001 and x1000, nearest to the baseline.
- Tornado: one input at a time, each moved to its
swing, else to the P10 and P90 of its distribution, else to the default swing. The largest effect comes first. - Monte Carlo: one multiplier per input id. It reports the P10, P50, P90 and mean of the fixed capital, LCOP, cash cost, NPV and IRR, the probability of meeting the decision, and Spearman rank correlations naming the drivers. Results are reproducible from the seed.
- NPV and IRR are reported only when the main product is priced.
- A metric that fewer than half the samples have (an IRR) gets no statistics.
- ISBL uncertainty: without its own, the ISBL is spread over the claimed class's AACE range
as a triangle, for example class 5 from x0.5 to x2.0 with its mode at 1. A capital range
narrower than the class can claim is
cheme.tea-capex-range. - Composite inputs: an uncertainty given on a part of one (exchange rate, escalation,
location factor, labour headcount) is reported as unused (
tea-uncertainty-unused); give it on the composite. The discount rate has only its own uncertainty, never a default.
Estimate class against the stage¶
The claimed class comes from isbl.estimate_class, else from the equipment estimate's
estimate_class in cost-basis.yaml, else 5. cheme.tea-class-stage warns in two cases:
- the class is finer than the stage supports, for example class 3 at concept, when the scope is not defined that far;
- the class is coarser than the stage expects before its decision.
Findings¶
| rule | level | finds |
|---|---|---|
cheme.tea-no-premises, -premises |
error | no estimate/tea.yaml, or it does not load |
cheme.tea-no-operation, -case, -product, -no-flowsheet |
error | what the evaluation cannot do without (the flowsheet's own errors stop it too) |
cheme.tea-flow-invalid |
error | a terminal's streams cannot be counted (mixed kinds, unknown unit, negative duty) |
cheme.tea-price-unknown, -price-unit, -price-component, -product-unit |
error | a price for a key that is not a terminal; a unit that needs a conversion; a component the streams do not carry; a main product unit that cannot be converted |
cheme.tea-variable-per, -emission-source, -emission-components, -case-price |
error / warning | a variable item or emission source on an unknown flow or component; a case price for the main product (error) or for a flow without a baseline price (warning); a case price for a key that is no flow is tea-price-unknown |
cheme.tea-currency, -capex-source |
error | an estimate in another currency without exchange_rate; the estimate it builds on does not compute |
cheme.tea-flow-incomplete |
warning | a stream state is missing; the rest is counted |
cheme.tea-price-missing |
warning | flows without a price and not free (a product costed by a disposal item or an emission source is not missing); a decision on npv or irr without a price of the main product |
cheme.tea-capex-uncovered |
warning / info | items not costed, or costed by purchase only, or take-off rows without a rate, without an allowance (info with one) |
cheme.tea-capex-own-cost |
warning | an own cost, or a catalyst fill in another currency than the estimate's, in neither the TEA's nor the estimate's currency and year: left out |
cheme.tea-cost-year |
warning / info | an estimate (or a given ISBL: info) of another year, not escalated; a catalyst fill priced in neither the index's nor the TEA's year |
cheme.tea-class-stage |
warning / info | the class against the stage |
cheme.tea-capex-range |
warning | a capital range narrower than the class supports |
cheme.tea-basis-mismatch |
warning | operation different from lca/lca.yaml |
cheme.tea-lca-price-mismatch |
warning / info | the products' relative prices differ by more than 5 % from the allocation prices of lca/lca.yaml (warning for method: economic, info when economic is only compared), or a product priced there is free or unpriced here |
cheme.tea-batch-overbooked |
warning | more batch hours than the trains have |
cheme.tea-emission-unpriced |
warning / info | lca/lca.yaml maps emissions no source prices (info without emissions) |
cheme.tea-conversion-unused |
warning | a conversion or escalation that is not needed, so not used |
cheme.tea-no-decision, -no-fixed-opex, -example, -example-rates, -scenario |
warning | what makes the result weaker than it looks |
cheme.tea-price-year, -no-year, -operation-from-lca, -capex-missing, -confidence, -case-terminal, -batch-basis, -event-beyond-life, -turnaround-hours, -depreciation, -uncertainty-unused |
info | worth knowing |
cheme.tea-not-comparable |
warning | --compare: the two analyses differ in a premise of their basis |
cheme.boundary-utility-unassigned |
warning | a step of a checked kind with no energy stream: the operating cost excludes its utility (see Boundary) |
cheme.boundary-unclassified |
warning / info | a product (warning) or feed (info) without a category; excluded from the cost only when it is also unpriced and not free |
cheme.boundary-adiabatic-unread |
warning / info | process/simulation.yaml does not load, so mode: adiabatic is not read: warning when an unassigned reactor depends on it (named), info otherwise |
cheme.boundary-energy-terminal, -utility-level |
info | an energy terminal not classified utility; a heat utility without its level |
Commands¶
cheme assess tea # estimate/tea.md (Markdown)
cheme assess tea --json # premises, results, findings, provenance
cheme assess tea --csv # the cash flow by year
cheme assess tea --sensitivity # tornado and break-even per input
cheme assess tea --write # estimate/tea.md, tea-cashflow.csv, tea-sensitivity.csv,
# tea-uncertainty.csv (with a Monte Carlo)
cheme assess tea --check # exit 1 when one of them is not current
cheme assess tea --compare main # what the two do not share, then the results
Gate kind: tea¶
require takes any of these:
prices: every flow with a rate, other than the main product, is priced or free.no-exampleclass-stage: the class equals the class the stage expects.decision: stated and met, with the confidence when there is a Monte Carlo.tea-current
Viewer API¶
GET /api/tea?rev= returns the document of cheme assess tea --json without the source, with
rev. Its keys are:
currency,cost_year,example,stage,estimate_class,expected_classdecision,metric,threshold,basis,operationcapex(each line in its estimate'scurrency, withtea_value),capex_rangeflows(per_year: the net income of the flow's lines, revenue positive and cost negative;prices: every price of the flow),lines,variable,emissions,opex,eventsresults,cost_of_production,cashflowscenarios,inputs,break_even,tornado,uncertainty,boundary,findings
A number that does not exist, such as an IRR, is null. The endpoint answers 404 without
estimate/tea.yaml, and 422 when the TEA does not compute. /api/repo has has_tea, and
cheme export-static exports tea. The viewer shows the analysis on its TEA page.