fix and add plant, formula and master data minor
parent
a9a8a63727
commit
9285169bb4
Binary file not shown.
@ -0,0 +1,193 @@
|
||||
"""
|
||||
Collected formulas and helpers extracted from module algorithms.
|
||||
|
||||
This file consolidates the core mathematical/financial formulas used across:
|
||||
- `Eac.py` (NPV, PMT, discounted projections, EAC calculation)
|
||||
- `insert_actual_data.py` (aggregation formulas, man-hour conversion)
|
||||
- `Prediksi.py` (future value / fv wrappers)
|
||||
|
||||
Keep these functions pure and well-documented to make debugging and
|
||||
comparisons easier.
|
||||
"""
|
||||
from typing import Iterable, List, Sequence
|
||||
import math
|
||||
import numpy as np
|
||||
try:
|
||||
import numpy_financial as npf
|
||||
except Exception: # numpy_financial may not be installed in all envs
|
||||
npf = None
|
||||
|
||||
|
||||
def npv_from_cash_flows(cash_flows: Sequence[float], rate: float, offset: int = 0) -> float:
|
||||
"""Compute discounted sum (NPV) of `cash_flows` at `rate` with optional time offset.
|
||||
|
||||
Formula used in `Eac.py`:
|
||||
NPV = Σ [Ct / (1 + r)^(offset + t)]
|
||||
where t is 1-based index of flows in `cash_flows`.
|
||||
|
||||
Args:
|
||||
cash_flows: sequence of cash flows (Ct) for consecutive periods.
|
||||
rate: periodic discount/inflation rate (e.g., 0.05 for 5%).
|
||||
offset: integer number of periods to offset (e.g., last_seq from actuals).
|
||||
|
||||
Returns:
|
||||
Discounted present value as float.
|
||||
"""
|
||||
if not cash_flows:
|
||||
return 0.0
|
||||
total = 0.0
|
||||
for i, value in enumerate(cash_flows):
|
||||
exp = offset + i + 1
|
||||
total += float(value) / ((1.0 + rate) ** exp)
|
||||
return float(total)
|
||||
|
||||
|
||||
def pmt_formula(rate: float, nper: int, pv: float) -> float:
|
||||
"""Compute level payment PMT for given rate, periods and present value.
|
||||
|
||||
Uses the standard annuity payment formula (same as numpy_financial.pmt but
|
||||
returned value uses same sign-convention as mathematical formula):
|
||||
|
||||
PMT = PV * [r (1+r)^n] / [(1+r)^n - 1]
|
||||
|
||||
Special-case when rate == 0 (equal division): PMT = PV / nper
|
||||
|
||||
Note: many code places use sign flips (e.g. `-npf.pmt(...)`) because of
|
||||
numpy_financial's cashflow sign policy. Use whichever sign convention your
|
||||
caller expects.
|
||||
|
||||
Args:
|
||||
rate: periodic rate (decimal)
|
||||
nper: number of periods (int)
|
||||
pv: present value to amortize
|
||||
|
||||
Returns:
|
||||
payment per period (float)
|
||||
"""
|
||||
if nper == 0:
|
||||
raise ValueError("nper must be > 0")
|
||||
if abs(rate) < 1e-12:
|
||||
return float(pv) / float(nper)
|
||||
r = float(rate)
|
||||
n = int(nper)
|
||||
numerator = pv * r * ((1 + r) ** n)
|
||||
denominator = ((1 + r) ** n) - 1
|
||||
return float(numerator / denominator)
|
||||
|
||||
|
||||
def pmt(rate: float, nper: int, pv: float) -> float:
|
||||
"""Wrapper to compute PMT using `numpy_financial` when available, else fall back to formula.
|
||||
|
||||
Returns the value that `numpy_financial.pmt` would return (i.e. typically
|
||||
negative for positive PV by that library's sign convention). The caller
|
||||
can negate the result to get a positive payment if desired (as seen in
|
||||
`Eac.py` where they do `-npf.pmt(...)`).
|
||||
"""
|
||||
if npf is not None:
|
||||
return float(npf.pmt(rate, nper, pv))
|
||||
# numpy_financial returns payments with sign convention; our fallback
|
||||
# returns payment with the same absolute magnitude but as negative (to
|
||||
# match npf behavior for positive PV) so callers that negate will get the
|
||||
# same sign semantics.
|
||||
res = pmt_formula(rate, nper, pv)
|
||||
return -res
|
||||
|
||||
|
||||
def fv(rate: float, nper: int, pmt: float, pv: float) -> float:
|
||||
"""Future value wrapper to `numpy_financial.fv` when available.
|
||||
|
||||
Used in `Prediksi.py` as `npf.fv(rate, nper + i, pmt, pv)` to compute
|
||||
forecasting values.
|
||||
"""
|
||||
if npf is not None:
|
||||
return float(npf.fv(rate, nper, pmt, pv))
|
||||
# fallback: compute iteratively
|
||||
r = float(rate)
|
||||
n = int(nper)
|
||||
fv_val = -pv
|
||||
# apply periods
|
||||
for _ in range(n):
|
||||
fv_val = fv_val * (1 + r) + (-pmt)
|
||||
return float(fv_val)
|
||||
|
||||
|
||||
def discounted_projection_sum(values: Sequence[float], inflation_rate: float, last_seq: int = 0) -> float:
|
||||
"""Discount a sequence of projected `values` using `inflation_rate` with an initial offset `last_seq`.
|
||||
|
||||
This mirrors the calculation in `Eac.py` where projected flows are discounted
|
||||
starting at `last_seq + 1`.
|
||||
"""
|
||||
return npv_from_cash_flows(values, inflation_rate, offset=last_seq)
|
||||
|
||||
|
||||
def rc_labor_cost(interval: float, labor_time: float, labor_human: float, man_hour_rate: float) -> float:
|
||||
"""Compute labor cost for corrective/pm/oh rows.
|
||||
|
||||
Formula from `Prediksi.py` / `insert_actual_data.py`:
|
||||
rc_xxx_labor_cost = (interval * labor_time * labor_human * man_hour_rate)
|
||||
"""
|
||||
return float(interval) * float(labor_time) * float(labor_human) * float(man_hour_rate)
|
||||
|
||||
|
||||
def rc_lost_cost(raw_loss_output_mw: float, raw_loss_output_price: float, cm_interval: float) -> float:
|
||||
"""Compute lost production cost as in `Prediksi.py`.
|
||||
|
||||
Formula used:
|
||||
rc_lost_cost = (raw_loss_output_MW * raw_loss_output_price * raw_cm_interval) * 1000
|
||||
The multiplication by 1000 appears in source and is preserved here.
|
||||
"""
|
||||
return float(raw_loss_output_mw) * float(raw_loss_output_price) * float(cm_interval) * 1000.0
|
||||
|
||||
|
||||
def rc_total_cost(**components) -> float:
|
||||
"""Sum all provided components into a total RC cost.
|
||||
|
||||
The `Eac.py` / `Prediksi.py` code builds a `rc_total_cost` as the sum of many
|
||||
parts; this helper simply sums values passed as keyword args (coalescing
|
||||
None to 0.0) to make testing/comparison easier.
|
||||
"""
|
||||
total = 0.0
|
||||
for k, v in components.items():
|
||||
try:
|
||||
total += 0.0 if v is None else float(v)
|
||||
except Exception:
|
||||
# ignore non-numeric components
|
||||
continue
|
||||
return float(total)
|
||||
|
||||
|
||||
def seconds_to_hours(seconds: float, ndigits: int = 2) -> float:
|
||||
"""Convert seconds to hours and round to `ndigits` decimals.
|
||||
|
||||
This mirrors usage like:
|
||||
ROUND(SUM(EXTRACT(EPOCH FROM (actfinish - actstart)) / 3600), 2)
|
||||
in SQL queries seen in `insert_actual_data.py`.
|
||||
"""
|
||||
hours = float(seconds) / 3600.0
|
||||
return float(round(hours, ndigits))
|
||||
|
||||
|
||||
def man_count_from_labtrans(count: int) -> int:
|
||||
"""Return man_count as per SQL CASE: if count == 0 -> 3 else count.
|
||||
|
||||
Implemented to match the SQL "CASE WHEN COUNT(b.laborcode) = 0 THEN 3 ELSE COUNT(b.laborcode)".
|
||||
"""
|
||||
try:
|
||||
c = int(count)
|
||||
except Exception:
|
||||
c = 0
|
||||
return 3 if c == 0 else c
|
||||
|
||||
|
||||
__all__ = [
|
||||
"npv_from_cash_flows",
|
||||
"pmt_formula",
|
||||
"pmt",
|
||||
"fv",
|
||||
"discounted_projection_sum",
|
||||
"rc_labor_cost",
|
||||
"rc_lost_cost",
|
||||
"rc_total_cost",
|
||||
"seconds_to_hours",
|
||||
"man_count_from_labtrans",
|
||||
]
|
||||
Binary file not shown.
Loading…
Reference in New Issue