PyPSA / atlite

Atlite: A Lightweight Python Package for Calculating Renewable Power Potentials and Time Series
https://atlite.readthedocs.io
268 stars 90 forks source link

Wind Conversion: potential bug when power curve does not end with zero after cutout speed #314

Closed joAschauer closed 1 year ago

joAschauer commented 1 year ago

Description

Hi there,

I am not sure whether this is a bug or if I did get something wrong. In the convert_wind() function, you use np.interp() in order to fit the wind speeds from wnd_hub to the specific power output:

https://github.com/PyPSA/atlite/blob/549d0fdf22925e06ac3f726e801f215d993671dc/atlite/convert.py#L466-L467

I think this assumes that the last value in the power curve (i.e. the value corresponding to the highest wind speed) to be a zero in order to account for cutout at high wind speeds of a wind turbine correctly. By default, np.interp assumes fp[-1] for x > xp[-1] (documented in the "right" argument in np.interp). This will lead to overestimation of specific generation for high wind speed if the power curve does not end with zero. See the following example:

In [1]: import numpy as np
   ...: # example curve from https://github.com/PyPSA/atlite/blob/master/atlite/resources/windturbine/NREL_ReferenceTurbine_2019ORCost_12MW_offshore.yaml
   ...: V = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
   ...: POW = [0.0, 0.0, 0.0, 0.4, 1.141, 2.189, 3.581, 5.323, 7.579, 10.397, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0]
   ...: P = np.max(POW)
   ...: print("specific generation at 30 m/s:", np.interp(30, V, POW / P))
   ...:
specific generation at 30 m/s: 1.0

I noticed that most of the OEDB power curves do not end with a zero in the power curve values (only for 5 turbines in the OEDB table this is true). See below a list of all turbines in OEDB, where the power curve does not end with a zero:

In [2]: import requests
   ...: import pandas as pd
   ...: import ast
   ...: oedb_json = requests.get("https://openenergy-platform.org/api/v0/schema/supply/tables/wind_turbine_library/rows")
   ...: oedb = pd.DataFrame.from_dict(oedb_json.json())
   ...: 
   ...: for row in oedb[oedb["has_power_curve"]].itertuples():
   ...:     if list(ast.literal_eval(row.power_curve_values))[-1] != 0:
   ...:         print(f"id: {row.id}, manufacturer: {row.manufacturer}, name: {row.name}")
   ...: 
