Skip to content

PV

pv

PV plants: performance ratio, availability and the budget-to-actual energy bridge.

performance

Performance ratio (PR), temperature-corrected PR and weather-corrected PR, and two cell temperature models.

Written from public descriptions of IEC 61724-1 and of Dierauf et al. (2013), NREL/TP-5200-57991; this is not a certified implementation of the standard. With E the AC energy, P_STC the DC rating at standard test conditions, G the plane-of-array irradiance, G_STC = 1000 W/m2, gamma the power temperature coefficient (1/K, negative) and T_c the cell temperature, summed over the intervals i used:

PR     = sum E_i / sum (P_STC G_i / G_STC) dt
PR_25  = sum E_i / sum (P_STC G_i / G_STC [1 + gamma (T_c,i - 25)]) dt
PR_wc  = sum E_i / sum (P_STC G_i / G_STC [1 + gamma (T_c,i - T_typ)]) dt

T_typ is the irradiance-weighted mean cell temperature of a typical year (e.g. the budget's TMY), so that the annual PR_wc stays close to the annual plain PR while the monthly values no longer swing with the seasons.

Intervals with a missing power or irradiance are left out of numerator and denominator alike. When cell temperatures are given, intervals without one are left out of all three ratios, so that the three describe the same intervals.

G_STC module-attribute

G_STC = 1000.0

Irradiance at standard test conditions, W/m2.

PRTerms dataclass

PRTerms(energy_kwh: float, insolation_kwh_m2: float, reference_kwh: float, reference_25_kwh: float, reference_wc_kwh: float, intervals: int)

Sums behind the PR family over a set of intervals (all energies in kWh).

Attributes:

Name Type Description
energy_kwh float

Measured AC energy.

insolation_kwh_m2 float

Plane-of-array irradiation H.

reference_kwh float

Reference energy P_STC H / G_STC (the reference yield times the DC rating).

reference_25_kwh float

Reference energy corrected to 25 degC cell temperature (NaN without temperatures).

reference_wc_kwh float

Reference energy corrected to T_typ (NaN without temperatures or T_typ).

intervals int

Number of intervals used.

pr property

pr: float

Plain performance ratio.

pr_temperature_corrected property

pr_temperature_corrected: float

Performance ratio corrected to 25 degC cell temperature.

pr_weather_corrected property

pr_weather_corrected: float

Weather-corrected performance ratio (cell temperature corrected to T_typ).

as_dict

as_dict() -> dict[str, float]

Sums and the three ratios as a flat dictionary.

pr_terms

pr_terms(p_ac_kw: ArrayLike, poa_wm2: ArrayLike, p_stc_kw: float, interval_h: float, *, t_cell_c: ArrayLike | None = None, gamma_per_k: float | None = None, t_typ_c: float | None = None, mask: ArrayLike | None = None) -> PRTerms

Sums for the performance ratios over the intervals selected by mask.

Parameters:

Name Type Description Default
p_ac_kw array - like

Mean AC power per interval, kW (at the inverters or the meter, as the contract defines).

required
poa_wm2 array - like

Mean plane-of-array irradiance per interval, W/m2.

required
p_stc_kw float

DC rating at STC, kWp.

required
interval_h float

Interval length, hours.

required
t_cell_c array - like

Cell temperature per interval, degC (see cell_temperature_from_module and cell_temperature_pvsyst). Needed for the corrected ratios.

None
gamma_per_k float

Power temperature coefficient, 1/K (e.g. -0.0037). Needed for the corrected ratios.

None
t_typ_c float

Irradiance-weighted typical cell temperature (typical_cell_temperature); needed for PR_wc.

None
mask array-like of bool

Intervals to use, e.g. irradiance above a threshold and no excluded event.

None

Returns:

Type Description
PRTerms

Examples:

One hour at 800 W/m2 and 50 degC cell temperature on a 100 kWp array delivering 68 kW:

>>> t = pr_terms([68.0], [800.0], 100.0, 1.0, t_cell_c=[50.0], gamma_per_k=-0.004, t_typ_c=40.0)
>>> round(t.pr, 4), round(t.pr_temperature_corrected, 4), round(t.pr_weather_corrected, 4)
(0.85, 0.9444, 0.8854)

performance_ratio

performance_ratio(p_ac_kw: ArrayLike, poa_wm2: ArrayLike, p_stc_kw: float, interval_h: float, mask: ArrayLike | None = None) -> float

Plain performance ratio, sum E / sum (P_STC G / G_STC) dt.

Examples:

>>> performance_ratio([40.0, 80.0], [500.0, 1000.0], 100.0, 0.25)
0.8

typical_cell_temperature

typical_cell_temperature(poa_wm2: ArrayLike, t_cell_c: ArrayLike, mask: ArrayLike | None = None) -> float

Irradiance-weighted mean cell temperature T_typ = sum G T_c / sum G (e.g. over a TMY).

Examples:

>>> typical_cell_temperature([200.0, 800.0], [20.0, 45.0])
40.0

pr_by_period

pr_by_period(frame: DataFrame, periods: ArrayLike, p_stc_kw: float, interval_h: float, *, gamma_per_k: float | None = None, t_typ_c: float | None = None, mask: ArrayLike | None = None) -> pd.DataFrame

PR, PR_25 and PR_wc per period label.

Parameters:

Name Type Description Default
frame DataFrame

Columns p_ac (kW), poa (W/m2) and optionally t_cell (degC), one row per interval.

required
periods array - like

Period label per row (see period_labels).

required
p_stc_kw float

As in pr_terms.

required
interval_h float

As in pr_terms.

required
gamma_per_k float

As in pr_terms.

required
t_typ_c float

As in pr_terms.

required
mask float

As in pr_terms.

required

Returns:

Type Description
DataFrame

One row per period with the columns of PRTerms.as_dict.

cell_temperature_from_module

cell_temperature_from_module(t_mod_c: ArrayLike, poa_wm2: ArrayLike, delta_t_c: float = 3.0) -> np.ndarray

Cell temperature from a back-of-module sensor: T_c = T_m + G / 1000 * dT (King et al., 2004).

Parameters:

Name Type Description Default
t_mod_c array - like

Back-of-module temperature, degC.

required
poa_wm2 array - like

Plane-of-array irradiance, W/m2.

required
delta_t_c float

Cell-to-back difference at 1000 W/m2 (3 K for glass/cell/polymer sheet modules on open racks in the Sandia array performance model).

3.0

Examples:

>>> cell_temperature_from_module([40.0], [500.0]).tolist()
[41.5]

cell_temperature_pvsyst

cell_temperature_pvsyst(poa_wm2: ArrayLike, t_amb_c: ArrayLike, wind_speed_ms: ArrayLike = 1.0, u_c: float = 29.0, u_v: float = 0.0, absorptance: float = 0.9, efficiency: float = 0.1) -> np.ndarray

Cell temperature with the PVsyst heat balance: T_c = T_a + alpha G (1 - eta) / (U_c + U_v v).

Parameters:

Name Type Description Default
poa_wm2 array - like

Plane-of-array irradiance, W/m2.

required
t_amb_c array - like

Ambient temperature, degC.

required
wind_speed_ms array - like

Wind speed, m/s.

1.0
u_c float

Constant and wind-dependent heat loss factors, W/(m2 K) and W s/(m3 K); 29 / 0 are the PVsyst defaults for free-standing arrays.

29.0 and 0.0
u_v float

Constant and wind-dependent heat loss factors, W/(m2 K) and W s/(m3 K); 29 / 0 are the PVsyst defaults for free-standing arrays.

29.0 and 0.0
absorptance float

Absorbed share of the irradiance and module efficiency.

0.9 and 0.1
efficiency float

Absorbed share of the irradiance and module efficiency.

0.9 and 0.1

Examples:

>>> cell_temperature_pvsyst([1000.0], [25.0]).round(3).tolist()
[52.931]

availability

Time-based and energy-based availability of a PV plant, weighted by capacity, with contractual exclusions.

Written from public descriptions of IEC 61724-1 and IEC TS 63019 and from typical O&M contract clauses; the treatment of each event class is a parameter, because contracts differ. For units u with capacity weights w_u (summing to 1) and intervals i of length dt:

counted_i,u  = daylight_i  and  state known  and  not excluded  and  not planned-within-allowance
A_time       = sum w_u [running and counted] / sum w_u [counted]
A_energy     = 1 - sum E_exp,i,u [stopped and counted] / sum E_exp,i,u [counted]
  • daylight: plane-of-array irradiance above a threshold (50 W/m2 by default);
  • excluded: periods outside the operator's responsibility (grid outage, grid-operator instruction, force majeure), removed from numerator and denominator;
  • planned maintenance: excluded until the capacity-weighted daylight hours of planned work in the period exceed an allowance, counted as downtime afterwards;
  • intervals without data on a unit are left out (unknown state).

The energy basis needs the expected power of each unit per interval (kW), e.g. from a performance model, after soiling, snow and clipping, so that a stopped inverter loses what it would have produced.

availability

availability(running: DataFrame, interval_h: float, *, capacity: Series | ArrayLike | None = None, poa_wm2: ArrayLike | None = None, daylight_wm2: float = 50.0, excluded: DataFrame | Series | ArrayLike | None = None, planned: DataFrame | Series | ArrayLike | None = None, planned_allowance_h: float = 0.0, expected_kw: DataFrame | ArrayLike | None = None, periods: ArrayLike | None = None) -> pd.DataFrame

Capacity-weighted time and energy availability per period.

Parameters:

Name Type Description Default
running DataFrame

One column per unit (inverter), one row per interval: True running, False stopped, NaN no data.

required
interval_h float

Interval length, hours.

required
capacity Series or array - like

Capacity of each unit (e.g. kWp DC), in the column order of running; equal weights by default.

None
poa_wm2 array - like

Plane-of-array irradiance per interval; intervals at or below daylight_wm2 are not counted. Without it every interval counts.

None
daylight_wm2 float

Daylight threshold, W/m2.

50.0
excluded DataFrame, Series or array-like of bool

Excluded intervals, per interval (whole plant) or per interval and unit.

None
planned DataFrame, Series or array-like of bool

Planned maintenance, per interval or per interval and unit.

None
planned_allowance_h float

Capacity-weighted daylight hours of planned maintenance excluded per period; the rest counts.

0.0
expected_kw DataFrame or array - like

Expected power per interval and unit (kW) for the energy basis.

None
periods array - like

Period label per interval (see period_labels); one period by default.

None

Returns:

Type Description
DataFrame

Per period: availability_time, availability_energy (NaN without expected_kw), counted_h and stopped_h (capacity-weighted hours), excluded_share (of the known daylight hours), planned_h, planned_counted_h, expected_kwh and lost_kwh.

Examples:

Two equal inverters over four daylight hours; inverter B is down for one hour:

>>> running = pd.DataFrame({"A": [True] * 4, "B": [True, False, True, True]})
>>> a = availability(running, 1.0, poa_wm2=[400.0] * 4, expected_kw=np.full((4, 2), 50.0))
>>> a[["availability_time", "availability_energy", "lost_kwh"]].round(3)
        availability_time  availability_energy  lost_kwh
period
all                 0.875                0.875      50.0

bridge

Energy bridge (waterfall) from a budget (e.g. P50) to the measured production, closing exactly.

Sequential method (loss accounting). The budget energy E_b is first re-stated for what was outside the operator's control, with ratios applied in order (e.g. irradiance, temperature, degradation):

bar_k = E_b r_1 ... r_(k-1) (r_k - 1)            E_w = E_b prod r_k

Then each loss k (soiling, snow, clipping, availability, curtailment, data gaps, ...) gets the bar budgeted loss minus measured loss, in kWh. The measured loss fractions l_k are applied in order to the potential of the actual plant X_a = E_act / prod (1 - l_k), the budgeted fractions b_k to the potential the budget implies for that weather X_b = E_w / prod (1 - b_k):

L_k = X_a prod_(j<k) (1 - l_j) l_k        B_k = X_b prod_(j<k) (1 - b_j) b_k        bar_k = B_k - L_k

The residual E_act - E_b - sum bars is what the measurements do not explain (model error, unmodelled losses, sensor level). By construction the bars add up to E_act - E_b exactly.

Shapley method. The same month written as a product E_act = E_b prod f_k of the weather ratios, the loss ratios (1 - l_k) / (1 - b_k) and the residual factor X_a / X_b; each bar is the Shapley value of the product game, which does not depend on the order of the factors. Its bars also sum to E_act - E_b, but no single bar equals a measured loss.

shapley_product

shapley_product(base: float, factors: ArrayLike) -> np.ndarray

Shapley values of the game v(S) = base * prod_(k in S) f_k.

They add up to base * (prod f - 1).

Parameters:

Name Type Description Default
base float

Value of the empty coalition (the budget energy).

required
factors array - like

Multiplicative factors.

required

Returns:

Type Description
ndarray

One value per factor.

Examples:

>>> shapley_product(100.0, [1.1, 0.9]).round(6).tolist()
[9.5, -10.5]

energy_bridge

energy_bridge(e_budget: ArrayLike, e_actual: ArrayLike, weather: Mapping[str, ArrayLike], losses_actual: Mapping[str, ArrayLike], losses_budget: Mapping[str, ArrayLike], *, method: str = 'sequential', min_keep: float = 0.05, index: Sequence[object] | Index | None = None) -> pd.DataFrame

Bars of the energy bridge from budget to actual, one row per period.

Parameters:

Name Type Description Default
e_budget float or array - like

Budget and measured energy per period (any energy unit, the same for both).

required
e_actual float or array - like

Budget and measured energy per period (any energy unit, the same for both).

required
weather mapping of str to float or array-like

Ordered ratios actual / budget of the conditions outside the operator's control, e.g. {"irradiance": r_irr, "temperature": r_T, "degradation": r_D}.

required
losses_actual mapping of str to float or array-like

Ordered measured loss fractions l_k (0..1), e.g. {"soiling": 0.021, "availability": 0.004}.

required
losses_budget mapping of str to float or array-like

Budgeted loss fractions b_k with the same keys (0 where the budget assumes none).

required
method ('sequential', 'shapley')

Allocation method (see the module description).

"sequential"
min_keep float

Smallest prod (1 - l_k) for which the actual potential is computed as E_act / prod (1 - l_k). In a period with less production left (e.g. a month-long outage) the measured losses are applied to the budget's potential instead, and a warning is logged.

0.05
index sequence

Period labels of the rows.

None

Returns:

Type Description
DataFrame

One column per weather step and loss, then residual; also e_budget, e_actual, e_weather (the budget re-stated for the weather), potential_actual, potential_budget and closure (e_actual - e_budget - sum of bars, zero up to rounding).

Examples:

>>> b = energy_bridge(1000.0, 905.0, {"irradiance": 0.95},
...                   {"soiling": 0.03, "availability": 0.01}, {"soiling": 0.02, "availability": 0.0})
>>> b[["irradiance", "soiling", "availability", "residual"]].round(2)
   irradiance  soiling  availability  residual
0       -50.0    -8.88         -9.14    -26.97
>>> abs(float(b["closure"].iloc[0])) < 1e-9
True