Wind¶
wind ¶
Wind turbines: availability by IEC 61400-26 categories, power curves (IEC 61400-12-1 binning), reliability.
availability ¶
Time-based and energy-based availability of wind turbines from the information categories of IEC 61400-26.
Written from public descriptions of IEC 61400-26-1 (time-based) and IEC 61400-26-2 (production-based availability); not a certified implementation. Every interval of every turbine carries one category; a contract says for each category whether its time counts as available, unavailable or is excluded (removed from numerator and denominator alike):
A_time = T_available / (T_available + T_unavailable)
A_energy = 1 - E_lost,unavailable / (E_actual + E_lost) over the categories not excluded
A maintenance allowance (hours per turbine and year) moves scheduled maintenance from unavailable to excluded until it is used up; its lost energy is excused in proportion.
The category list follows the top levels of the standard's information model; the standard's tree is finer (e.g. "out of environmental specification" covers low wind, high wind, icing and temperature). The two contract presets are illustrations, not readings of any agreement.
CATEGORIES
module-attribute
¶
CATEGORIES: dict[str, str] = {'full_performance': 'Full performance', 'partial_performance': 'Partial performance (technical derating)', 'technical_standby': 'Technical standby', 'environmental': 'Out of environmental specification', 'requested': 'Requested shutdown or curtailment (grid operator or owner)', 'electrical': 'Out of electrical specification (grid)', 'scheduled_maintenance': 'Scheduled maintenance', 'planned_corrective': 'Planned corrective action', 'forced_outage': 'Forced outage', 'no_info': 'Information unavailable'}
Information categories used here, with their descriptions.
CONTRACTS
module-attribute
¶
CONTRACTS: dict[str, dict[str, str]] = {'iec_operational': {'full_performance': AVAILABLE, 'partial_performance': AVAILABLE, 'technical_standby': AVAILABLE, 'environmental': AVAILABLE, 'requested': AVAILABLE, 'electrical': AVAILABLE, 'scheduled_maintenance': UNAVAILABLE, 'planned_corrective': UNAVAILABLE, 'forced_outage': UNAVAILABLE, 'no_info': EXCLUDED}, 'oem_warranty': {'full_performance': AVAILABLE, 'partial_performance': AVAILABLE, 'technical_standby': AVAILABLE, 'environmental': EXCLUDED, 'requested': EXCLUDED, 'electrical': EXCLUDED, 'scheduled_maintenance': UNAVAILABLE, 'planned_corrective': UNAVAILABLE, 'forced_outage': UNAVAILABLE, 'no_info': AVAILABLE}}
Illustrative treatments: iec_operational reads the information model from the operator's side (every
operative state available, missing information excluded); oem_warranty is a typical manufacturer guarantee
(grid, environment and requested stops excused, missing information counted as available).
MAINTENANCE_ALLOWANCE_H
module-attribute
¶
MAINTENANCE_ALLOWANCE_H: dict[str, float] = {'iec_operational': 0.0, 'oem_warranty': 60.0}
Illustrative scheduled-maintenance allowances, hours per turbine and year.
WindAvailability
dataclass
¶
WindAvailability(time: float, energy: float, available_h: float, unavailable_h: float, excused_h: float, lost_unavailable_kwh: float, lost_total_kwh: float, potential_kwh: float)
Availability of one group (a turbine-year, a farm-month, ...).
Attributes:
| Name | Type | Description |
|---|---|---|
time, energy |
float
|
Time-based and energy-based availability. |
available_h, unavailable_h, excused_h |
float
|
Hours counted available, unavailable, and scheduled maintenance excused by the allowance. |
lost_unavailable_kwh, lost_total_kwh, potential_kwh |
float
|
Lost energy of unavailable categories (after the allowance), of all categories, and the potential (actual plus lost) of the categories not excluded. |
category_totals ¶
category_totals(category: ArrayLike, interval_h: float, lost_kwh: ArrayLike | None = None, actual_kwh: ArrayLike | None = None, groups: ArrayLike | None = None, unknown: str = 'forced_outage') -> pd.DataFrame
Hours, lost and actual energy per (group, category).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
category
|
array-like of str
|
Category of every interval (any shape, e.g. time x turbine); missing values become |
required |
interval_h
|
float
|
Interval length, hours. |
required |
lost_kwh
|
array - like
|
Lost energy (expected minus actual, during stops and derates) and actual energy per interval; negative actual energy (own consumption) counts as zero. |
None
|
actual_kwh
|
array - like
|
Lost energy (expected minus actual, during stops and derates) and actual energy per interval; negative actual energy (own consumption) counts as zero. |
None
|
groups
|
array - like
|
Group label of every interval (same shape), e.g. turbine-year; one group |
None
|
unknown
|
str
|
Category of intervals without one: the conservative choice counts them against the turbine. |
"forced_outage"
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Examples:
>>> t = category_totals(["full_performance"] * 5 + ["forced_outage"], 1 / 6)
>>> t.set_index("category").loc[["full_performance", "forced_outage"], "hours"].round(4).tolist()
[0.8333, 0.1667]
availability ¶
availability(totals: DataFrame, contract: str | Mapping[str, str] = 'iec_operational', allowance_h: float = 0.0, n_turbine_years: float = 1.0) -> WindAvailability
Time-based and energy-based availability from category totals of one group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
totals
|
DataFrame
|
|
required |
contract
|
str or mapping
|
Preset name from |
"iec_operational"
|
allowance_h
|
float
|
Scheduled maintenance allowance, hours per turbine and year. |
0.0
|
n_turbine_years
|
float
|
Turbine-years the totals cover (scales the allowance). |
1.0
|
Returns:
| Type | Description |
|---|---|
WindAvailability
|
|
Examples:
A turbine-year with 8600 h in operation, 100 h of faults and 60 h of scheduled maintenance:
>>> tot = pd.DataFrame({"category": ["full_performance", "forced_outage", "scheduled_maintenance"],
... "hours": [8600.0, 100.0, 60.0]})
>>> round(availability(tot, "iec_operational").time, 4)
0.9817
>>> round(availability(tot, "oem_warranty", allowance_h=60.0).time, 4)
0.9885
remap ¶
remap(category: ArrayLike, mapping: Mapping[str, str]) -> np.ndarray
Re-label categories, e.g. to quantify what a misclassification does to contractual availability.
Examples:
>>> remap(["requested", "forced_outage"], {"requested": "forced_outage"}).tolist()
['forced_outage', 'forced_outage']
power_curve ¶
Power curves by the method of bins, following the description of IEC 61400-12-1, and AEP for a Rayleigh wind.
Written from public descriptions of the standard; not a certified implementation. The standard measures the wind with a met mast in front of the turbine; SCADA-based analyses use the nacelle anemometer instead, so the absolute curve carries that anemometer's bias, while comparisons between turbines and over time remain valid.
- Air density from temperature and pressure (dry air; pressure from the ISO standard atmosphere at the site
elevation when it is not measured):
rho = p / (R_d T). - Normalisation of the wind speed to the reference density, for pitch-regulated turbines:
V_n = V (rho / rho_0)^(1/3). - Bins of 0.5 m/s centred on multiples of 0.5 m/s; mean wind speed and power per bin, the bin's statistical
(category A) uncertainty
s / sqrt(n); bins with fewer than three 10-minute records are dropped. - AEP for a Rayleigh distribution of mean
V_ave:AEP = N_h sum (F(V_i) - F(V_(i-1))) (P_(i-1) + P_i) / 2withF(V) = 1 - exp(-pi / 4 (V / V_ave)^2), the curve held at its last value up to cut-out.
standard_pressure ¶
standard_pressure(elevation_m: ArrayLike) -> np.ndarray
Air pressure of the ISO standard atmosphere at a height above sea level, Pa.
Examples:
>>> round(float(standard_pressure(0.0)), 1), round(float(standard_pressure(500.0)), 0)
(101325.0, 95461.0)
air_density ¶
air_density(temp_c: ArrayLike, pressure_pa: ArrayLike | None = None, elevation_m: ArrayLike | None = None) -> np.ndarray
Dry-air density, kg/m3, from temperature and pressure (or the standard pressure at an elevation).
Humidity, which the standard also corrects for, changes the density by well under 1 % at temperate temperatures and is ignored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
temp_c
|
array - like
|
Air temperature, degC. |
required |
pressure_pa
|
array - like
|
Air pressure, Pa. |
None
|
elevation_m
|
array - like
|
Height above sea level, used when the pressure is not given. |
None
|
Examples:
>>> round(float(air_density(15.0, 101325.0)), 4)
1.225
normalise_wind_speed ¶
normalise_wind_speed(wind_speed_ms: ArrayLike, rho: ArrayLike, rho_ref: float = RHO_0) -> np.ndarray
Wind speed normalised to the reference density, V (rho / rho_ref)^(1/3).
Examples:
>>> round(float(normalise_wind_speed(10.0, 1.15)), 4)
9.7916
bin_power_curve ¶
bin_power_curve(wind_speed_ms: ArrayLike, power_kw: ArrayLike, bin_width: float = BIN_WIDTH, min_count: int = 3) -> pd.DataFrame
Power curve by the method of bins.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wind_speed_ms
|
array - like
|
Normalised 10-minute mean wind speeds, m/s (normal operation only). |
required |
power_kw
|
array - like
|
10-minute mean power, kW. |
required |
bin_width
|
float
|
Bin width; bins are centred on its multiples. |
0.5
|
min_count
|
int
|
Bins with fewer records are dropped (three 10-minute records, the minimum of the standard). |
3
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Index: bin centre; columns |
Examples:
>>> c = bin_power_curve([4.9, 5.0, 5.1, 5.3, 7.0], [100.0, 110.0, 120.0, 130.0, 400.0])
>>> c.index.tolist(), c["power_kw"].tolist(), c["n"].tolist()
([5.0], [110.0], [3])
database_complete ¶
database_complete(curve: DataFrame, rated_kw: float, cut_in_ms: float = 3.0, step_h: float = 1 / 6, bin_width: float = BIN_WIDTH) -> bool
Completeness of a binned data set in the sense of the standard.
Every bin from 1 m/s below cut-in to 1.5 times the wind speed at 85 % of rated power is present, and the data cover at least 180 hours.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
curve
|
DataFrame
|
Output of |
required |
rated_kw
|
float
|
Rated power. |
required |
cut_in_ms
|
float
|
Cut-in wind speed. |
3.0
|
step_h
|
float
|
Length of one record, hours. |
1/6
|
bin_width
|
float
|
Bin width used for the curve. |
0.5
|
Examples:
>>> ws = np.repeat(np.arange(2.0, 14.01, 0.5), 60)
>>> curve = bin_power_curve(ws, np.clip((ws - 3.0) * 300.0, 0.0, 2000.0))
>>> database_complete(curve, rated_kw=2000.0)
True
curve_power ¶
curve_power(curve_ws: ArrayLike, curve_kw: ArrayLike, wind_speed_ms: ArrayLike, cut_out_ms: float = 25.0) -> np.ndarray
Power from a curve at any wind speed: linear in between, last value held up to cut-out, 0 outside.
Examples:
>>> curve_power([3.0, 4.0, 5.0], [0.0, 100.0, 300.0], [2.0, 4.5, 20.0, 26.0]).tolist()
[0.0, 200.0, 300.0, 0.0]
aep_rayleigh ¶
aep_rayleigh(curve_ws: ArrayLike, curve_kw: ArrayLike, mean_ws: float, cut_out_ms: float = 25.0, step: float = 0.1, availability: float = 1.0) -> float
Annual energy production for a Rayleigh wind speed distribution, MWh.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
curve_ws
|
array - like
|
Power curve (m/s, kW), e.g. the |
required |
curve_kw
|
array - like
|
Power curve (m/s, kW), e.g. the |
required |
mean_ws
|
float
|
Annual mean wind speed at hub height, m/s. |
required |
cut_out_ms
|
float
|
The curve is extended with its last value up to cut-out ("AEP-extrapolated"). |
25.0
|
step
|
float
|
Integration step, m/s. |
0.1
|
availability
|
float
|
Multiplier for an assumed availability. |
1.0
|
Examples:
A flat 1000 kW curve produces 8760 MWh less the hours above cut-out (exp(-pi/4 (25/7)^2) of the year):
>>> round(aep_rayleigh([0.0, 30.0], [1000.0, 1000.0], 7.0), 1)
8759.6
reliability ¶
Reliability indicators from failure records: MTBF and MTTR with confidence bounds, Crow-AMSAA and Laplace.
Definitions:
- Operating time - observation time minus all stop time (a failure cannot start while the unit is stopped),
see
operating_time. -
MTBF - operating hours / failures, with the two-sided chi-square interval of a time-truncated test:
MTBF_lo = 2 T / chi2(1 - a/2; 2n + 2) MTBF_hi = 2 T / chi2(a/2; 2n)
-
MTTR - mean time to restore: from the stop to the restart, so it includes waiting, travel and logistics, not only hands-on repair time.
-
Crow-AMSAA - the power-law non-homogeneous Poisson process with intensity
u(t) = lambda beta t^(beta - 1)in unit aget, fitted by maximum likelihood over all units and all their observation windows(S_j, T_j](late starts and gaps handled exactly):ln L = n ln(lambda) + n ln(beta) + (beta - 1) sum ln t_i - lambda sum (T_j^beta - S_j^beta)
beta < 1: failures thin out (reliability grows), beta > 1: they become more frequent. The verdict
comes from the likelihood-ratio test of beta = 1; the bounds of beta from the profile likelihood.
* Laplace test - model-free trend test: under a constant rate every failure time is uniform in its window, U = sum
(t_i - (S_i + L_i / 2)) / sqrt(sum L_i^2 / 12) is standard normal; U > 0 means failures cluster late.
laplace_observed applies it on each unit's observed-time clock
(its windows joined end to end), so that it does not depend on the choice of the age origin.
CrowAMSAA
dataclass
¶
CrowAMSAA(beta: float, lam: float, beta_lo: float, beta_hi: float, failures: int, units: int, hours: float, p_trend: float, laplace_u: float, p_laplace: float)
Result of a Crow-AMSAA (power-law NHPP) fit.
Attributes:
| Name | Type | Description |
|---|---|---|
beta, beta_lo, beta_hi |
float
|
Shape and its profile-likelihood bounds. |
lam |
float
|
Scale, per hour**beta. |
failures, units |
int
|
Number of failures and of units. |
hours |
float
|
Observed unit-hours. |
p_trend |
float
|
p-value of the likelihood-ratio test of |
laplace_u, p_laplace |
float
|
Laplace statistic and its two-sided p-value on the observed-time clock. |
operating_time ¶
operating_time(windows: DataFrame, stops: DataFrame, unit_col: str = 'turbine_id') -> pd.DataFrame
Operating intervals per unit: observation windows minus all stops.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
windows
|
DataFrame
|
Observation windows: |
required |
stops
|
DataFrame
|
Every stop (failures and others): |
required |
unit_col
|
str
|
|
"turbine_id"
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Examples:
>>> w = pd.DataFrame({"turbine_id": ["T1"], "start": ["2024-01-01"], "end": ["2024-01-11"]})
>>> s = pd.DataFrame({"turbine_id": ["T1"], "start": ["2024-01-02"], "end": ["2024-01-03"]})
>>> float(operating_time(w, s)["hours"].sum())
216.0
mtbf_interval ¶
mtbf_interval(hours: float, failures: int, conf: float = 0.9) -> tuple[float, float]
Two-sided chi-square confidence interval of the MTBF for a time-truncated observation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hours
|
float
|
Operating hours. |
required |
failures
|
int
|
Number of failures. |
required |
conf
|
float
|
Confidence level. |
0.9
|
Returns:
| Type | Description |
|---|---|
tuple of float
|
Lower and upper bound, hours (upper is infinite without failures). |
Examples:
>>> lo, hi = mtbf_interval(1000.0, 4)
>>> round(lo, 1), round(hi, 1)
(109.2, 731.9)
mtbf_mttr ¶
mtbf_mttr(failures: DataFrame, operating_h: Series, by: str | Sequence[str] | None = 'turbine_id', unit_col: str = 'turbine_id', conf: float = 0.9) -> pd.DataFrame
Failures, MTBF with its interval, failure rate and restore-time statistics per group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failures
|
DataFrame
|
One row per failure with |
required |
operating_h
|
Series
|
Operating hours per unit (index: unit id), e.g. |
required |
by
|
str, sequence of str or None
|
Grouping. When it contains |
"turbine_id"
|
unit_col
|
str
|
|
"turbine_id"
|
conf
|
float
|
Confidence level of the MTBF interval. |
0.9
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Examples:
>>> f = pd.DataFrame({"turbine_id": ["T1", "T1", "T2"], "duration_h": [2.0, 6.0, 1.0]})
>>> up = pd.Series({"T1": 4000.0, "T2": 4380.0})
>>> mtbf_mttr(f, up)[["turbine_id", "failures", "mtbf_h", "mttr_h"]]
turbine_id failures mtbf_h mttr_h
0 T1 2 2000.0 4.0
1 T2 1 4380.0 1.0
crow_amsaa ¶
crow_amsaa(failures: DataFrame, windows: DataFrame, origin: Mapping[object, object] | None = None, unit_col: str = 'turbine_id', conf: float = 0.9) -> CrowAMSAA
Fit the power-law NHPP to the failures of several units over their observation windows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failures
|
DataFrame
|
|
required |
windows
|
DataFrame
|
Observation windows: |
required |
origin
|
mapping
|
Age origin per unit (e.g. commissioning date); default: the start of the unit's first window. |
None
|
unit_col
|
str
|
|
"turbine_id"
|
conf
|
float
|
Confidence level of the bounds of |
0.9
|
Returns:
| Type | Description |
|---|---|
CrowAMSAA
|
|
Examples:
Failures that thin out over 4000 hours of one unit:
>>> start = pd.Timestamp("2024-01-01", tz="UTC")
>>> hours = [10, 30, 70, 150, 300, 600, 1100, 1900, 3200]
>>> f = pd.DataFrame({"turbine_id": "T1", "start": [start + pd.Timedelta(hours=h) for h in hours]})
>>> w = pd.DataFrame({"turbine_id": ["T1"], "start": [start], "end": [start + pd.Timedelta(hours=4000)]})
>>> ca = crow_amsaa(f, w)
>>> round(ca.beta, 3), ca.verdict
(0.361, 'improving')
laplace_test ¶
laplace_test(t: ArrayLike, s: ArrayLike, e: ArrayLike) -> tuple[float, float]
Laplace trend statistic for event times t, each observed in its own window (s, e).
Returns:
| Type | Description |
|---|---|
tuple of float
|
|
Examples:
>>> u, p = laplace_test([90.0, 95.0, 99.0], [0.0, 0.0, 0.0], [100.0, 100.0, 100.0])
>>> round(u, 3), round(p, 4)
(2.68, 0.0074)
laplace_observed ¶
laplace_observed(failures: DataFrame, windows: DataFrame, unit_col: str = 'turbine_id') -> tuple[float, float]
Laplace test on each unit's observed-time clock (its observation windows joined end to end).
Gaps then shorten the clock instead of splitting it, so the test looks at the long-term trend and not at what happens around each gap.