id: 0, manufacturer: Enercon, name: E-141/4200 EP 4
id: 1, manufacturer: Enercon, name: E-126/4200 EP4
id: 2, manufacturer: Enercon, name: E-101/3500 E2
id: 3, manufacturer: Enercon, name: E-115/3200
id: 5, manufacturer: Enercon, name: E-115/3000
id: 6, manufacturer: Enercon, name: E-82/3000
id: 7, manufacturer: Enercon, name: E-92/2350
id: 8, manufacturer: Enercon, name: E-82/2350
id: 9, manufacturer: Enercon, name: E-82/2300 E2
id: 10, manufacturer: Enercon, name: E-70/2300 E4
id: 11, manufacturer: Enercon, name: E-82/2000 E2
id: 12, manufacturer: Enercon, name: E-70/2000
id: 13, manufacturer: Enercon, name: E-53/800
id: 15, manufacturer: Nordex, name: None
id: 16, manufacturer: Nordex, name: None
id: 17, manufacturer: Nordex, name: None
id: 20, manufacturer: Nordex, name: None
id: 63, manufacturer: Siemens, name: SWT-3.3-130
id: 22, manufacturer: Nordex, name: None
id: 23, manufacturer: Nordex, name: None
id: 33, manufacturer: Vestas, name: V164-8.0 MW
id: 97, manufacturer: Senvion/REpower, name: MM92
id: 42, manufacturer: Vestas, name: V126-3.45 MW
id: 43, manufacturer: Vestas, name: V126-3.3 MW
id: 44, manufacturer: Vestas, name: V126-3.0 MW
id: 47, manufacturer: Vestas, name: V117-3.45 MW
id: 48, manufacturer: Vestas, name: V117-3.3 MW
id: 49, manufacturer: Vestas, name: V112-3.45 MW
id: 50, manufacturer: Vestas, name: V112-3.3 MW
id: 51, manufacturer: Vestas, name: V112-3.075 MW
id: 52, manufacturer: Vestas, name: V112-3.0 MW
id: 53, manufacturer: Vestas, name: V90-3.0 MW
id: 54, manufacturer: Vestas, name: V90-2.0 MW
id: 55, manufacturer: Vestas, name: V80-2.0 MW
id: 60, manufacturer: Siemens, name: SWT-3.6-130
id: 61, manufacturer: Siemens, name: SWT-3.6-120
id: 64, manufacturer: Siemens, name: SWT-3.2-113
id: 67, manufacturer: Siemens, name: SWT-3.15-142
id: 73, manufacturer: Siemens, name: SWT-2.3-113
id: 84, manufacturer: Senvion/REpower, name: 6.3M152
id: 86, manufacturer: Senvion/REpower, name: 6.2M126
id: 89, manufacturer: Senvion/REpower, name: 3.4M114
id: 90, manufacturer: Senvion/REpower, name: 3.4M104
id: 93, manufacturer: Senvion/REpower, name: 3.2M114 NES
id: 94, manufacturer: Senvion/REpower, name: 3.2M122
id: 95, manufacturer: Senvion/REpower, name: 3.0M122
id: 96, manufacturer: Senvion/REpower, name: MM100
id: 114, manufacturer: GE Wind, name: GE 3.2-130
id: 124, manufacturer: Eno, name: ENO 126 3.5
id: 116, manufacturer: GE Wind, name: GE 2.75-120
id: 117, manufacturer: GE Wind, name: GE 2.75-103
id: 119, manufacturer: GE Wind, name: GE 2.5-120
id: 127, manufacturer: Eno, name: ENO 114 3.5
id: 128, manufacturer: Eno, name: ENO 100
id: 131, manufacturer: Vestas, name: None
id: 132, manufacturer: Enercon, name: E-48 800
id: 133, manufacturer: Vestas, name: V90/2000 GS
id: 134, manufacturer: Vestas, name: V100/1800
id: 135, manufacturer: Vestas, name: V100/1800 GS
id: 136, manufacturer: Vestas, name: V117-3.6
id: 137, manufacturer: aerodyn, name: SCD 168 8000
id: 138, manufacturer: Enercon, name: E-126/7500
id: 140, manufacturer: IEA, name: IEA 15 MW offshore reference turbine

The same is true for some of the atlite-builtin power curves:

In [3]: import atlite
   ...: for turbine in atlite.windturbines.keys():
   ...:     cfg = atlite.resource.get_windturbineconfig(turbine)
   ...:     if cfg["POW"][-1] != 0:
   ...:         print(turbine)
   ...: 
NREL_ReferenceTurbine_2016CACost_10MW_offshore
NREL_ReferenceTurbine_2016CACost_6MW_offshore 
NREL_ReferenceTurbine_2016CACost_8MW_offshore 
NREL_ReferenceTurbine_2019ORCost_12MW_offshore
NREL_ReferenceTurbine_2019ORCost_15MW_offshore
NREL_ReferenceTurbine_2020ATB_12MW_offshore   
NREL_ReferenceTurbine_2020ATB_15MW_offshore   
NREL_ReferenceTurbine_2020ATB_18MW_offshore

Expected Behavior

I think atlite should do one of the two following options: 1) check if a high wind speed cutoff in the power curve is present and warn the user if this is not the case 2) by default assume a specific generation of 0 for wind speeds higher than the last value in the power curve

Potential Fixes

In case of option 2), one could either modify the get_windturbineconfig() function to append a zero to the end of the power curve or change the _interpolate() function to:

def _interpolate(da):
    return np.interp(da, V, POW / P, right=0)
euronion commented 1 year ago

Hi @joAschauer !

You're right, that is a problem and an old ghost coming back to haunt us (https://github.com/PyPSA/atlite/pull/10#issuecomment-468217178).

Back then we decided that a power curve should have a trailing 0 if it has a cutoff speed. If the power curve does not offer a 0 at the highest wind speed, it was considered not to have a cutoff speed. We obviously failed to uphold that convention, considering not all wind turbine models we ship with atlite adhere to it.

I would go with a mix of 1.) and 2.):

Do you think that's a sensible approach?