Contracts¶
contracts ¶
Contract terms in YAML: guaranteed values, exclusions, allowances and liquidated damages (LD).
The KPI functions of res_asset_kpi.pv, res_asset_kpi.wind and
res_asset_kpi.bess measure; this module compares the measurements with what a contract
guarantees and prices the shortfall. Contract terms are kept in YAML so that they can be reviewed next to the agreement
they come from.
Warning
The example files shipped in res_asset_kpi/data (example_path) are
illustrative. They are not taken from any real agreement. Thresholds, exclusions, damages formulas and caps
differ from one contract to the next and must be checked against the actual contract, which always prevails. Nothing
here is legal advice.
Damages methods (guarantees.<name>.damages.method), with shortfall s = max(0, g_eff - measured):
| method | amount | typical use |
|---|---|---|
energy |
s x E_basis [MWh] x price_per_mwh |
PV or wind availability, PR |
per_point |
rate_per_pp x 100 s x multiplier |
BESS availability, RTE |
rte_energy_cost |
E_out x (1 / RTE - 1 / RTE_g) x price_per_mwh when RTE < RTE_g |
BESS RTE |
per_mwh |
rate_per_mwh x max(0, E_g - E_corr) on a failed test |
BESS capacity |
Each guarantee's damages can be capped per period (cap);
apply_annual_cap caps the total of a year. The guaranteed value can step
down with the contract year (annual_degradation, relative) and be reduced by a measurement tolerance: g_eff =
g (1 - d)^(year - 1) - tolerance.
ContractError ¶
Bases: ValueError
A contract file does not follow the expected structure.
Damages
dataclass
¶
Damages(method: Method = 'energy', price_per_mwh: float = 0.0, rate_per_pp: float = 0.0, multiplier: float = 1.0, rate_per_mwh: float = 0.0, cap: float | None = None)
Liquidated damages clause of one guarantee.
Bonus
dataclass
¶
Bonus(threshold: float, share: float = 0.0, price_per_mwh: float = 0.0)
Bonus above a threshold: (measured - threshold) x E_basis [MWh] x price_per_mwh x share.
Guarantee
dataclass
¶
Guarantee(guaranteed: float, basis: str = '', annual_degradation: float = 0.0, tolerance: float = 0.0, min_data_share: float = 0.0, damages: Damages = Damages(), bonus: Bonus | None = None, parameters: dict[str, Any] = dict())
One guaranteed KPI.
Attributes:
| Name | Type | Description |
|---|---|---|
guaranteed |
float
|
Guaranteed value in contract year 1 (a fraction for ratios, an energy for capacity). |
basis |
str
|
Which measurement is compared (e.g. |
annual_degradation, tolerance |
float
|
See |
min_data_share |
float
|
Below this share of valid data the test of a period is inconclusive. |
damages |
Damages
|
|
bonus |
Bonus or None
|
|
parameters |
dict
|
Asset-specific test settings (thresholds, windows, retention curves), passed through unchanged. |
Contract
dataclass
¶
Contract(name: str, asset: Literal['pv', 'wind', 'bess'], currency: str = 'EUR', period: str = 'year', contract_start: str | None = None, excluded_events: tuple[str, ...] = (), event_treatments: dict[str, str] = dict(), planned_allowance_h: float = 0.0, annual_cap: float | None = None, guarantees: dict[str, Guarantee] = dict())
Contract terms loaded from YAML (see the example files).
load_contract ¶
load_contract(source: str | Path | dict[str, Any]) -> Contract
Read and check contract terms from a YAML file or a dictionary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
(str, Path or dict)
|
Path of the YAML file, or its parsed content. |
required |
Returns:
| Type | Description |
|---|---|
Contract
|
|
Raises:
| Type | Description |
|---|---|
ContractError
|
Unknown keys, unknown damages methods or periods, or a missing guaranteed value. |
Examples:
>>> c = load_contract(example_path("pv_om_contract"))
>>> c.asset, c.guarantees["availability"].guaranteed
('pv', 0.98)
example_path ¶
example_path(name: str) -> Path
Path of an illustrative contract or mapping shipped with the package.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
|
required |
Examples:
>>> example_path("bess_ltsa").name
'bess_ltsa.example.yaml'
effective_guarantee ¶
effective_guarantee(guarantee: Guarantee, contract_year: int | ArrayLike = 1) -> np.ndarray
Guaranteed value in a contract year: g (1 - d)^(year - 1) - tolerance.
Examples:
>>> g = Guarantee(guaranteed=0.78, annual_degradation=0.005, tolerance=0.02)
>>> effective_guarantee(g, [1, 3]).round(5).tolist()
[0.76, 0.75222]
guaranteed_retention ¶
guaranteed_retention(years: ArrayLike, retention: ArrayLike, elapsed_years: ArrayLike, efc_total: ArrayLike | None = None, efc_per_year: float | None = None) -> np.ndarray
Guaranteed state of health from a warranty retention table, linear between points.
With efc_per_year, the table is read at the equivalent age max(years, EFC / efc_per_year), so that
cycling faster than the warranty allows ages the guarantee faster.
Examples:
>>> guaranteed_retention([0, 1, 2], [1.0, 0.97, 0.95], [1.5], efc_total=[730], efc_per_year=365).tolist()
[0.95]
damages_amount ¶
damages_amount(damages: Damages, measured: ArrayLike, guaranteed: ArrayLike, *, basis_energy_mwh: ArrayLike | None = None, e_out_mwh: ArrayLike | None = None) -> np.ndarray
Damages per period before any annual cap (see the table in the module description).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
damages
|
Damages
|
|
required |
measured
|
array - like
|
Measured and (effective) guaranteed values per period. |
required |
guaranteed
|
array - like
|
Measured and (effective) guaranteed values per period. |
required |
basis_energy_mwh
|
array - like
|
Energy the shortfall is priced on ( |
None
|
e_out_mwh
|
array - like
|
Discharged energy of each window ( |
None
|
Examples:
>>> d = Damages(method="energy", price_per_mwh=60.0, cap=20000.0)
>>> damages_amount(d, [0.975, 0.99], 0.98, basis_energy_mwh=[30000.0, 30000.0]).round(2).tolist()
[9000.0, 0.0]
assess ¶
assess(guarantee: Guarantee, measured: Series, *, contract_year: ArrayLike | int = 1, basis_energy_mwh: ArrayLike | None = None, e_out_mwh: ArrayLike | None = None, data_share: ArrayLike | None = None) -> pd.DataFrame
Compare measured values per period with a guarantee and price the shortfall.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
guarantee
|
Guarantee
|
|
required |
measured
|
Series
|
Measured value per period (index: period label). |
required |
contract_year
|
int or array - like
|
Contract year of each period, for the step-down of the guaranteed value. |
1
|
basis_energy_mwh
|
array - like
|
See |
None
|
e_out_mwh
|
array - like
|
See |
None
|
data_share
|
array - like
|
Share of valid data per period; below |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Examples:
>>> c = load_contract(example_path("pv_om_contract"))
>>> a = pd.Series({"2023": 0.975, "2024": 0.997})
>>> r = assess(c.guarantees["availability"], a, basis_energy_mwh=[30000.0, 31000.0])
>>> r[["passed", "damages", "bonus"]].round(1)
passed damages bonus
2023 False 9000.0 0.0
2024 True 0.0 1860.0
apply_annual_cap ¶
apply_annual_cap(damages: Series, years: ArrayLike, cap: float | None) -> pd.Series
Scale the damages of each year down so that their sum does not exceed cap.
Examples:
>>> d = pd.Series([300.0, 500.0, 100.0])
>>> apply_annual_cap(d, [2024, 2024, 2025], 400.0).tolist()
[150.0, 250.0, 100.0]