mixle.analysis.valuation module

Parametric extraction-cost curves and Monte-Carlo project valuation (work-plan Sec.7-J, J4).

A general project-economics toolkit: a parametric per-unit cost as a function of depth, grade (concentration of the recovered constituent), and throughput; a capex/opex roll-up over a multi-period plan; and a Monte-Carlo discounted-cash-flow valuation over posterior grade draws and price-path scenarios. This module’s worked instantiation is mine economics – J’s objective function needs a $/t cost that is a function of where the ore is (depth), what it is (grade), and how fast it is mined (throughput) before J2 can turn price paths + posterior grade draws into an NPV distribution, and before H’s block-level optimizers (mixle.stochastic_opt, mixle.relations) have a block_cost to subtract from revenue – but the cost curve and DCF machinery apply to any extraction/production project with a depth-, grade-, and throughput-sensitive cost structure:

  • cost_curve() – parametric mining + processing cost in $/t, monotone increasing in haul/ pumping depth, complexity-adjusted by grade, and shaped like the classic economies-of-scale curve in throughput: cheapest at the plant’s design capacity, more expensive both under- and over-utilized.

  • capex_opex() – rolls a period-by-period mine plan (tonnage, depth, grade, throughput, plus any lumpy capital spend) up into total capital and total operating cost, via cost_curve().

This module is created by J4 (Wave 1) and extended here by J2 (Wave 2) with monte_carlo_npv() / NPVDistribution, the risk-neutral expected-DCF distribution H4’s objective is priced against:

  • monte_carlo_npv() – Monte-Carlo DCF over posterior.samples(n, rng) x price scenarios (DR-ALG J2): grade draws come straight off an IC-1 Posterior (frozen mixle.reason .posterior_protocol.Posterior), price scenarios come from J1’s PriceForecast.paths (or any array-like of per-period price paths), and per-period tonnage/capex come off schedule. Returns the full NPVDistribution (mean, P10/P50/P90, and a grade-vs-price variance decomposition) – a distribution, never a single point estimate.

Repo-boundary note (see the PR body for the full explanation): as of this PR, J1 (mixle.inference.price_forecast.forecast_price, whose PriceForecast.paths this task’s own Algorithm text names as the price-scenario source) and A5 (mixle_pde.decision_quantities, IC-8’s calibrated net-pay/tonnage quantities) had not landed. monte_carlo_npv therefore consumes price_paths and schedule structurally (an (n_paths, n_periods) array-like and a tonnage/capex-per-period mapping or array-like, respectively) rather than importing either concrete module by name – exactly what its frozen Public API signature already commits to (price_paths: Any, schedule: Any). A real PriceForecast.paths / A5 decision-quantity slots in without any change to this function once those tasks land, since both already produce array-likes of that shape.

class NPVDistribution(samples, mean, p10, p50, p90, sensitivity)[source]

Bases: _NPVDistributionFields

A Monte-Carlo DCF outcome: the full NPV sample, not just a point estimate.

samples is the length-n array of per-draw NPVs (real dollars, one entry per Monte-Carlo trial) that value_at_risk() / conditional_value_at_risk() (J5) consume directly. mean/p10/p50/p90 are the usual project-finance summary of that distribution. sensitivity decomposes the NPV variance into the grade- and price-uncertainty contributions, as Sobol first-order variance-based sensitivity indices (see monte_carlo_npv() and _sobol_first_order_share()): keyed "grade" / "price" (each factor’s fraction of total variance explained on its own, mathematically in [0, 1] and not required to sum to 1 – NPV is multiplicative in grade and price, so some variance is a grade/price interaction effect attributed to neither) plus the raw "grade_variance" / "price_variance" / "total_variance".

Construction validates samples: non-empty and finite (no NaN/Inf) – defense-in-depth so invalid state can never flow downstream to a caller even if some upstream computation (here, monte_carlo_npv()) fails to validate its own inputs, the same guard applied to carcinogenic_risk.RiskQuantity and the _SampleDerivedQuantity carriers elsewhere in mixle.analysis.

Parameters:
Return type:

NPVDistribution

capex_opex(plan, *, params)[source]

Roll a mine plan’s tonnage/depth/grade/throughput profile up into (total capex, total opex).

plan is a mapping or attribute-bearing object exposing, per scheduling period:

  • tonnage: array-like, tonnes mined/processed each period (required).

  • depth, grade, throughput: array-likes (or scalars, broadcast against tonnage) fed to cost_curve() to get each period’s $/t.

  • capex_schedule (optional): array-like of lumpy capital spend per period (e.g. pre-strip, plant construction, fleet purchases); summed into total capex on top of the params below.

params is passed through to cost_curve() for the opex side, plus two capex-only keys: capex_fixed (one-off, tonnage-independent capital) and capex_per_tonne (sustaining capital that scales with total tonnage mined over the plan).

Total opex is sum(tonnage * cost_curve(depth, grade, throughput, params=params)); total capex is capex_fixed + capex_per_tonne * sum(tonnage) + sum(capex_schedule). Returns (capex, opex), both plain floats – the totals monte_carlo_npv() (J2) discounts into a DCF, and the same $/t curve this function calls is what feeds block_cost for H’s optimizers.

