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.
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 |
reference_kwh |
float
|
Reference energy |
reference_25_kwh |
float
|
Reference energy corrected to 25 degC cell temperature (NaN without temperatures). |
reference_wc_kwh |
float
|
Reference energy corrected to |
intervals |
int
|
Number of intervals used. |
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
|
None
|
gamma_per_k
|
float
|
Power temperature coefficient, 1/K (e.g. |
None
|
t_typ_c
|
float
|
Irradiance-weighted typical cell temperature
( |
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 |
required |
periods
|
array - like
|
Period label per row (see |
required |
p_stc_kw
|
float
|
As in |
required |
interval_h
|
float
|
As in |
required |
gamma_per_k
|
float
|
As in |
required |
t_typ_c
|
float
|
As in |
required |
mask
|
float
|
As in |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per period with the columns of |
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: |
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 |
None
|
poa_wm2
|
array - like
|
Plane-of-array irradiance per interval; intervals at or below |
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 |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Per period: |
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.
|
required |
losses_actual
|
mapping of str to float or array-like
|
Ordered measured loss fractions |
required |
losses_budget
|
mapping of str to float or array-like
|
Budgeted loss fractions |
required |
method
|
('sequential', 'shapley')
|
Allocation method (see the module description). |
"sequential"
|
min_keep
|
float
|
Smallest |
0.05
|
index
|
sequence
|
Period labels of the rows. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One column per weather step and loss, then |
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