Coverage for backend/django/Economics/formulas/builders/metrics.py: 100%
82 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1from __future__ import annotations
3from dataclasses import dataclass
4from decimal import Decimal
5from typing import Mapping
7import sympy
9from Economics.formulas.engine.core import EconomicsFormula, FormulaInput, decimal_to_sympy
12@dataclass(frozen=True)
13class BoundMetricFormula:
14 formula: EconomicsFormula
15 bindings: dict[str, Decimal]
17 def evaluate(self) -> Decimal | None:
18 return self.formula.evaluate(self.bindings)
20 def render_property_formula(self, render_bindings: Mapping[str, str] | None = None) -> str:
21 """Render the formula for PropertyValue storage.
23 Numeric bindings remain the source of truth for evaluation, but callers
24 can override selected symbols with references to lower-level
25 PropertyValues so rendered economics properties compose like ordinary
26 formulas instead of collapsing everything to constants.
27 """
29 rendered_bindings = {key: _decimal_literal(value) for key, value in self.bindings.items()}
30 rendered_bindings.update(render_bindings or {})
31 return self.formula.render_property_formula(rendered_bindings)
34def annual_profit_formula(*, target_annual_revenue: Decimal, target_annual_opex: Decimal, unit: str) -> BoundMetricFormula:
35 return _bound_formula(
36 key="annual_profit",
37 expression=sympy.Symbol("target_annual_revenue") - sympy.Symbol("target_annual_opex"),
38 unit=unit,
39 bindings={
40 "target_annual_revenue": target_annual_revenue,
41 "target_annual_opex": target_annual_opex,
42 },
43 )
46def annual_savings_formula(
47 *,
48 baseline_annual_opex: Decimal,
49 target_annual_opex: Decimal,
50 unit: str,
51) -> BoundMetricFormula:
52 return _bound_formula(
53 key="annual_savings",
54 expression=(
55 sympy.Symbol("baseline_annual_opex")
56 - sympy.Symbol("target_annual_opex")
57 ),
58 unit=unit,
59 bindings={
60 "baseline_annual_opex": baseline_annual_opex,
61 "target_annual_opex": target_annual_opex,
62 },
63 )
66def annual_net_benefit_formula(
67 *,
68 annual_savings: Decimal,
69 target_annual_revenue: Decimal,
70 unit: str,
71) -> BoundMetricFormula:
72 return _bound_formula(
73 key="annual_net_benefit",
74 expression=(
75 sympy.Symbol("annual_savings")
76 + sympy.Symbol("target_annual_revenue")
77 ),
78 unit=unit,
79 bindings={
80 "annual_savings": annual_savings,
81 "target_annual_revenue": target_annual_revenue,
82 },
83 )
86def depreciation_tax_shield_formula(
87 *,
88 annual_depreciation: Decimal,
89 tax_rate: Decimal,
90 unit: str,
91) -> BoundMetricFormula:
92 return _bound_formula(
93 key="depreciation_tax_shield",
94 expression=sympy.Symbol("annual_depreciation") * sympy.Symbol("tax_rate"),
95 unit=unit,
96 bindings={
97 "annual_depreciation": annual_depreciation,
98 "tax_rate": tax_rate,
99 },
100 )
103def after_tax_annual_cash_flow_formula(
104 *,
105 annual_net_benefit: Decimal,
106 annual_depreciation: Decimal,
107 tax_rate: Decimal,
108 unit: str,
109) -> BoundMetricFormula:
110 return _bound_formula(
111 key="after_tax_annual_cash_flow",
112 expression=(
113 sympy.Symbol("annual_net_benefit") * (decimal_to_sympy(Decimal("1")) - sympy.Symbol("tax_rate"))
114 + sympy.Symbol("annual_depreciation") * sympy.Symbol("tax_rate")
115 ),
116 unit=unit,
117 bindings={
118 "annual_net_benefit": annual_net_benefit,
119 "annual_depreciation": annual_depreciation,
120 "tax_rate": tax_rate,
121 },
122 )
125def incremental_capex_formula(*, target_capex: Decimal, baseline_capex: Decimal, unit: str) -> BoundMetricFormula:
126 return _bound_formula(
127 key="incremental_capex",
128 expression=sympy.Symbol("target_capex") - sympy.Symbol("baseline_capex"),
129 unit=unit,
130 bindings={
131 "target_capex": target_capex,
132 "baseline_capex": baseline_capex,
133 },
134 )
137def metric_value_formula(*, key: str, value: Decimal, unit: str, input_key: str | None = None) -> BoundMetricFormula:
138 """Wrap an already-resolved scalar so metric rows still use formula evaluation."""
140 formula_input = input_key or key
141 return _bound_formula(
142 key=key,
143 expression=sympy.Symbol(formula_input),
144 unit=unit,
145 bindings={formula_input: value},
146 )
149def roi_percent_formula(
150 *,
151 incremental_capex: Decimal,
152 annual_cash_flow: Decimal,
153 project_lifetime_years: int,
154 residual_value: Decimal,
155) -> BoundMetricFormula:
156 incremental_capex_symbol = sympy.Symbol("incremental_capex")
157 numerator_terms = [
158 sympy.Symbol("annual_cash_flow") * decimal_to_sympy(project_lifetime_years),
159 -incremental_capex_symbol,
160 ]
161 bindings = {
162 "incremental_capex": incremental_capex,
163 "annual_cash_flow": annual_cash_flow,
164 }
165 if residual_value != Decimal("0"):
166 numerator_terms.append(sympy.Symbol("residual_value"))
167 bindings["residual_value"] = residual_value
168 expression = sympy.Add(*numerator_terms, evaluate=False) / incremental_capex_symbol * decimal_to_sympy(Decimal("100"))
169 return _bound_formula(
170 key="roi_percent",
171 expression=expression,
172 unit="percent",
173 bindings=bindings,
174 )
177def cash_flow_formula(
178 *,
179 year: int,
180 project_lifetime_years: int,
181 incremental_capex: Decimal,
182 annual_cash_flow: Decimal,
183 residual_value: Decimal,
184 unit: str,
185) -> BoundMetricFormula:
186 return _bound_formula(
187 key=f"cash_flow_year_{year}",
188 expression=_cash_flow_expression(year=year, project_lifetime_years=project_lifetime_years),
189 unit=unit,
190 bindings={
191 "incremental_capex": incremental_capex,
192 "annual_cash_flow": annual_cash_flow,
193 "residual_value": residual_value,
194 },
195 )
198def discounted_cash_flow_formula(
199 *,
200 year: int,
201 cash_flow: Decimal,
202 discount_rate: Decimal,
203 unit: str,
204) -> BoundMetricFormula:
205 return _bound_formula(
206 key=f"discounted_cash_flow_year_{year}",
207 expression=_discounted_expression(sympy.Symbol("cash_flow"), year=year),
208 unit=unit,
209 bindings={
210 "cash_flow": cash_flow,
211 "discount_rate": discount_rate,
212 },
213 )
216def discount_factor_formula(*, year: int, discount_rate: Decimal) -> BoundMetricFormula:
217 return _bound_formula(
218 key=f"discount_factor_year_{year}",
219 expression=_discounted_expression(decimal_to_sympy(Decimal("1")), year=year),
220 unit="factor",
221 bindings={"discount_rate": discount_rate},
222 )
225def cumulative_cash_flow_formula(
226 *,
227 year: int,
228 project_lifetime_years: int,
229 incremental_capex: Decimal,
230 annual_cash_flow: Decimal,
231 residual_value: Decimal,
232 unit: str,
233) -> BoundMetricFormula:
234 """Build the cumulative undiscounted cash-flow formula through ``year``."""
236 return _bound_formula(
237 key=f"cumulative_cash_flow_year_{year}",
238 expression=sympy.Add(
239 *(
240 _cash_flow_expression(year=row_year, project_lifetime_years=project_lifetime_years)
241 for row_year in range(0, year + 1)
242 ),
243 evaluate=False,
244 ),
245 unit=unit,
246 bindings={
247 "incremental_capex": incremental_capex,
248 "annual_cash_flow": annual_cash_flow,
249 "residual_value": residual_value,
250 },
251 )
254def cumulative_present_value_formula(
255 *,
256 key: str,
257 year: int,
258 project_lifetime_years: int,
259 incremental_capex: Decimal,
260 annual_cash_flow: Decimal,
261 discount_rate: Decimal,
262 residual_value: Decimal,
263 unit: str,
264) -> BoundMetricFormula:
265 """Build the cumulative discounted cash-flow formula through ``year``."""
267 return _bound_formula(
268 key=key,
269 expression=sympy.Add(
270 *(
271 _discounted_expression(
272 _cash_flow_expression(year=row_year, project_lifetime_years=project_lifetime_years),
273 year=row_year,
274 )
275 for row_year in range(0, year + 1)
276 ),
277 evaluate=False,
278 ),
279 unit=unit,
280 bindings={
281 "incremental_capex": incremental_capex,
282 "annual_cash_flow": annual_cash_flow,
283 "discount_rate": discount_rate,
284 "residual_value": residual_value,
285 },
286 )
289def npv_formula(
290 *,
291 incremental_capex: Decimal,
292 annual_cash_flow: Decimal,
293 project_lifetime_years: int,
294 discount_rate: Decimal,
295 residual_value: Decimal,
296 unit: str,
297) -> BoundMetricFormula:
298 """Build NPV as the final cumulative discounted cash-flow formula."""
300 annuity_factor = _discounted_annuity_expression(
301 project_lifetime_years=project_lifetime_years,
302 discount_rate=discount_rate,
303 )
304 final_discount_factor = _discounted_expression(
305 decimal_to_sympy(Decimal("1")),
306 year=project_lifetime_years,
307 )
308 terms = [
309 -sympy.Symbol("incremental_capex"),
310 sympy.Symbol("annual_cash_flow") * annuity_factor,
311 ]
312 bindings = {
313 "incremental_capex": incremental_capex,
314 "annual_cash_flow": annual_cash_flow,
315 "discount_rate": discount_rate,
316 }
317 if residual_value != Decimal("0"):
318 terms.append(sympy.Symbol("residual_value") * final_discount_factor)
319 bindings["residual_value"] = residual_value
320 expression = sympy.Add(*terms, evaluate=False)
321 return _bound_formula(
322 key="npv",
323 expression=expression,
324 unit=unit,
325 bindings=bindings,
326 )
329def _bound_formula(
330 *,
331 key: str,
332 expression: sympy.Expr,
333 unit: str,
334 bindings: dict[str, Decimal],
335) -> BoundMetricFormula:
336 return BoundMetricFormula(
337 formula=EconomicsFormula(
338 key=f"metric:{key}",
339 expression=expression,
340 unit=unit,
341 inputs=tuple(
342 FormulaInput(key=input_key, label=input_key.replace("_", " "), unit="")
343 for input_key in bindings
344 ),
345 ),
346 bindings=bindings,
347 )
350def _decimal_literal(value: Decimal | int | str) -> str:
351 decimal_value = Decimal(str(value))
352 return format(decimal_value, "f").rstrip("0").rstrip(".") or "0"
355def _cash_flow_expression(*, year: int, project_lifetime_years: int) -> sympy.Expr:
356 if year == 0:
357 return -sympy.Symbol("incremental_capex")
358 expression = sympy.Symbol("annual_cash_flow")
359 if year == project_lifetime_years:
360 expression += sympy.Symbol("residual_value")
361 return expression
364def _discounted_expression(expression: sympy.Expr, *, year: int) -> sympy.Expr:
365 if year == 0:
366 return expression
367 discount_denominator = (decimal_to_sympy(Decimal("1")) + sympy.Symbol("discount_rate")) ** year
368 return expression / discount_denominator
371def _discounted_annuity_expression(*, project_lifetime_years: int, discount_rate: Decimal) -> sympy.Expr:
372 if discount_rate == Decimal("0"):
373 return decimal_to_sympy(project_lifetime_years)
374 discount_rate_symbol = sympy.Symbol("discount_rate")
375 return (
376 decimal_to_sympy(Decimal("1"))
377 - (decimal_to_sympy(Decimal("1")) + discount_rate_symbol) ** -project_lifetime_years
378 ) / discount_rate_symbol