Skip to content

cost function math

github-actions edited this page Jun 9, 2026 · 1 revision

HSEM Cost Function — Mathematical Reference

This document provides the complete mathematical formulation of the HSEM cost function (planner/cost_function.py). It is the source of truth for all cost calculations.


Two-aggregate architecture

The cost function returns two distinct aggregates for every plan:

Aggregate Symbol Contents Used for
total_cost $C_{total}$ Real money terms only Auditing, bill comparison
score $S$ $C_{total}$ + synthetic penalties + terminal SoC value Candidate selection

The selector picks the plan with the lowest score, not the lowest money cost.


Total cost (money terms)

$$ C_{total} = C_{import} - R_{export} + C_{cycle} + C_{loss} + C_{tariff} $$

Grid import cost

$$ C_{import} = \sum_{t \in slots} gi[t] \cdot p_{imp}[t] $$

Where $gi[t]$ is the actual grid import in kWh and $p_{imp}[t]$ is the import price in currency/kWh.

The cost function prices actual grid energy drawn, not stored energy. If the battery stores $x$ kWh and charge efficiency is $e$, the grid import is $x/e$, which includes conversion losses implicitly.

Export revenue

$$ R_{export} = \sum_{t \in slots} ge[t] \cdot p_{exp}[t] $$

Where $ge[t]$ is the grid export in kWh and $p_{exp}[t]$ is the export price.

Revenue is subtracted from total cost. Negative export prices (curtailment penalties) increase total cost.

Battery cycle cost (depreciation)

$$ C_{cycle} = \sum_{t \in slots} \max(charge[t], discharge[t]) \cdot c_{cycle} $$

Where $c_{cycle}$ is the cycle cost per kWh throughput.

The cycle cost counts the maximum of charge and discharge per slot, not their sum. This matches the MILP formulation where $m[t] = \max(ec[t], ed[t])$ and the 2× denominator in the cycle cost formula:

$$ c_{cycle} = \frac{purchase_price}{2 \cdot usable_kwh \cdot expected_cycles} $$

The 2× denominator accounts for one full round-trip (charge + discharge = 2 × usable_kwh throughput per cycle). With this factor, charging $x$ kWh and discharging $x$ kWh costs $c_{cycle} \cdot \max(x, x) = c_{cycle} \cdot x$, which equals $\frac{purchase_price \cdot x}{2 \cdot usable \cdot cycles}$ — matching the expected wear for moving $x$ kWh through the battery in one direction.

Conversion loss cost

$$ C_{loss} = \sum_{t \in slots} \frac{charge[t] + discharge[t]}{2} \cdot \frac{\eta_{loss}}{100} \cdot \frac{p_{imp}[t] + p_{exp}[t]}{2} $$

Where $\eta_{loss}$ is the round-trip conversion loss percentage.

The conversion loss term prices the energy lost as heat during charge/discharge at the average of import and export price — an opportunity-cost proxy.

When separate charge/discharge efficiencies are configured:

$$ \eta_{loss} = (1 - \eta_{chg} \cdot \eta_{dis}) \times 100 $$

Where $\eta_{chg}$ and $\eta_{dis}$ are efficiency fractions (e.g. 0.97).

Tariff cost

$$ C_{tariff} = \sum_{t \in slots} tariff[t] $$

An optional per-slot fixed tariff cost, typically zero unless the user configures grid tariff fees.


Score (selector objective)

$$ S = C_{total} + P_{soc} + P_{grid} + P_{override} + V_{terminal} $$

SoC penalties (quadratic guard)

$$ P_{soc} = \sum_{t \in slots} \begin{cases} w_{low} \cdot (soc_{min} - soc[t])^2 & \mathrm{if } soc[t] < soc_{min} \\ w_{high} \cdot (soc[t] - soc_{max})^2 & \mathrm{if } soc[t] > soc_{max} \\ 0 & \mathrm{otherwise} \end{cases} $$

These are soft guards — the SoC simulation already hard-clamps at hardware limits, so violations are rare. The quadratic form heavily penalises large deviations while tolerating tiny numerical rounding errors.

Past-slot exclusion: Slots with time_passed recommendation are excluded from SoC penalty calculation. The SoC simulator writes estimated_battery_soc = 0.0 as a sentinel on past slots, which would otherwise generate a false penalty of $w_{low} \cdot soc_{min}^2$ per past slot — identical across all candidates but log-misleading.

Grid limit penalty

$$ P_{grid} = \sum_{t \in slots} \max(0, \frac{|gi[t] - ge[t]|}{\Delta t} - L_{grid}) \cdot \Delta t \cdot w_{grid} $$

Where $\Delta t$ is slot duration in hours, $L_{grid}$ is the configured grid power limit in kW, and $w_{grid}$ is the penalty weight per excess kWh.

Override penalty

$$ P_{override} = N_{override} \cdot w_{override} $$

Where $N_{override}$ counts slots whose recommendation was forced by an override, and $w_{override}$ is the cost per override slot.

Terminal SoC value (opportunity cost)

$$ V_{terminal} = (E_{initial} - E_{final}) \cdot p_{replacement} $$

Where:

  • $E_{initial}$ = stored battery energy above the discharge floor at the start of the horizon (kWh)
  • $E_{final}$ = stored battery energy above the discharge floor at the end of the horizon (kWh)
  • $p_{replacement}$ = replacement price per kWh (minimum future import price)

Sign convention:

$$\begin{aligned} \Delta E &< 0 \mathrm{ (more energy at end)} \rightarrow V_{terminal} < 0 \mathrm{ (credit)} \\ \Delta E &> 0 \mathrm{ (less energy at end)} \rightarrow V_{terminal} > 0 \mathrm{ (penalty)} \end{aligned}$$

The replacement price uses the minimum future import price across the horizon because:

  • It represents the marginal cost of re-purchasing one stored kWh at the cheapest opportunity
  • Using the average (including expensive peak prices) over-values stored energy during high-price periods and biases against discharging

Past-slot exclusion rules

The cost function skips any slot whose recommendation is time_passed:

  • All energy-flow fields (grid_import_kwh, batteries_charged, etc.) are zero on past slots
  • Including them would only affect the SoC penalty (bogus $w_{low} \cdot soc_{min}^2$)
  • Skipping past slots does not change the winner (the bogus penalty is identical across candidates) but keeps the reported cost clean

Cost invariants (test assertions)

For every planner run:

  1. $C_{total} = C_{import} - R_{export} + C_{cycle} + C_{loss}$ (exact)
  2. No synthetic penalty enters $C_{total}$
  3. $S = C_{total} + P_{soc} + P_{grid} + P_{override} + V_{terminal}$ (exact)
  4. When all penalties = 0 and terminal-SoC is disabled: $S = C_{total}$
  5. Selector picks minimum $S$, not minimum $C_{total}$
  6. $score_{winner} = score_{final_output}$ (no post-selection mutation)
  7. Two identical plans, one ending with more stored energy → lower $V_{terminal}$ → lower $S$

HSEM Documentation

Quick Reference

Architecture Decision Records

Clone this wiki locally