tonnage must be finite and non-negative and have at least one period; depth/grade/ throughput must each be a scalar or share tonnage’s exact shape (see _require_scalar_or_period_shape()); capex_schedule, if given, must be finite and non-negative. A negative tonnage or a negative cost/capital term would otherwise turn modeled spending into modeled revenue with no error (MXR-080-0119); raises ValueError if violated.

Parameters:
Return type:

tuple[float, float]

cost_curve(depth, grade, throughput, *, params)[source]

Parametric mining + processing cost in $/t, as a function of depth, grade, and throughput.

depth, grade, and throughput are broadcastable array-likes (one entry per block or per scheduling period; scalars broadcast against the others). params recognizes (all optional, defaulting to zero/no-effect):

  • base_cost: $/t floor cost.

  • haul_cost_per_m: $/t per metre of depth – haulage and pumping/dewatering cost, modeled as linear in depth, so the curve is strictly increasing in depth whenever this is positive.

  • grade_complexity_coef: $/t scale of a 1 / grade metallurgical-complexity penalty – lower-grade ore needs proportionally more material handled and processed per unit of recovered metal, so this term falls as grade rises.

  • throughput_scale_coef / design_capacity: the plant has one throughput, design_capacity (Q*), at which fixed costs are spread most efficiently; cost rises quadratically away from it in either direction – throughput_scale_coef * ((Q - Q*) / Q*) ** 2 – capturing both under-utilized fixed-cost drag below Q* and overtime/expediting/accelerated-wear cost above it (the classic “decreasing then rising past design capacity” U-shaped average-cost curve).

depth must be finite and non-negative (metres below surface/datum; there is no such thing as negative haul depth); grade and throughput must be finite and strictly positive (both are also divisors elsewhere in the mine-planning stack, and a zero or negative ore grade or throughput is not a physical plan). Every numeric params entry must be finite, and the cost-coefficient entries (base_cost, haul_cost_per_m, grade_complexity_coef, throughput_scale_coef) must be non-negative – a negative coefficient would let one additive term of the curve go negative and silently discount an otherwise non-negative total cost into a subsidy (MXR-080-0119). Raises ValueError if any of these are violated.

Returns the elementwise $/t cost, broadcast to the common shape of the three inputs.

Parameters:
Return type:

ndarray

monte_carlo_npv(posterior, price_paths, cost_model, schedule, *, discount_rate, n=10000, rng)[source]

Monte-Carlo discounted-cash-flow NPV distribution (DR-ALG J2).

Draws n grade realizations off the IC-1 posterior and pairs them, draw for draw, with n (resampled/aligned as needed) price scenarios from price_paths; for each draw and each of the schedule’s periods, cashflow_t = tonnage_t * grade_t * price_t - opex_t - capex_t, discounted at discount_rate (period 0 undiscounted, period t divided by (1 + discount_rate) ** t) and summed into one NPV per draw:

  • posterior: an IC-1 Posterior (frozen mixle.reason.posterior_protocol.Posterior); its .samples(n, rng) are the grade draws. A single project-life grade (d == 1) broadcasts across every period; a per-period posterior (d == len(schedule)) is used period by period.

  • price_paths: a scenario-major (m, n_periods) array-like (one row per scenario), OR a J1 PriceForecast.paths passed directly – (n_periods, m), time-major – detected and transposed automatically (see _align_price_paths()). Resampled with replacement to n rows when m != n.

  • cost_model: declares exactly one supported positional signature, either cost_model(t, tonnage_t) or cost_model(t), to get that period’s deterministic opex_t. Signature binding selects the form before execution; an interface accepting both is rejected as ambiguous rather than guessed.

  • schedule: per-period tonnage (required) and capex (optional, default zero); see _unpack_schedule(). len(schedule)’s tonnage vector fixes n_periods.

  • discount_rate: the DCF discount rate per period. Must be finite and strictly greater than -1 – discount_rate <= -1 makes the per-period discount factor 1 / (1 + discount_rate) ** t divide by zero (at exactly -1) or alternate sign every other period (below -1), neither of which is a coherent discount rate.

  • n / rng: Monte-Carlo draw count and the shared numpy.random.Generator. n must be a positive int (not merely a positive-valued float); posterior.samples(n, rng) is required to return exactly n draws, checked explicitly rather than trusted – a posterior that returns fewer (e.g. a single row) used to silently numpy-broadcast across every price scenario, fabricating n “independent” draws that all shared one real grade sample (MXR-080-0118).

Every numeric input – the grade draws, price_paths, schedule’s tonnage/capex, and cost_model’s returned opex – must be finite and non-negative; raises ValueError if any of these, n, or discount_rate are violated, before any DCF arithmetic runs.

Returns an NPVDistribution with the raw samples, mean/p10/p50/p90, and a sensitivity dict decomposing NPV variance into grade vs. price contributions as Sobol first-order variance-based sensitivity indices (Saltelli/Jansen two-independent-sample estimator; see _sobol_first_order_share()) – each factor’s own share of the total NPV variance, estimated from a SECOND joint sample independent of the one samples/mean/p10/p50/ p90 are built from, so the indices stay meaningful (population-bounded in [0, 1]) even when a caller’s price_paths happen to be paired against the posterior’s draws in a way that makes the actual joint sample’s own variance small (MXR-080-0117). Not a full ANOVA decomposition (grade and price’s interaction/second-order effect, if any, is not itself reported), but each reported share is individually a real, correctly-estimated first-order Sobol index.

Parameters:
Return type:

NPVDistribution