detquantlib 3.15.1__tar.gz → 5.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {detquantlib-3.15.1 → detquantlib-5.0.0}/PKG-INFO +1 -1
- detquantlib-5.0.0/detquantlib/assets/__init__.py +4 -0
- detquantlib-5.0.0/detquantlib/assets/wind_turbine.py +345 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/dates/dates.py +4 -65
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/tradable_products/tradable_products.py +83 -24
- {detquantlib-3.15.1 → detquantlib-5.0.0}/pyproject.toml +1 -1
- detquantlib-3.15.1/detquantlib/assets/__init__.py +0 -4
- detquantlib-3.15.1/detquantlib/assets/wind_turbine.py +0 -92
- {detquantlib-3.15.1 → detquantlib-5.0.0}/LICENSE.txt +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/README.md +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/assets/battery.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/converters/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/converters/definitions.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/converters/energy.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/converters/helpers.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/converters/price.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/data/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/data/databases/detdatabase.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/data/databases/helpers.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/forecasting/forecasting.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/stats/data_analysis.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/tradable_products/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/utils/__init__.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/utils/logging.py +0 -0
- {detquantlib-3.15.1 → detquantlib-5.0.0}/detquantlib/utils/utils.py +0 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Third-party packages
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from scipy.interpolate import CubicSpline
|
|
5
|
+
|
|
6
|
+
# Internal modules
|
|
7
|
+
from detquantlib.converters import power_to_energy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WindTurbine:
|
|
11
|
+
"""A class to store wind turbine information and perform wind turbine-related calculations."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
rated_power_mw: float,
|
|
16
|
+
cut_in: float,
|
|
17
|
+
cut_out: float,
|
|
18
|
+
nr_turbines: int,
|
|
19
|
+
power_curve: CubicSpline = None,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Constructor method.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
rated_power_mw: Rated power of a single wind turbine (in MW)
|
|
26
|
+
cut_in: Cut-in wind speed
|
|
27
|
+
cut_out: Cut-out wind speed
|
|
28
|
+
nr_turbines: Number of wind turbines in the wind farm
|
|
29
|
+
power_curve: Power curve of a single wind turbine, stored as a cubic spline object.
|
|
30
|
+
If not provided, can be set via the 'fit_power_curve()' method below.
|
|
31
|
+
"""
|
|
32
|
+
self.rated_power_mw = rated_power_mw
|
|
33
|
+
self.cut_in = cut_in
|
|
34
|
+
self.cut_out = cut_out
|
|
35
|
+
self.nr_turbines = nr_turbines
|
|
36
|
+
self.power_curve = power_curve
|
|
37
|
+
|
|
38
|
+
def fit_power_curve(
|
|
39
|
+
self, wind_speed: list | np.ndarray | pd.Series, power_mw: list | np.ndarray | pd.Series
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Fits the wind turbine's power curve with a cubic spline, and stores the resulting spline
|
|
43
|
+
object in attribute self.power_curve.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
wind_speed: Wind speeds
|
|
47
|
+
power_mw: Corresponding power in MW (for a single wind turbine)
|
|
48
|
+
"""
|
|
49
|
+
# Make sure input data is stored as numpy array
|
|
50
|
+
wind_speed, power_mw = np.array(wind_speed), np.array(power_mw)
|
|
51
|
+
|
|
52
|
+
# Trim data to cut-in - cut-out interval
|
|
53
|
+
idx = (wind_speed >= self.cut_in) & (wind_speed <= self.cut_out)
|
|
54
|
+
wind_speed_trim, power_trim = wind_speed[idx], power_mw[idx]
|
|
55
|
+
|
|
56
|
+
# Add data point with power=0.0 before cut-in to ensure smooth behaviour of spline
|
|
57
|
+
# interpolation
|
|
58
|
+
w0 = wind_speed_trim[0] - np.diff(wind_speed_trim[:2])
|
|
59
|
+
p0 = [0]
|
|
60
|
+
wind_speed_trim, power_trim = np.concat([w0, wind_speed_trim]), np.concat([p0, power_trim])
|
|
61
|
+
|
|
62
|
+
# Fit cubic spline
|
|
63
|
+
cs = CubicSpline(wind_speed_trim, power_trim)
|
|
64
|
+
|
|
65
|
+
# Store spline object
|
|
66
|
+
self.power_curve = cs
|
|
67
|
+
|
|
68
|
+
def wind_to_power(self, wind_speed: list | np.ndarray | pd.Series) -> np.ndarray:
|
|
69
|
+
"""
|
|
70
|
+
Converts wind speeds to generated power, aggregated over the total number of turbines.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
wind_speed: Wind speeds
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Corresponding power in MW (total over the number of turbines set in the object)
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: Raises an error if self.power_curve is None.
|
|
80
|
+
"""
|
|
81
|
+
# Check that power curve attribute is defined
|
|
82
|
+
if self.power_curve is None:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Cannot execute method self.wind_to_power() if attribute self.power_curve is "
|
|
85
|
+
"None. Use method self.fit_power_curve() to set self.power_curve based on power "
|
|
86
|
+
"curve data."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Make sure input data is stored as numpy array
|
|
90
|
+
wind_speed = np.array(wind_speed)
|
|
91
|
+
|
|
92
|
+
# Cubic spline interpolation
|
|
93
|
+
power_mw = self.power_curve(wind_speed)
|
|
94
|
+
|
|
95
|
+
# Cap to rated power
|
|
96
|
+
power_mw = np.minimum(power_mw, self.rated_power_mw)
|
|
97
|
+
|
|
98
|
+
# Adjust values outside of cut-in - cut-out bounds
|
|
99
|
+
idx = (wind_speed < self.cut_in) | (wind_speed > self.cut_out)
|
|
100
|
+
power_mw[idx] = 0.0
|
|
101
|
+
|
|
102
|
+
# Rescale power to number of wind turbines
|
|
103
|
+
power_mw *= self.nr_turbines
|
|
104
|
+
|
|
105
|
+
return power_mw
|
|
106
|
+
|
|
107
|
+
def wind_to_energy(self, wind_speed: list | np.ndarray | pd.Series, tenor: str) -> np.ndarray:
|
|
108
|
+
"""
|
|
109
|
+
Converts wind speeds to generated energy, aggregated over the total number of turbines.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
wind_speed: Wind speeds
|
|
113
|
+
tenor: Product tenor
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Corresponding energy in MWh (total over the number of turbines set in the object)
|
|
117
|
+
"""
|
|
118
|
+
power_mw = self.wind_to_power(wind_speed)
|
|
119
|
+
energy_mwh = power_to_energy(
|
|
120
|
+
power=power_mw, power_unit="MW", energy_unit="MWh", tenor=tenor
|
|
121
|
+
)
|
|
122
|
+
return energy_mwh
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def build_from_model(model: dict, nr_turbines: int):
|
|
126
|
+
"""
|
|
127
|
+
Builds a WindTurbine object from a known wind turbine model.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
model: Wind turbine model. The dictionary can be taken from the list of models in
|
|
131
|
+
the WindTurbineModels class. Otherwise, the dictionary should have the same
|
|
132
|
+
structure as those in the WindTurbineModels class.
|
|
133
|
+
nr_turbines: Number of wind turbines in the wind farm
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
WindTurbine object
|
|
137
|
+
"""
|
|
138
|
+
# Create object
|
|
139
|
+
obj = WindTurbine(
|
|
140
|
+
rated_power_mw=model["rated_power_mw"],
|
|
141
|
+
cut_in=model["cut_in"],
|
|
142
|
+
cut_out=model["cut_out"],
|
|
143
|
+
nr_turbines=nr_turbines,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Set power_curve attribute
|
|
147
|
+
obj.fit_power_curve(
|
|
148
|
+
wind_speed=[d["wind_speed_(m/s)"] for d in model["power_curve"]],
|
|
149
|
+
power_mw=[d["power(mw)"] for d in model["power_curve"]],
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return obj
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class WindTurbineModels:
|
|
156
|
+
"""
|
|
157
|
+
An inventory of known wind turbine models. Each model is stored as an object attribute. All
|
|
158
|
+
attribute dictionaries must have the same structure.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
enercon_e126_ep3_4mw = dict(
|
|
162
|
+
brand="Enercon",
|
|
163
|
+
model="E-126 EP3 4.0MW",
|
|
164
|
+
rated_power_mw=4.0,
|
|
165
|
+
cut_in=2.5,
|
|
166
|
+
cut_out=25.0,
|
|
167
|
+
power_curve_source="https://www.thewindpower.net/turbine_en_1576_enercon_e126-4000.php",
|
|
168
|
+
power_curve=[
|
|
169
|
+
{"wind_speed_(m/s)": 0.0, "power(mw)": 0.0},
|
|
170
|
+
{"wind_speed_(m/s)": 0.5, "power(mw)": 0.0},
|
|
171
|
+
{"wind_speed_(m/s)": 1.0, "power(mw)": 0.0},
|
|
172
|
+
{"wind_speed_(m/s)": 1.5, "power(mw)": 0.0},
|
|
173
|
+
{"wind_speed_(m/s)": 2.0, "power(mw)": 0.0},
|
|
174
|
+
{"wind_speed_(m/s)": 2.5, "power(mw)": 0.02},
|
|
175
|
+
{"wind_speed_(m/s)": 3.0, "power(mw)": 0.047},
|
|
176
|
+
{"wind_speed_(m/s)": 3.5, "power(mw)": 0.095},
|
|
177
|
+
{"wind_speed_(m/s)": 4.0, "power(mw)": 0.15},
|
|
178
|
+
{"wind_speed_(m/s)": 4.5, "power(mw)": 0.29},
|
|
179
|
+
{"wind_speed_(m/s)": 5.0, "power(mw)": 0.441},
|
|
180
|
+
{"wind_speed_(m/s)": 5.5, "power(mw)": 0.582},
|
|
181
|
+
{"wind_speed_(m/s)": 6.0, "power(mw)": 0.732},
|
|
182
|
+
{"wind_speed_(m/s)": 6.5, "power(mw)": 0.945},
|
|
183
|
+
{"wind_speed_(m/s)": 7.0, "power(mw)": 1.165},
|
|
184
|
+
{"wind_speed_(m/s)": 7.5, "power(mw)": 1.43},
|
|
185
|
+
{"wind_speed_(m/s)": 8.0, "power(mw)": 1.7},
|
|
186
|
+
{"wind_speed_(m/s)": 8.5, "power(mw)": 2.012},
|
|
187
|
+
{"wind_speed_(m/s)": 9.0, "power(mw)": 2.323},
|
|
188
|
+
{"wind_speed_(m/s)": 9.5, "power(mw)": 2.677},
|
|
189
|
+
{"wind_speed_(m/s)": 10.0, "power(mw)": 3.023},
|
|
190
|
+
{"wind_speed_(m/s)": 10.5, "power(mw)": 3.27},
|
|
191
|
+
{"wind_speed_(m/s)": 11.0, "power(mw)": 3.504},
|
|
192
|
+
{"wind_speed_(m/s)": 11.5, "power(mw)": 3.698},
|
|
193
|
+
{"wind_speed_(m/s)": 12.0, "power(mw)": 3.88},
|
|
194
|
+
{"wind_speed_(m/s)": 12.5, "power(mw)": 3.917},
|
|
195
|
+
{"wind_speed_(m/s)": 13.0, "power(mw)": 3.945},
|
|
196
|
+
{"wind_speed_(m/s)": 13.5, "power(mw)": 3.959},
|
|
197
|
+
{"wind_speed_(m/s)": 14.0, "power(mw)": 3.969},
|
|
198
|
+
{"wind_speed_(m/s)": 14.5, "power(mw)": 3.995},
|
|
199
|
+
{"wind_speed_(m/s)": 15.0, "power(mw)": 4.0},
|
|
200
|
+
{"wind_speed_(m/s)": 15.5, "power(mw)": 4.0},
|
|
201
|
+
{"wind_speed_(m/s)": 16.0, "power(mw)": 4.0},
|
|
202
|
+
{"wind_speed_(m/s)": 16.5, "power(mw)": 4.0},
|
|
203
|
+
{"wind_speed_(m/s)": 17.0, "power(mw)": 4.0},
|
|
204
|
+
{"wind_speed_(m/s)": 17.5, "power(mw)": 4.0},
|
|
205
|
+
{"wind_speed_(m/s)": 18.0, "power(mw)": 4.0},
|
|
206
|
+
{"wind_speed_(m/s)": 18.5, "power(mw)": 4.0},
|
|
207
|
+
{"wind_speed_(m/s)": 19.0, "power(mw)": 4.0},
|
|
208
|
+
{"wind_speed_(m/s)": 19.5, "power(mw)": 4.0},
|
|
209
|
+
{"wind_speed_(m/s)": 20.0, "power(mw)": 4.0},
|
|
210
|
+
{"wind_speed_(m/s)": 20.5, "power(mw)": 4.0},
|
|
211
|
+
{"wind_speed_(m/s)": 21.0, "power(mw)": 4.0},
|
|
212
|
+
{"wind_speed_(m/s)": 21.5, "power(mw)": 4.0},
|
|
213
|
+
{"wind_speed_(m/s)": 22.0, "power(mw)": 4.0},
|
|
214
|
+
{"wind_speed_(m/s)": 22.5, "power(mw)": 3.985},
|
|
215
|
+
{"wind_speed_(m/s)": 23.0, "power(mw)": 3.96},
|
|
216
|
+
{"wind_speed_(m/s)": 23.5, "power(mw)": 3.96},
|
|
217
|
+
{"wind_speed_(m/s)": 24.0, "power(mw)": 3.952},
|
|
218
|
+
{"wind_speed_(m/s)": 24.5, "power(mw)": 3.93},
|
|
219
|
+
{"wind_speed_(m/s)": 25.0, "power(mw)": 3.898},
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
enercon_e53_800 = dict(
|
|
224
|
+
brand="Enercon",
|
|
225
|
+
model="E-53 800kW",
|
|
226
|
+
rated_power_mw=0.81,
|
|
227
|
+
cut_in=2.0,
|
|
228
|
+
cut_out=25.0,
|
|
229
|
+
power_curve_source="https://www.thewindpower.net/turbine_en_4_enercon_e53-800.php",
|
|
230
|
+
power_curve=[
|
|
231
|
+
{"wind_speed_(m/s)": 0.0, "power(mw)": 0.0},
|
|
232
|
+
{"wind_speed_(m/s)": 0.5, "power(mw)": 0.0},
|
|
233
|
+
{"wind_speed_(m/s)": 1.0, "power(mw)": 0.0},
|
|
234
|
+
{"wind_speed_(m/s)": 1.5, "power(mw)": 0.0},
|
|
235
|
+
{"wind_speed_(m/s)": 2.0, "power(mw)": 0.002},
|
|
236
|
+
{"wind_speed_(m/s)": 2.5, "power(mw)": 0.008},
|
|
237
|
+
{"wind_speed_(m/s)": 3.0, "power(mw)": 0.014},
|
|
238
|
+
{"wind_speed_(m/s)": 3.5, "power(mw)": 0.026},
|
|
239
|
+
{"wind_speed_(m/s)": 4.0, "power(mw)": 0.038},
|
|
240
|
+
{"wind_speed_(m/s)": 4.5, "power(mw)": 0.057},
|
|
241
|
+
{"wind_speed_(m/s)": 5.0, "power(mw)": 0.077},
|
|
242
|
+
{"wind_speed_(m/s)": 5.5, "power(mw)": 0.109},
|
|
243
|
+
{"wind_speed_(m/s)": 6.0, "power(mw)": 0.141},
|
|
244
|
+
{"wind_speed_(m/s)": 6.5, "power(mw)": 0.185},
|
|
245
|
+
{"wind_speed_(m/s)": 7.0, "power(mw)": 0.228},
|
|
246
|
+
{"wind_speed_(m/s)": 7.5, "power(mw)": 0.282},
|
|
247
|
+
{"wind_speed_(m/s)": 8.0, "power(mw)": 0.336},
|
|
248
|
+
{"wind_speed_(m/s)": 8.5, "power(mw)": 0.408},
|
|
249
|
+
{"wind_speed_(m/s)": 9.0, "power(mw)": 0.48},
|
|
250
|
+
{"wind_speed_(m/s)": 9.5, "power(mw)": 0.562},
|
|
251
|
+
{"wind_speed_(m/s)": 10.0, "power(mw)": 0.645},
|
|
252
|
+
{"wind_speed_(m/s)": 10.5, "power(mw)": 0.71},
|
|
253
|
+
{"wind_speed_(m/s)": 11.0, "power(mw)": 0.744},
|
|
254
|
+
{"wind_speed_(m/s)": 11.5, "power(mw)": 0.765},
|
|
255
|
+
{"wind_speed_(m/s)": 12.0, "power(mw)": 0.785},
|
|
256
|
+
{"wind_speed_(m/s)": 12.5, "power(mw)": 0.802},
|
|
257
|
+
{"wind_speed_(m/s)": 13.0, "power(mw)": 0.81},
|
|
258
|
+
{"wind_speed_(m/s)": 13.5, "power(mw)": 0.81},
|
|
259
|
+
{"wind_speed_(m/s)": 14.0, "power(mw)": 0.81},
|
|
260
|
+
{"wind_speed_(m/s)": 14.5, "power(mw)": 0.81},
|
|
261
|
+
{"wind_speed_(m/s)": 15.0, "power(mw)": 0.81},
|
|
262
|
+
{"wind_speed_(m/s)": 15.5, "power(mw)": 0.81},
|
|
263
|
+
{"wind_speed_(m/s)": 16.0, "power(mw)": 0.81},
|
|
264
|
+
{"wind_speed_(m/s)": 16.5, "power(mw)": 0.81},
|
|
265
|
+
{"wind_speed_(m/s)": 17.0, "power(mw)": 0.81},
|
|
266
|
+
{"wind_speed_(m/s)": 17.5, "power(mw)": 0.81},
|
|
267
|
+
{"wind_speed_(m/s)": 18.0, "power(mw)": 0.81},
|
|
268
|
+
{"wind_speed_(m/s)": 18.5, "power(mw)": 0.81},
|
|
269
|
+
{"wind_speed_(m/s)": 19.0, "power(mw)": 0.81},
|
|
270
|
+
{"wind_speed_(m/s)": 19.5, "power(mw)": 0.81},
|
|
271
|
+
{"wind_speed_(m/s)": 20.0, "power(mw)": 0.81},
|
|
272
|
+
{"wind_speed_(m/s)": 20.5, "power(mw)": 0.81},
|
|
273
|
+
{"wind_speed_(m/s)": 21.0, "power(mw)": 0.81},
|
|
274
|
+
{"wind_speed_(m/s)": 21.5, "power(mw)": 0.81},
|
|
275
|
+
{"wind_speed_(m/s)": 22.0, "power(mw)": 0.81},
|
|
276
|
+
{"wind_speed_(m/s)": 22.5, "power(mw)": 0.81},
|
|
277
|
+
{"wind_speed_(m/s)": 23.0, "power(mw)": 0.81},
|
|
278
|
+
{"wind_speed_(m/s)": 23.5, "power(mw)": 0.81},
|
|
279
|
+
{"wind_speed_(m/s)": 24.0, "power(mw)": 0.81},
|
|
280
|
+
{"wind_speed_(m/s)": 24.5, "power(mw)": 0.81},
|
|
281
|
+
{"wind_speed_(m/s)": 25.0, "power(mw)": 0.81},
|
|
282
|
+
],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
vestas_v52_850 = dict(
|
|
286
|
+
brand="Vestas",
|
|
287
|
+
model="V52 850kW",
|
|
288
|
+
rated_power_mw=0.85,
|
|
289
|
+
cut_in=3.0,
|
|
290
|
+
cut_out=25.0,
|
|
291
|
+
power_curve_source="https://www.thewindpower.net/turbine_en_27_vestas_v52-850.php",
|
|
292
|
+
power_curve=[
|
|
293
|
+
{"wind_speed_(m/s)": 0.0, "power(mw)": 0.0},
|
|
294
|
+
{"wind_speed_(m/s)": 0.5, "power(mw)": 0.0},
|
|
295
|
+
{"wind_speed_(m/s)": 1.0, "power(mw)": 0.0},
|
|
296
|
+
{"wind_speed_(m/s)": 1.5, "power(mw)": 0.0},
|
|
297
|
+
{"wind_speed_(m/s)": 2.0, "power(mw)": 0.0},
|
|
298
|
+
{"wind_speed_(m/s)": 2.5, "power(mw)": 0.0},
|
|
299
|
+
{"wind_speed_(m/s)": 3.0, "power(mw)": 0.005},
|
|
300
|
+
{"wind_speed_(m/s)": 3.5, "power(mw)": 0.015},
|
|
301
|
+
{"wind_speed_(m/s)": 4.0, "power(mw)": 0.03},
|
|
302
|
+
{"wind_speed_(m/s)": 4.5, "power(mw)": 0.049},
|
|
303
|
+
{"wind_speed_(m/s)": 5.0, "power(mw)": 0.067},
|
|
304
|
+
{"wind_speed_(m/s)": 5.5, "power(mw)": 0.097},
|
|
305
|
+
{"wind_speed_(m/s)": 6.0, "power(mw)": 0.127},
|
|
306
|
+
{"wind_speed_(m/s)": 6.5, "power(mw)": 0.162},
|
|
307
|
+
{"wind_speed_(m/s)": 7.0, "power(mw)": 0.197},
|
|
308
|
+
{"wind_speed_(m/s)": 7.5, "power(mw)": 0.244},
|
|
309
|
+
{"wind_speed_(m/s)": 8.0, "power(mw)": 0.29},
|
|
310
|
+
{"wind_speed_(m/s)": 8.5, "power(mw)": 0.35},
|
|
311
|
+
{"wind_speed_(m/s)": 9.0, "power(mw)": 0.41},
|
|
312
|
+
{"wind_speed_(m/s)": 9.5, "power(mw)": 0.475},
|
|
313
|
+
{"wind_speed_(m/s)": 10.0, "power(mw)": 0.539},
|
|
314
|
+
{"wind_speed_(m/s)": 10.5, "power(mw)": 0.6},
|
|
315
|
+
{"wind_speed_(m/s)": 11.0, "power(mw)": 0.66},
|
|
316
|
+
{"wind_speed_(m/s)": 11.5, "power(mw)": 0.71},
|
|
317
|
+
{"wind_speed_(m/s)": 12.0, "power(mw)": 0.751},
|
|
318
|
+
{"wind_speed_(m/s)": 12.5, "power(mw)": 0.784},
|
|
319
|
+
{"wind_speed_(m/s)": 13.0, "power(mw)": 0.816},
|
|
320
|
+
{"wind_speed_(m/s)": 13.5, "power(mw)": 0.84},
|
|
321
|
+
{"wind_speed_(m/s)": 14.0, "power(mw)": 0.85},
|
|
322
|
+
{"wind_speed_(m/s)": 14.5, "power(mw)": 0.85},
|
|
323
|
+
{"wind_speed_(m/s)": 15.0, "power(mw)": 0.85},
|
|
324
|
+
{"wind_speed_(m/s)": 15.5, "power(mw)": 0.85},
|
|
325
|
+
{"wind_speed_(m/s)": 16.0, "power(mw)": 0.85},
|
|
326
|
+
{"wind_speed_(m/s)": 16.5, "power(mw)": 0.85},
|
|
327
|
+
{"wind_speed_(m/s)": 17.0, "power(mw)": 0.85},
|
|
328
|
+
{"wind_speed_(m/s)": 17.5, "power(mw)": 0.85},
|
|
329
|
+
{"wind_speed_(m/s)": 18.0, "power(mw)": 0.85},
|
|
330
|
+
{"wind_speed_(m/s)": 18.5, "power(mw)": 0.85},
|
|
331
|
+
{"wind_speed_(m/s)": 19.0, "power(mw)": 0.85},
|
|
332
|
+
{"wind_speed_(m/s)": 19.5, "power(mw)": 0.85},
|
|
333
|
+
{"wind_speed_(m/s)": 20.0, "power(mw)": 0.85},
|
|
334
|
+
{"wind_speed_(m/s)": 20.5, "power(mw)": 0.85},
|
|
335
|
+
{"wind_speed_(m/s)": 21.0, "power(mw)": 0.85},
|
|
336
|
+
{"wind_speed_(m/s)": 21.5, "power(mw)": 0.85},
|
|
337
|
+
{"wind_speed_(m/s)": 22.0, "power(mw)": 0.85},
|
|
338
|
+
{"wind_speed_(m/s)": 22.5, "power(mw)": 0.85},
|
|
339
|
+
{"wind_speed_(m/s)": 23.0, "power(mw)": 0.85},
|
|
340
|
+
{"wind_speed_(m/s)": 23.5, "power(mw)": 0.85},
|
|
341
|
+
{"wind_speed_(m/s)": 24.0, "power(mw)": 0.85},
|
|
342
|
+
{"wind_speed_(m/s)": 24.5, "power(mw)": 0.85},
|
|
343
|
+
{"wind_speed_(m/s)": 25.0, "power(mw)": 0.85},
|
|
344
|
+
],
|
|
345
|
+
)
|
|
@@ -3,16 +3,10 @@ from datetime import datetime, timezone
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
5
|
# Third-party packages
|
|
6
|
-
import pandas as pd
|
|
7
6
|
from dateutil.relativedelta import *
|
|
8
7
|
|
|
9
8
|
# Specify functions to expose
|
|
10
|
-
__all__ = [
|
|
11
|
-
"count_nr_hours",
|
|
12
|
-
"count_delivery_periods",
|
|
13
|
-
"calc_months_diff",
|
|
14
|
-
"datetime_to_month_code",
|
|
15
|
-
]
|
|
9
|
+
__all__ = ["count_nr_hours", "calc_months_diff", "datetime_to_month_code"]
|
|
16
10
|
|
|
17
11
|
|
|
18
12
|
def count_nr_hours(start_date: datetime, end_date: datetime) -> float:
|
|
@@ -46,61 +40,6 @@ def count_nr_hours(start_date: datetime, end_date: datetime) -> float:
|
|
|
46
40
|
return td.days * 24 + td.seconds / 3600
|
|
47
41
|
|
|
48
42
|
|
|
49
|
-
def count_delivery_periods(
|
|
50
|
-
start_date: datetime,
|
|
51
|
-
end_date: datetime,
|
|
52
|
-
delivery_frequency: str,
|
|
53
|
-
full_periods_only: bool = False,
|
|
54
|
-
timezone: str = None,
|
|
55
|
-
) -> int:
|
|
56
|
-
"""
|
|
57
|
-
Counts the number of delivery periods within a time interval.
|
|
58
|
-
|
|
59
|
-
Note: When the end date is exactly equal to the start of the next delivery period, that next
|
|
60
|
-
period is not included (because it has not yet started). For example, if the end date is
|
|
61
|
-
15-Jan-2025 00:00:00 and the delivery frequency is daily, then the period
|
|
62
|
-
15-Jan-2025 00:00:00 to 16-Jan-2025 00:00:00 is not included.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
start_date: Delivery start date
|
|
66
|
-
end_date: Delivery end date
|
|
67
|
-
delivery_frequency: Delivery frequency, expressed as Pandas offset aliases
|
|
68
|
-
full_periods_only: Indicates whether to count only fully elapsed periods.
|
|
69
|
-
For example, suppose that start date is 15-Jan-2025 00:15:00, end date is
|
|
70
|
-
3-Feb-2025 03:45:00, and delivery frequency is hourly. Then:
|
|
71
|
-
- If full_periods_only=True, the number of periods is 2 (01:00:00-02:00:00 and
|
|
72
|
-
02:00:00-03:00:00), because the hours 00:00:00-01:00:00 and 00:03:00-04:00:00
|
|
73
|
-
are not full.
|
|
74
|
-
- If full_periods_only=False, the number of periods is 4, because incomplete hours
|
|
75
|
-
00:00:00-01:00:00 and 00:03:00-04:00:00 are also included. Note:
|
|
76
|
-
- If end date is 3-Feb-2025 04:00:00, hour 04:00:00-05:00:00 is not included.
|
|
77
|
-
- If end date is 3-Feb-2025 04:00:01, hour 04:00:00-05:00:00 is included.
|
|
78
|
-
timezone: Timezone (needed to account for DST switches)
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
Number of delivery periods in the interval
|
|
82
|
-
"""
|
|
83
|
-
# Convert to pandas timestamp
|
|
84
|
-
start_date = pd.Timestamp(start_date)
|
|
85
|
-
end_date = pd.Timestamp(end_date)
|
|
86
|
-
|
|
87
|
-
if full_periods_only:
|
|
88
|
-
start_date = start_date.ceil(delivery_frequency)
|
|
89
|
-
end_date = end_date.floor(delivery_frequency)
|
|
90
|
-
else:
|
|
91
|
-
start_date = start_date.floor(delivery_frequency)
|
|
92
|
-
|
|
93
|
-
periods = pd.date_range(
|
|
94
|
-
start=start_date,
|
|
95
|
-
end=end_date,
|
|
96
|
-
freq=delivery_frequency,
|
|
97
|
-
inclusive="left",
|
|
98
|
-
tz=timezone,
|
|
99
|
-
)
|
|
100
|
-
nr_periods = len(periods)
|
|
101
|
-
return nr_periods
|
|
102
|
-
|
|
103
|
-
|
|
104
43
|
def calc_months_diff(
|
|
105
44
|
start_date: datetime,
|
|
106
45
|
end_date: datetime,
|
|
@@ -138,8 +77,8 @@ def calc_months_diff(
|
|
|
138
77
|
Month difference between 2 dates
|
|
139
78
|
|
|
140
79
|
Raises:
|
|
141
|
-
ValueError: Raises an error when end_date < start_date
|
|
142
|
-
ValueError: Raises an error when the input argument 'diff_method' is invalid
|
|
80
|
+
ValueError: Raises an error when end_date < start_date.
|
|
81
|
+
ValueError: Raises an error when the input argument 'diff_method' is invalid.
|
|
143
82
|
"""
|
|
144
83
|
# Input validation
|
|
145
84
|
if end_date < start_date and diff_method != "month":
|
|
@@ -185,7 +124,7 @@ def datetime_to_month_code(d: datetime) -> int:
|
|
|
185
124
|
Corresponding month code
|
|
186
125
|
|
|
187
126
|
Raises:
|
|
188
|
-
ValueError: Raises an error if the input datetime is before 1 January 1900
|
|
127
|
+
ValueError: Raises an error if the input datetime is before 1 January 1900.
|
|
189
128
|
"""
|
|
190
129
|
if d < datetime(1900, 1, 1):
|
|
191
130
|
raise ValueError("Input date cannot be before 1 January 1900.")
|
|
@@ -15,6 +15,7 @@ from detquantlib.utils import list_to_str
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"convert_delivery_start_date_to_maturity",
|
|
17
17
|
"convert_maturity_to_delivery_start_date",
|
|
18
|
+
"count_delivery_periods",
|
|
18
19
|
"tenor_to_pd_offset_alias",
|
|
19
20
|
"tenor_to_nr_hours",
|
|
20
21
|
]
|
|
@@ -70,56 +71,114 @@ def tenor_to_nr_hours(
|
|
|
70
71
|
return nr_hours
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
def count_delivery_periods(
|
|
75
|
+
start_date: datetime,
|
|
76
|
+
end_date: datetime,
|
|
77
|
+
tenor: str,
|
|
78
|
+
full_periods_only: bool = False,
|
|
79
|
+
tz: str = None,
|
|
80
|
+
) -> int:
|
|
81
|
+
"""
|
|
82
|
+
Counts the number of delivery periods within a time interval.
|
|
83
|
+
|
|
84
|
+
Note: When the end date is exactly equal to the start of the next delivery period, that next
|
|
85
|
+
period is not included (because it has not yet started). For example, if the end date is
|
|
86
|
+
15-Jan-2025 00:00:00 and the delivery frequency is daily, then the period
|
|
87
|
+
15-Jan-2025 00:00:00 to 16-Jan-2025 00:00:00 is not included.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
start_date: Delivery start date
|
|
91
|
+
end_date: Delivery end date
|
|
92
|
+
tenor: Product tenor
|
|
93
|
+
full_periods_only: Indicates whether to count only fully elapsed periods.
|
|
94
|
+
For example, suppose that start date is 15-Jan-2025 00:15:00, end date is
|
|
95
|
+
3-Feb-2025 03:45:00, and delivery frequency is hourly. Then:
|
|
96
|
+
- If full_periods_only=True, the number of periods is 2 (01:00:00-02:00:00 and
|
|
97
|
+
02:00:00-03:00:00), because the hours 00:00:00-01:00:00 and 00:03:00-04:00:00
|
|
98
|
+
are not full.
|
|
99
|
+
- If full_periods_only=False, the number of periods is 4, because incomplete hours
|
|
100
|
+
00:00:00-01:00:00 and 00:03:00-04:00:00 are also included. Note:
|
|
101
|
+
- If end date is 3-Feb-2025 04:00:00, hour 04:00:00-05:00:00 is not included.
|
|
102
|
+
- If end date is 3-Feb-2025 04:00:01, hour 04:00:00-05:00:00 is included.
|
|
103
|
+
tz: Timezone (needed to account for DST switches)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Number of delivery periods in the interval
|
|
107
|
+
"""
|
|
108
|
+
# Convert tenor to offset alias
|
|
109
|
+
freq = tenor_to_pd_offset_alias(tenor)
|
|
110
|
+
|
|
111
|
+
# Convert to pandas timestamp
|
|
112
|
+
start_date = pd.Timestamp(start_date)
|
|
113
|
+
end_date = pd.Timestamp(end_date)
|
|
114
|
+
|
|
115
|
+
if full_periods_only:
|
|
116
|
+
start_date = start_date.ceil(freq)
|
|
117
|
+
end_date = end_date.floor(freq)
|
|
118
|
+
else:
|
|
119
|
+
start_date = start_date.floor(freq)
|
|
120
|
+
|
|
121
|
+
periods = pd.date_range(
|
|
122
|
+
start=start_date,
|
|
123
|
+
end=end_date,
|
|
124
|
+
freq=freq,
|
|
125
|
+
inclusive="left",
|
|
126
|
+
tz=tz,
|
|
127
|
+
)
|
|
128
|
+
nr_periods = len(periods)
|
|
129
|
+
return nr_periods
|
|
130
|
+
|
|
131
|
+
|
|
73
132
|
def convert_delivery_start_date_to_maturity(
|
|
74
133
|
trading_date: datetime,
|
|
75
134
|
delivery_start_date: datetime,
|
|
76
|
-
|
|
135
|
+
tenor: Literal["day", "weekend", "week", "month", "quarter", "year"],
|
|
77
136
|
) -> int:
|
|
78
137
|
"""
|
|
79
138
|
Calculates the number of maturities between the input trading date and the input delivery
|
|
80
|
-
date, based on the input product
|
|
139
|
+
date, based on the input product tenor.
|
|
81
140
|
|
|
82
141
|
Args:
|
|
83
142
|
trading_date: Trading date
|
|
84
143
|
delivery_start_date: Delivery start date
|
|
85
|
-
|
|
144
|
+
tenor: Product tenor (e.g. "month", "quarter", "year")
|
|
86
145
|
|
|
87
146
|
Returns:
|
|
88
147
|
Product maturity
|
|
89
148
|
|
|
90
149
|
Raises:
|
|
91
|
-
ValueError: Raises an error when the input product
|
|
150
|
+
ValueError: Raises an error when the input product tenor is not recognized.
|
|
92
151
|
"""
|
|
93
152
|
# Make input product string lower case only
|
|
94
|
-
|
|
153
|
+
tenor = tenor.lower()
|
|
95
154
|
|
|
96
155
|
# Set trading date and delivery date to midnight
|
|
97
156
|
trading_date = datetime.combine(trading_date, time())
|
|
98
157
|
delivery_start_date = datetime.combine(delivery_start_date, time())
|
|
99
158
|
|
|
100
|
-
if
|
|
159
|
+
if tenor == "day":
|
|
101
160
|
maturity = (delivery_start_date - trading_date).days
|
|
102
161
|
|
|
103
|
-
elif
|
|
162
|
+
elif tenor == "week":
|
|
104
163
|
maturity = math.ceil((delivery_start_date - trading_date).days / 7)
|
|
105
164
|
|
|
106
|
-
elif
|
|
165
|
+
elif tenor == "weekend":
|
|
107
166
|
maturity = math.ceil((delivery_start_date - trading_date).days / 7)
|
|
108
167
|
|
|
109
|
-
elif
|
|
168
|
+
elif tenor == "month":
|
|
110
169
|
maturity = calc_months_diff(
|
|
111
170
|
start_date=trading_date,
|
|
112
171
|
end_date=delivery_start_date,
|
|
113
172
|
diff_method="month",
|
|
114
173
|
)
|
|
115
174
|
|
|
116
|
-
elif
|
|
175
|
+
elif tenor == "quarter":
|
|
117
176
|
trading_quarter_start_date = convert_maturity_to_delivery_start_date(
|
|
118
|
-
trading_date=trading_date, maturity=0,
|
|
177
|
+
trading_date=trading_date, maturity=0, tenor="quarter"
|
|
119
178
|
)
|
|
120
179
|
|
|
121
180
|
delivery_quarter_start_date = convert_maturity_to_delivery_start_date(
|
|
122
|
-
trading_date=delivery_start_date, maturity=0,
|
|
181
|
+
trading_date=delivery_start_date, maturity=0, tenor="quarter"
|
|
123
182
|
)
|
|
124
183
|
|
|
125
184
|
months_diff = calc_months_diff(
|
|
@@ -129,11 +188,11 @@ def convert_delivery_start_date_to_maturity(
|
|
|
129
188
|
)
|
|
130
189
|
maturity = months_diff / 3
|
|
131
190
|
|
|
132
|
-
elif
|
|
191
|
+
elif tenor == "year":
|
|
133
192
|
maturity = delivery_start_date.year - trading_date.year
|
|
134
193
|
|
|
135
194
|
else:
|
|
136
|
-
raise ValueError("Invalid input product
|
|
195
|
+
raise ValueError("Invalid input product tenor.")
|
|
137
196
|
|
|
138
197
|
return maturity
|
|
139
198
|
|
|
@@ -141,42 +200,42 @@ def convert_delivery_start_date_to_maturity(
|
|
|
141
200
|
def convert_maturity_to_delivery_start_date(
|
|
142
201
|
trading_date: datetime,
|
|
143
202
|
maturity: int,
|
|
144
|
-
|
|
203
|
+
tenor: Literal["month", "quarter", "year"],
|
|
145
204
|
) -> datetime:
|
|
146
205
|
"""
|
|
147
|
-
Calculates the delivery start date of the input product, based on the input trading
|
|
148
|
-
and input maturity.
|
|
206
|
+
Calculates the delivery start date of the input product tenor, based on the input trading
|
|
207
|
+
date and input maturity.
|
|
149
208
|
|
|
150
209
|
Args:
|
|
151
210
|
trading_date: Trading date
|
|
152
211
|
maturity: Product maturity
|
|
153
|
-
|
|
212
|
+
tenor: Product tenor (e.g. "month", "quarter", "year")
|
|
154
213
|
|
|
155
214
|
Returns:
|
|
156
215
|
Delivery start date
|
|
157
216
|
|
|
158
217
|
Raises:
|
|
159
|
-
ValueError: Raises an error when the input product
|
|
218
|
+
ValueError: Raises an error when the input product tenor is not recognized.
|
|
160
219
|
"""
|
|
161
220
|
|
|
162
221
|
# Make input product string lower case only
|
|
163
|
-
|
|
222
|
+
tenor = tenor.lower()
|
|
164
223
|
|
|
165
|
-
if
|
|
224
|
+
if tenor == "month":
|
|
166
225
|
month_start_date = datetime(trading_date.year, trading_date.month, 1)
|
|
167
226
|
delivery_start_date = month_start_date + relativedelta(months=maturity)
|
|
168
227
|
|
|
169
|
-
elif
|
|
228
|
+
elif tenor == "quarter":
|
|
170
229
|
quarter = pd.Timestamp(trading_date).quarter
|
|
171
230
|
year_start_date = datetime(trading_date.year, 1, 1)
|
|
172
231
|
quarter_start_date = year_start_date + relativedelta(months=((quarter - 1) * 3))
|
|
173
232
|
delivery_start_date = quarter_start_date + relativedelta(months=(maturity * 3))
|
|
174
233
|
|
|
175
|
-
elif
|
|
234
|
+
elif tenor == "year":
|
|
176
235
|
year_start_date = datetime(trading_date.year, 1, 1)
|
|
177
236
|
delivery_start_date = year_start_date + relativedelta(years=maturity)
|
|
178
237
|
|
|
179
238
|
else:
|
|
180
|
-
raise ValueError("Invalid input product
|
|
239
|
+
raise ValueError("Invalid input product tenor.")
|
|
181
240
|
|
|
182
241
|
return delivery_start_date
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# Third-party packages
|
|
2
|
-
import numpy as np
|
|
3
|
-
import pandas as pd
|
|
4
|
-
from scipy.interpolate import CubicSpline
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class WindTurbine:
|
|
8
|
-
"""A class to store wind turbine information and perform wind turbine-related calculations."""
|
|
9
|
-
|
|
10
|
-
def __init__(
|
|
11
|
-
self, rated_power: float, cut_in: float, cut_out: float, power_curve: CubicSpline = None
|
|
12
|
-
):
|
|
13
|
-
"""
|
|
14
|
-
Constructor method.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
rated_power: Rated power
|
|
18
|
-
cut_in: Cut-in wind speed
|
|
19
|
-
cut_out: Cut-out wind speed
|
|
20
|
-
power_curve: Power curve, stored as a cubic spline object. If not provided, can be
|
|
21
|
-
set via the 'fit_power_curve()' method below.
|
|
22
|
-
"""
|
|
23
|
-
self.rated_power = rated_power
|
|
24
|
-
self.cut_in = cut_in
|
|
25
|
-
self.cut_out = cut_out
|
|
26
|
-
self.power_curve = power_curve
|
|
27
|
-
|
|
28
|
-
def fit_power_curve(
|
|
29
|
-
self, wind_speed: list | np.ndarray | pd.Series, power: list | np.ndarray | pd.Series
|
|
30
|
-
):
|
|
31
|
-
"""
|
|
32
|
-
Fits the wind turbine's power curve with a cubic spline, and stores the resulting spline
|
|
33
|
-
object in attribute self.power_curve.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
wind_speed: Wind speeds
|
|
37
|
-
power: Corresponding power
|
|
38
|
-
"""
|
|
39
|
-
# Make sure input data is stored as numpy array
|
|
40
|
-
wind_speed, power = np.array(wind_speed), np.array(power)
|
|
41
|
-
|
|
42
|
-
# Trim data to cut-in - cut-out interval
|
|
43
|
-
idx = (wind_speed >= self.cut_in) & (wind_speed <= self.cut_out)
|
|
44
|
-
wind_speed_trim, power_trim = wind_speed[idx], power[idx]
|
|
45
|
-
|
|
46
|
-
# Add data point with power=0.0 before cut-in to ensure smooth behaviour of spline
|
|
47
|
-
# interpolation
|
|
48
|
-
w0 = wind_speed_trim[0] - np.diff(wind_speed_trim[:2])
|
|
49
|
-
p0 = [0]
|
|
50
|
-
wind_speed_trim, power_trim = np.concat([w0, wind_speed_trim]), np.concat([p0, power_trim])
|
|
51
|
-
|
|
52
|
-
# Fit cubic spline
|
|
53
|
-
cs = CubicSpline(wind_speed_trim, power_trim)
|
|
54
|
-
|
|
55
|
-
# Store spline object
|
|
56
|
-
self.power_curve = cs
|
|
57
|
-
|
|
58
|
-
def wind_to_power(self, wind_speed: list | np.ndarray | pd.Series) -> np.ndarray:
|
|
59
|
-
"""
|
|
60
|
-
Converts wind speeds to wind turbine power.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
wind_speed: Wind speeds
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
Corresponding power
|
|
67
|
-
|
|
68
|
-
Raises:
|
|
69
|
-
ValueError: Raises an error if self.power_curve is None.
|
|
70
|
-
"""
|
|
71
|
-
# Check that power curve attribute is defined
|
|
72
|
-
if self.power_curve is None:
|
|
73
|
-
raise ValueError(
|
|
74
|
-
"Cannot execute method self.wind_to_power() if attribute self.power_curve is "
|
|
75
|
-
"None. Use method self.fit_power_curve() to set self.power_curve based on power "
|
|
76
|
-
"curve data."
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
# Make sure input data is stored as numpy array
|
|
80
|
-
wind_speed = np.array(wind_speed)
|
|
81
|
-
|
|
82
|
-
# Cubic spline interpolation
|
|
83
|
-
power = self.power_curve(wind_speed)
|
|
84
|
-
|
|
85
|
-
# Cap to rated power
|
|
86
|
-
power = np.minimum(power, self.rated_power)
|
|
87
|
-
|
|
88
|
-
# Adjust values outside of cut-in - cut-out bounds
|
|
89
|
-
idx = (wind_speed < self.cut_in) | (wind_speed > self.cut_out)
|
|
90
|
-
power[idx] = 0.0
|
|
91
|
-
|
|
92
|
-
return power
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|