Skip to content

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.

as_dict

as_dict() -> dict[str, float]

Fields as a dictionary.

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 unknown.

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 "all" by default.

None
unknown str

Category of intervals without one: the conservative choice counts them against the turbine.

"forced_outage"

Returns:

Type Description
DataFrame

group, category, hours, lost_kwh, actual_kwh for every category of CATEGORIES (zeros included).

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

category, hours and optionally lost_kwh and actual_kwh (rows of one group of category_totals).

required
contract str or mapping

Preset name from CONTRACTS or a mapping category -> available / unavailable / excluded covering every category present.

"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) / 2 with F(V) = 1 - exp(-pi / 4 (V / V_ave)^2), the curve held at its last value up to cut-out.

RHO_0 module-attribute

RHO_0 = 1.225

Reference air density, kg/m3.

R_DRY module-attribute

R_DRY = 287.05

Gas constant of dry air, J/(kg K).

BIN_WIDTH module-attribute

BIN_WIDTH = 0.5

Width of the wind speed bins, m/s.

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 ws (mean wind speed), power_kw (mean power), std_kw, n and u_a_kw (statistical uncertainty of the bin mean, std / sqrt(n)).

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 bin_power_curve.

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 ws and power_kw columns of bin_power_curve.

required
curve_kw array - like

Power curve (m/s, kW), e.g. the ws and power_kw columns of bin_power_curve.

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 age t, 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.

HOURS_PER_YEAR module-attribute

HOURS_PER_YEAR = 8766.0

Hours in an average year (365.25 days).

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 beta = 1.

laplace_u, p_laplace float

Laplace statistic and its two-sided p-value on the observed-time clock.

verdict property

verdict: str

improving, deteriorating or no significant trend at 5 % (likelihood-ratio test).

laplace_agrees property

laplace_agrees: bool

The Laplace test is significant at 5 % in the direction of the verdict.

intensity

intensity(age_h: ArrayLike) -> np.ndarray

Failures per hour per unit at unit age age_h.

as_dict

as_dict() -> dict[str, object]

Fields and derived values as a flat dictionary.

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: unit_col, start, end (periods with valid logging).

required
stops DataFrame

Every stop (failures and others): unit_col, start, end.

required
unit_col str
"turbine_id"

Returns:

Type Description
DataFrame

unit_col, start, end, hours and window (position of the observation window within the unit).

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 duration_h (restore time) and the grouping columns.

required
operating_h Series

Operating hours per unit (index: unit id), e.g. operating_time(...).groupby(unit_col)["hours"].sum().

required
by str, sequence of str or None

Grouping. When it contains unit_col, each group uses its unit's operating hours and every unit gets a row (also without failures); otherwise (e.g. by component, or None for the fleet) the operating hours of all units are used, since every unit carries every component.

"turbine_id"
unit_col str
"turbine_id"
conf float

Confidence level of the MTBF interval.

0.9

Returns:

Type Description
DataFrame

failures, operating_h, mtbf_h, mtbf_lo_h, mtbf_hi_h, failures_per_unit_year, mttr_h, ttr_median_h, ttr_p90_h, downtime_h, sorted by downtime.

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

unit_col and start of every failure.

required
windows DataFrame

Observation windows: unit_col, start, end.

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 beta.

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

U (positive: events cluster late) and the two-sided p-value; NaN without events.

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.