Files
HAMeter/tests/test_cost.py
2026-03-06 12:25:27 -05:00

97 lines
4.4 KiB
Python

"""Tests for hameter.cost."""
from hameter.config import RateComponent
from hameter.cost import calculate_incremental_cost
class TestCalculateIncrementalCost:
"""Tests for the cost calculation engine."""
def test_single_per_unit_rate(self):
factors = [RateComponent(name="Supply", rate=0.14742, type="per_unit")]
result = calculate_incremental_cost(100.0, factors)
assert result.delta == 100.0
assert abs(result.total_incremental_cost - 14.742) < 0.001
assert len(result.component_costs) == 1
assert result.component_costs[0]["name"] == "Supply"
assert abs(result.component_costs[0]["cost"] - 14.742) < 0.001
def test_multiple_per_unit_rates(self):
factors = [
RateComponent(name="Supply", rate=0.14742, type="per_unit"),
RateComponent(name="Distribution", rate=0.09443, type="per_unit"),
RateComponent(name="Transmission", rate=0.04673, type="per_unit"),
]
result = calculate_incremental_cost(961.0, factors)
expected = 961.0 * (0.14742 + 0.09443 + 0.04673)
assert abs(result.total_incremental_cost - expected) < 0.01
def test_negative_rate_credit(self):
factors = [
RateComponent(name="Supply", rate=0.10, type="per_unit"),
RateComponent(name="Credit", rate=-0.02, type="per_unit"),
]
result = calculate_incremental_cost(100.0, factors)
# 100 * 0.10 + 100 * (-0.02) = 10.0 - 2.0 = 8.0
assert abs(result.total_incremental_cost - 8.0) < 0.001
def test_fixed_charges_excluded(self):
factors = [
RateComponent(name="Supply", rate=0.10, type="per_unit"),
RateComponent(name="Customer Charge", rate=9.65, type="fixed"),
]
result = calculate_incremental_cost(100.0, factors)
# Only per_unit: 100 * 0.10 = 10.0
assert abs(result.total_incremental_cost - 10.0) < 0.001
# Fixed component should have cost=0
fixed = [c for c in result.component_costs if c["type"] == "fixed"]
assert len(fixed) == 1
assert fixed[0]["cost"] == 0.0
def test_zero_delta(self):
factors = [RateComponent(name="Supply", rate=0.10, type="per_unit")]
result = calculate_incremental_cost(0.0, factors)
assert result.total_incremental_cost == 0.0
assert result.delta == 0.0
def test_empty_cost_factors(self):
result = calculate_incremental_cost(100.0, [])
assert result.total_incremental_cost == 0.0
assert result.component_costs == []
def test_full_bill_example(self):
"""Match the user's electric bill: 961 kWh, $313.10 total."""
factors = [
RateComponent(name="Generation", rate=0.14742, type="per_unit"),
RateComponent(name="Dist 1", rate=0.09443, type="per_unit"),
RateComponent(name="Dist 2", rate=0.01037, type="per_unit"),
RateComponent(name="Transition", rate=-0.00077, type="per_unit"),
RateComponent(name="Transmission", rate=0.04673, type="per_unit"),
RateComponent(name="Net Meter Recovery", rate=0.00625, type="per_unit"),
RateComponent(name="Revenue Decoupling", rate=-0.00044, type="per_unit"),
RateComponent(name="Distributed Solar", rate=0.00583, type="per_unit"),
RateComponent(name="Renewable Energy", rate=0.00050, type="per_unit"),
RateComponent(name="Energy Efficiency", rate=0.02506, type="per_unit"),
RateComponent(name="EV Program", rate=0.00238, type="per_unit"),
RateComponent(name="Customer Charge", rate=9.65, type="fixed"),
]
result = calculate_incremental_cost(961.0, factors)
# Per-unit total (excluding fixed and the tiered distribution nuance)
# Sum of per-unit rates: 0.32776
per_unit_sum = sum(cf.rate for cf in factors if cf.type == "per_unit")
expected = 961.0 * per_unit_sum
assert abs(result.total_incremental_cost - expected) < 0.01
def test_component_costs_preserve_all_fields(self):
factors = [
RateComponent(name="Supply", rate=0.10, type="per_unit"),
RateComponent(name="Fixed", rate=5.0, type="fixed"),
]
result = calculate_incremental_cost(50.0, factors)
assert len(result.component_costs) == 2
for c in result.component_costs:
assert "name" in c
assert "rate" in c
assert "type" in c
assert "cost" in c