detquantlib 5.1.1__tar.gz → 5.2.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-5.1.1 → detquantlib-5.2.0}/PKG-INFO +1 -1
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/assets/wind_turbine.py +70 -32
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/data/databases/detdatabase.py +32 -17
- {detquantlib-5.1.1 → detquantlib-5.2.0}/pyproject.toml +1 -1
- {detquantlib-5.1.1 → detquantlib-5.2.0}/LICENSE.txt +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/README.md +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/assets/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/assets/battery.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/converters/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/converters/definitions.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/converters/energy.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/converters/helpers.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/converters/price.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/data/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/data/databases/helpers.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/dates/dates.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/forecasting/forecasting.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/stats/data_analysis.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/tradable_products/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/tradable_products/tradable_products.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/utils/__init__.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/utils/logging.py +0 -0
- {detquantlib-5.1.1 → detquantlib-5.2.0}/detquantlib/utils/utils.py +0 -0
|
@@ -5,6 +5,7 @@ from scipy.interpolate import CubicSpline
|
|
|
5
5
|
|
|
6
6
|
# Internal modules
|
|
7
7
|
from detquantlib.converters import power_to_energy
|
|
8
|
+
from detquantlib.utils import list_to_str
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class WindTurbine:
|
|
@@ -35,6 +36,73 @@ class WindTurbine:
|
|
|
35
36
|
self.nr_turbines = nr_turbines
|
|
36
37
|
self.power_curve = power_curve
|
|
37
38
|
|
|
39
|
+
@staticmethod
|
|
40
|
+
def list_models() -> list[str]:
|
|
41
|
+
"""Gets the list of available wind turbine models."""
|
|
42
|
+
return [d for d in WindTurbineModels.__dict__ if not d.startswith("__")]
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def get_model(model_name: str) -> dict:
|
|
46
|
+
"""Gets the model information of a given wind turbine."""
|
|
47
|
+
try:
|
|
48
|
+
return getattr(WindTurbineModels, model_name)
|
|
49
|
+
except AttributeError as e:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Invalid model name '{model_name}'. Available models: "
|
|
52
|
+
f"{list_to_str(WindTurbine.list_models())}."
|
|
53
|
+
) from e
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def build_from_model(model_name: str = None, model_info: dict = None, nr_turbines: int = 1):
|
|
57
|
+
"""
|
|
58
|
+
Builds a WindTurbine object from a known wind turbine model.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
model_name: Wind turbine model name, as given in the WindTurbineModels class. If not
|
|
62
|
+
provided, argument 'model_info' should be provided.
|
|
63
|
+
model_info: Wind turbine model information. The dictionary can be taken from the
|
|
64
|
+
list of models in the WindTurbineModels class. Otherwise, the dictionary should
|
|
65
|
+
have the same structure as those in the WindTurbineModels class. If not provided,
|
|
66
|
+
argument 'model_name' should be provided.
|
|
67
|
+
nr_turbines: Number of turbines in the wind farm
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
WindTurbine object
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: Raises an error if both 'model_name' and 'model_info' are missing.
|
|
74
|
+
ValueError: Raises an error if both 'model_name' and 'model_info' are provided.
|
|
75
|
+
"""
|
|
76
|
+
# Inputs validation
|
|
77
|
+
if model_name is None and model_info is None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Cannot determine turbine model. Please provide either argument 'model_name' "
|
|
80
|
+
"or 'model_info'."
|
|
81
|
+
)
|
|
82
|
+
elif model_name is not None and model_info is not None:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Cannot determine turbine model. Please provide either argument 'model_name' "
|
|
85
|
+
"or 'model_info', but not both."
|
|
86
|
+
)
|
|
87
|
+
elif model_name is not None:
|
|
88
|
+
model_info = WindTurbine.get_model(model_name)
|
|
89
|
+
|
|
90
|
+
# Create object
|
|
91
|
+
obj = WindTurbine(
|
|
92
|
+
rated_power_mw=model_info["rated_power_mw"],
|
|
93
|
+
cut_in=model_info["cut_in"],
|
|
94
|
+
cut_out=model_info["cut_out"],
|
|
95
|
+
nr_turbines=nr_turbines,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Set power_curve attribute
|
|
99
|
+
obj.fit_power_curve(
|
|
100
|
+
wind_speed=[d["wind_speed_(m/s)"] for d in model_info["power_curve"]],
|
|
101
|
+
power_mw=[d["power(mw)"] for d in model_info["power_curve"]],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return obj
|
|
105
|
+
|
|
38
106
|
def fit_power_curve(
|
|
39
107
|
self, wind_speed: list | np.ndarray | pd.Series, power_mw: list | np.ndarray | pd.Series
|
|
40
108
|
):
|
|
@@ -92,8 +160,8 @@ class WindTurbine:
|
|
|
92
160
|
# Cubic spline interpolation
|
|
93
161
|
power_mw = self.power_curve(wind_speed)
|
|
94
162
|
|
|
95
|
-
#
|
|
96
|
-
power_mw = np.
|
|
163
|
+
# Bound between 0 and rated power
|
|
164
|
+
power_mw = np.clip(a=power_mw, a_min=0.0, a_max=self.rated_power_mw)
|
|
97
165
|
|
|
98
166
|
# Adjust values outside of cut-in - cut-out bounds
|
|
99
167
|
idx = (wind_speed < self.cut_in) | (wind_speed > self.cut_out)
|
|
@@ -121,36 +189,6 @@ class WindTurbine:
|
|
|
121
189
|
)
|
|
122
190
|
return energy_mwh
|
|
123
191
|
|
|
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
192
|
|
|
155
193
|
class WindTurbineModels:
|
|
156
194
|
"""
|
|
@@ -878,15 +878,20 @@ class DetDatabase:
|
|
|
878
878
|
Args:
|
|
879
879
|
profile: Customer/profile name.
|
|
880
880
|
Note on the 'Portfolio' and 'Portfolioweekend' profiles:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
881
|
+
- [Legacy data until March 2026]:
|
|
882
|
+
- Profile names 'Portfolio' and 'Portfolioweekend' refer to the same set
|
|
883
|
+
of connection points.
|
|
884
|
+
- In the database, the profile name is set to 'Portfolio' when the
|
|
885
|
+
forecast date is between Monday and Friday.
|
|
886
|
+
- In the database, the profile name is set to 'Portfolioweekend' when
|
|
887
|
+
the forecast date is Saturday or Sunday.
|
|
888
|
+
- [Data since March 2026]:
|
|
889
|
+
- The 'Portfolioweekend' name is deprecated. Day-ahead forecasts are
|
|
890
|
+
now stored under the 'Portfolio' profile only.
|
|
891
|
+
- When calling this method, set the input argument 'profile' to
|
|
892
|
+
'PortfolioAll' to automatically switch between 'Portfolio' and
|
|
893
|
+
'Portfolioweekend', based on the used-input forecast date.
|
|
894
|
+
'PortfolioAll' supports both the legacy and new data structure.
|
|
890
895
|
forecast_date: Date on which customer volume forecast is generated.
|
|
891
896
|
start_delivery_date: First delivery date included.
|
|
892
897
|
end_delivery_date: Last delivery date included.
|
|
@@ -902,13 +907,6 @@ class DetDatabase:
|
|
|
902
907
|
Raises:
|
|
903
908
|
ValueError: Raises an error if no volume forecast data is found for user inputs.
|
|
904
909
|
"""
|
|
905
|
-
# Convert profile if set to "PortfolioAll"
|
|
906
|
-
if profile == "PortfolioAll":
|
|
907
|
-
if forecast_date.weekday() > 4:
|
|
908
|
-
profile = "Portfolioweekend"
|
|
909
|
-
else:
|
|
910
|
-
profile = "Portfolio"
|
|
911
|
-
|
|
912
910
|
# Convert start delivery date from local timezone to UTC and string
|
|
913
911
|
start_delivery_date = start_delivery_date.replace(tzinfo=ZoneInfo(local_timezone))
|
|
914
912
|
start_delivery_date = start_delivery_date.astimezone(ZoneInfo("UTC"))
|
|
@@ -930,17 +928,34 @@ class DetDatabase:
|
|
|
930
928
|
# Convert dates from datetime to string
|
|
931
929
|
forecast_date_str = forecast_date.strftime("%Y-%m-%d")
|
|
932
930
|
|
|
931
|
+
# Define query profile name. If 'PortfolioAll', first try with 'Portfolio' (compatibility
|
|
932
|
+
# with new data).
|
|
933
|
+
query_profile = "Portfolio" if profile == "PortfolioAll" else profile
|
|
934
|
+
|
|
933
935
|
# Query db
|
|
934
936
|
table = DetDatabase.get_table_name("client_volume_forecast")
|
|
935
937
|
query = (
|
|
936
938
|
f"SELECT {columns_str} FROM {table} "
|
|
937
|
-
f"WHERE Profile='{
|
|
939
|
+
f"WHERE Profile='{query_profile}' "
|
|
938
940
|
f"AND ForecastDate='{forecast_date_str}' "
|
|
939
941
|
f"AND Datetime>='{start_date_str}' "
|
|
940
942
|
f"AND Datetime<'{end_date_str}'"
|
|
941
943
|
)
|
|
942
944
|
df = self.query_db(query)
|
|
943
945
|
|
|
946
|
+
if profile == "PortfolioAll" and df.empty and forecast_date.weekday() > 4:
|
|
947
|
+
# If 'Portfolio' not found, try with 'Portfolioweekend' (backward compatibility with
|
|
948
|
+
# legacy data)
|
|
949
|
+
query_profile = "Portfolioweekend"
|
|
950
|
+
query = (
|
|
951
|
+
f"SELECT {columns_str} FROM {table} "
|
|
952
|
+
f"WHERE Profile='{query_profile}' "
|
|
953
|
+
f"AND ForecastDate='{forecast_date_str}' "
|
|
954
|
+
f"AND Datetime>='{start_date_str}' "
|
|
955
|
+
f"AND Datetime<'{end_date_str}'"
|
|
956
|
+
)
|
|
957
|
+
df = self.query_db(query)
|
|
958
|
+
|
|
944
959
|
# Assert data
|
|
945
960
|
if df.empty:
|
|
946
961
|
raise ValueError("No volume forecast data found for user-defined inputs.")
|
|
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
|
|
File without changes
|
|
File without changes
|