Skip to content

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. time or energy availability, pr_wc, incl_aux).

annual_degradation, tolerance float
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

pv_om_contract, wind_availability_warranty, bess_ltsa, mapping_wide or mapping_long.

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 (energy method), e.g. expected energy of the counted intervals for availability or the reference energy for PR.

None
e_out_mwh array - like

Discharged energy of each window (rte_energy_cost method).

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 damages_amount; also prices the bonus.

None
e_out_mwh array - like

See damages_amount; also prices the bonus.

None
data_share array - like

Share of valid data per period; below guarantee.min_data_share the result is inconclusive (no verdict, no damages).

None

Returns:

Type Description
DataFrame

measured, guaranteed_effective, shortfall, passed (True, False or None), damages and bonus per period.

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]