technologydata 0.1.0__py3-none-any.whl
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.
- technologydata/__init__.py +41 -0
- technologydata/constants/__init__.py +12 -0
- technologydata/constants/energy_density.py +324 -0
- technologydata/datapackage.py +145 -0
- technologydata/package_data/dea_energy_storage/dea_energy_storage.py +691 -0
- technologydata/package_data/dea_energy_storage/sources.json +12 -0
- technologydata/package_data/dea_energy_storage/technologies.json +17508 -0
- technologydata/package_data/raw/Technology_datasheet_for_energy_storage.xlsx +0 -0
- technologydata/parameter.py +662 -0
- technologydata/source.py +430 -0
- technologydata/source_collection.py +243 -0
- technologydata/technologies/__init__.py +5 -0
- technologydata/technologies/growth_models.py +503 -0
- technologydata/technology.py +191 -0
- technologydata/technology_collection.py +466 -0
- technologydata/utils/__init__.py +13 -0
- technologydata/utils/carriers.txt +33 -0
- technologydata/utils/commons.py +310 -0
- technologydata/utils/heating_values.txt +17 -0
- technologydata/utils/units.py +519 -0
- technologydata-0.1.0.dist-info/METADATA +135 -0
- technologydata-0.1.0.dist-info/RECORD +25 -0
- technologydata-0.1.0.dist-info/WHEEL +5 -0
- technologydata-0.1.0.dist-info/licenses/LICENSE +18 -0
- technologydata-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Data parser for the DEA energy storage data set.
|
|
7
|
+
|
|
8
|
+
How to run:
|
|
9
|
+
From the repository root, execute:
|
|
10
|
+
python src/technologydata/package_data/dea_energy_storage/dea_energy_storage.py
|
|
11
|
+
|
|
12
|
+
Configuration options (command-line arguments):
|
|
13
|
+
--num_digits <int> Number of significant digits to round the values. Default: 4
|
|
14
|
+
--store_source Store the source object on the Wayback Machine. Default: False
|
|
15
|
+
--filter_params Filter the parameters stored to technologies.json. Default: False
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
python src/technologydata/package_data/dea_energy_storage/dea_energy_storage.py --num_digits 3 --store_source --filter_params
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import logging
|
|
24
|
+
import pathlib
|
|
25
|
+
import re
|
|
26
|
+
import typing
|
|
27
|
+
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import pydantic
|
|
30
|
+
|
|
31
|
+
from technologydata import (
|
|
32
|
+
Commons,
|
|
33
|
+
Parameter,
|
|
34
|
+
Source,
|
|
35
|
+
SourceCollection,
|
|
36
|
+
Technology,
|
|
37
|
+
TechnologyCollection,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
path_cwd = pathlib.Path.cwd()
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def drop_invalid_rows(dataframe: pd.DataFrame) -> pd.DataFrame:
|
|
46
|
+
"""
|
|
47
|
+
Clean and filter a DataFrame by removing rows with invalid or incomplete data.
|
|
48
|
+
|
|
49
|
+
This function performs multiple validation checks to ensure data quality:
|
|
50
|
+
- Removes rows with None or NaN values in critical columns
|
|
51
|
+
- Removes rows with empty or whitespace-only strings
|
|
52
|
+
- Filters rows based on specific data integrity criteria
|
|
53
|
+
- Discards rows where 'val' column contains comparator symbols or non-numeric values
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
dataframe : pd.DataFrame
|
|
58
|
+
The input DataFrame to be cleaned and validated.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
pd.DataFrame
|
|
63
|
+
A new DataFrame with invalid rows removed, maintaining data integrity.
|
|
64
|
+
|
|
65
|
+
Notes
|
|
66
|
+
-----
|
|
67
|
+
Validation criteria include:
|
|
68
|
+
- Non-empty 'Technology', 'par', and 'val' columns
|
|
69
|
+
- 'year' column containing a valid 4-digit year
|
|
70
|
+
- 'val' column containing only numeric values (no comparator symbols)
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
# Create a copy to avoid modifying the original DataFrame
|
|
74
|
+
df_cleaned = dataframe.copy()
|
|
75
|
+
|
|
76
|
+
# Validate column existence
|
|
77
|
+
required_columns = ["Technology", "par", "val", "year"]
|
|
78
|
+
missing_columns = [col for col in required_columns if col not in df_cleaned.columns]
|
|
79
|
+
if missing_columns:
|
|
80
|
+
raise ValueError(f"Missing required columns: {missing_columns}")
|
|
81
|
+
|
|
82
|
+
# Remove rows with None or NaN values in critical columns
|
|
83
|
+
df_cleaned = df_cleaned.dropna(subset=required_columns)
|
|
84
|
+
|
|
85
|
+
# Remove rows with empty or whitespace-only strings
|
|
86
|
+
for column in required_columns:
|
|
87
|
+
df_cleaned = df_cleaned[df_cleaned[column].astype(str).str.strip() != ""]
|
|
88
|
+
|
|
89
|
+
# Filter rows with valid year (4 consecutive digits)
|
|
90
|
+
df_cleaned = df_cleaned[
|
|
91
|
+
df_cleaned["year"].astype(str).str.contains(r"\d{4}", regex=True)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Remove rows with comparator symbols or without digits in 'val' column
|
|
95
|
+
df_cleaned = df_cleaned[
|
|
96
|
+
(~df_cleaned["val"].astype(str).str.contains(r"[<>≤≥]", regex=True))
|
|
97
|
+
& (df_cleaned["val"].astype(str).str.contains(r"\d", regex=True))
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
return df_cleaned
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pydantic.validate_call
|
|
104
|
+
def clean_parameter_string(text_string: str) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Remove any string between [] or [), any leading hyphen or double quotes from the input string. Lower-case all.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
text_string : str
|
|
111
|
+
input string to be cleaned.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
str
|
|
116
|
+
cleaned string with [] and [) and leading hyphen or double quotes removed.
|
|
117
|
+
|
|
118
|
+
Examples
|
|
119
|
+
--------
|
|
120
|
+
>>> clean_parameter_string("- Charge efficiency [%]")
|
|
121
|
+
charge efficiency
|
|
122
|
+
>>> clean_parameter_string("Energy storage capacity for one unit [MWh)")
|
|
123
|
+
energy storage capacity for one unit
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
# Remove leading hyphen
|
|
127
|
+
text_string = text_string.lstrip("-")
|
|
128
|
+
|
|
129
|
+
# Remove content inside square brackets including the brackets themselves
|
|
130
|
+
result = re.sub(r"\[.*?\]", "", text_string)
|
|
131
|
+
|
|
132
|
+
# Remove content inside square bracket and parenthesis including the brackets/parenthesis themselves
|
|
133
|
+
result = re.sub(r"\[.*?\)", "", result)
|
|
134
|
+
|
|
135
|
+
# Remove extra spaces resulting from the removal and set all to lower case
|
|
136
|
+
result = re.sub(r"\s+", " ", result).strip().casefold()
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pydantic.validate_call
|
|
142
|
+
def clean_technology_string(tech_str: str) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Clean a technology string by removing numeric patterns and standardizing case.
|
|
145
|
+
|
|
146
|
+
This function pre-processes technology-related strings by:
|
|
147
|
+
- Removing three-digit numeric patterns (with optional letter)
|
|
148
|
+
- Stripping leading and trailing whitespace
|
|
149
|
+
- Converting to lowercase for case-insensitive comparison
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
tech_str : str
|
|
154
|
+
Input technology string to be cleaned.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
str
|
|
159
|
+
Cleaned technology string with:
|
|
160
|
+
- Numeric patterns (like '123' or '456a') removed
|
|
161
|
+
- Whitespace stripped
|
|
162
|
+
- Converted to lowercase
|
|
163
|
+
|
|
164
|
+
Raises
|
|
165
|
+
------
|
|
166
|
+
Exception
|
|
167
|
+
If string conversion or processing fails, logs the error and returns the original input.
|
|
168
|
+
|
|
169
|
+
Examples
|
|
170
|
+
--------
|
|
171
|
+
>>> clean_technology_string("143a Rock-based Carnot battery")
|
|
172
|
+
rock-based carnot battery
|
|
173
|
+
>>> clean_technology_string("Pit Thermal Energy Storage [PTES]")
|
|
174
|
+
pit thermal energy storage [ptes]
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
# Remove three-digit patterns or three digits followed by a letter
|
|
179
|
+
return re.sub(r"^(\d{3}[a-zA-Z]?)", "", tech_str.strip()).strip().casefold()
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Error cleaning technology '{tech_str}': {e}")
|
|
182
|
+
return tech_str
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pydantic.validate_call
|
|
186
|
+
def format_val_number(input_value: str, num_decimals: int) -> float | None | typing.Any:
|
|
187
|
+
"""
|
|
188
|
+
Parse various number formats into a float value.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
input_value : str
|
|
193
|
+
The input number in different formats, such as:
|
|
194
|
+
- Scientific notation with "x10^": e.g., "2.84x10^23"
|
|
195
|
+
- Numbers with commas as decimal separators: e.g., "1,1"
|
|
196
|
+
num_decimals : int
|
|
197
|
+
Number of decimals
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
float
|
|
202
|
+
The parsed numerical value as a float.
|
|
203
|
+
|
|
204
|
+
Raises
|
|
205
|
+
------
|
|
206
|
+
ValueError
|
|
207
|
+
If the input cannot be parsed into a float.
|
|
208
|
+
|
|
209
|
+
Examples
|
|
210
|
+
--------
|
|
211
|
+
>>> format_val_number("1,1")
|
|
212
|
+
1.1
|
|
213
|
+
>>> format_val_numer("2.84×10-27")
|
|
214
|
+
2.84e-27
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
s = str(input_value).strip()
|
|
218
|
+
|
|
219
|
+
# Handle scientific notation like "2.84x10^23"
|
|
220
|
+
match = re.match(r"([+-]?\d*\.?\d+)×10([+-]?\d+)", s)
|
|
221
|
+
if match:
|
|
222
|
+
base, exponent = match.groups()
|
|
223
|
+
return round(float(base), num_decimals) * (10 ** int(exponent))
|
|
224
|
+
|
|
225
|
+
# Replace comma with dot for decimal numbers
|
|
226
|
+
s = s.replace(",", ".")
|
|
227
|
+
try:
|
|
228
|
+
return round(float(s), num_decimals)
|
|
229
|
+
except ValueError:
|
|
230
|
+
raise ValueError(f"Cannot parse number from input: {input_value}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pydantic.validate_call
|
|
234
|
+
def extract_year(year_str: str) -> int | None:
|
|
235
|
+
"""
|
|
236
|
+
Extract the first year (integer) from a given input.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
year_str : str
|
|
241
|
+
Input value containing a potential year.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
int, None
|
|
246
|
+
Extracted first year.
|
|
247
|
+
|
|
248
|
+
Examples
|
|
249
|
+
--------
|
|
250
|
+
>>> extract_year('uncertainty (2050)')
|
|
251
|
+
2050
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
# Extract digits
|
|
255
|
+
digits = re.findall(r"\d+", year_str)
|
|
256
|
+
|
|
257
|
+
# Convert to integer
|
|
258
|
+
return int(digits[0]) if digits else None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pydantic.validate_call
|
|
262
|
+
def clean_est_string(est_str: str) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Casefold the 'est' string, trim whitespace and replace `ctrl` with `control`.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
est_str : str
|
|
269
|
+
The input 'est' string to be cleaned.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
str
|
|
274
|
+
The cleaned 'est' string.
|
|
275
|
+
|
|
276
|
+
Examples
|
|
277
|
+
--------
|
|
278
|
+
>>> clean_est_string("Lower")
|
|
279
|
+
lower
|
|
280
|
+
>>> clean_est_string("ctrl")
|
|
281
|
+
control
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
if est_str == "ctrl":
|
|
285
|
+
cleaned_str = "control"
|
|
286
|
+
else:
|
|
287
|
+
cleaned_str = est_str.casefold().strip()
|
|
288
|
+
return cleaned_str
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def standardize_units(series: pd.Series) -> pd.Series:
|
|
292
|
+
"""
|
|
293
|
+
Complete missing units based on parameter names and replace incorrect units.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
series : pandas.Series
|
|
298
|
+
A series containing two elements: [par, unit]
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
pandas.Series
|
|
303
|
+
Updated series with completed and corrected unit.
|
|
304
|
+
|
|
305
|
+
Notes
|
|
306
|
+
-----
|
|
307
|
+
The following substitutions are driven by the `pint` documentation available
|
|
308
|
+
at https://github.com/hgrecco/pint/blob/master/pint/default_en.txt:
|
|
309
|
+
- "pct.": "percent",
|
|
310
|
+
- "m2": "meter**2",
|
|
311
|
+
- "m3": "meter**3",
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
par, unit = series
|
|
315
|
+
|
|
316
|
+
# Mapping of parameters to their default units
|
|
317
|
+
param_unit_map = {
|
|
318
|
+
"energy storage capacity for one unit": "MWh",
|
|
319
|
+
"typical temperature difference in storage": "K",
|
|
320
|
+
"fixed o&m": "pct./year",
|
|
321
|
+
"lifetime in total number of cycles": "cycles",
|
|
322
|
+
"cycle life": "cycles",
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Mapping of incorrect units to correct units
|
|
326
|
+
unit_corrections = {
|
|
327
|
+
"pct./period": "percent",
|
|
328
|
+
"⁰C": "C",
|
|
329
|
+
"°C": "C",
|
|
330
|
+
"pct./30sec": "pct.",
|
|
331
|
+
"m2": "meter**2",
|
|
332
|
+
"m3": "meter**3",
|
|
333
|
+
"MWhoutput": "MWh",
|
|
334
|
+
"hot/cold,K": "K",
|
|
335
|
+
"pct.investement": "percent",
|
|
336
|
+
"pct.investment": "percent",
|
|
337
|
+
"tank/": "",
|
|
338
|
+
"pct.": "percent",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Complete missing or empty units
|
|
342
|
+
if (not isinstance(unit, str)) or (unit.strip() == ""):
|
|
343
|
+
unit = param_unit_map.get(par, unit)
|
|
344
|
+
|
|
345
|
+
# Replace wrong units
|
|
346
|
+
for incorrect, correct in unit_corrections.items():
|
|
347
|
+
if incorrect == unit or incorrect in unit:
|
|
348
|
+
unit = unit.replace(incorrect, correct)
|
|
349
|
+
|
|
350
|
+
return pd.Series([par, unit])
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def filter_parameters(dataframe: pd.DataFrame, filter_flag: bool) -> pd.DataFrame:
|
|
354
|
+
"""
|
|
355
|
+
Filter rows of a DataFrame by allowed technology parameters.
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
dataframe : pandas.DataFrame
|
|
360
|
+
Input DataFrame containing at least a "Technology" column.
|
|
361
|
+
filter_flag : Boolean
|
|
362
|
+
If true, filter parameter `par` column
|
|
363
|
+
|
|
364
|
+
Returns
|
|
365
|
+
-------
|
|
366
|
+
pandas.DataFrame
|
|
367
|
+
The filtered DataFrame. The returned
|
|
368
|
+
DataFrame contains only rows where the `par` column is one of:
|
|
369
|
+
"technical lifetime", "fixed o&m", "specific investment", or
|
|
370
|
+
"variable o&m".
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
allowed_set = {
|
|
374
|
+
"technical lifetime",
|
|
375
|
+
"fixed o&m",
|
|
376
|
+
"specific investment",
|
|
377
|
+
"variable o&m",
|
|
378
|
+
"charge efficiency",
|
|
379
|
+
"discharge efficiency",
|
|
380
|
+
"capacity",
|
|
381
|
+
}
|
|
382
|
+
print("filter_flag", filter_flag)
|
|
383
|
+
if filter_flag:
|
|
384
|
+
# Filter the DataFrame based on the allowed set
|
|
385
|
+
df_filtered = dataframe[dataframe["par"].isin(allowed_set)].reset_index(
|
|
386
|
+
drop=True
|
|
387
|
+
)
|
|
388
|
+
logger.info(
|
|
389
|
+
f"technologies.json contains a subset of the allowed parameters: {allowed_set}."
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
# Return the original DataFrame if filter_flag is False
|
|
393
|
+
df_filtered = dataframe
|
|
394
|
+
logger.info("All parameters are outputted to technologies.json")
|
|
395
|
+
return df_filtered
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def build_technology_collection(
|
|
399
|
+
dataframe: pd.DataFrame,
|
|
400
|
+
sources_path: pathlib.Path,
|
|
401
|
+
store_source: bool = False,
|
|
402
|
+
output_schema: bool = False,
|
|
403
|
+
) -> TechnologyCollection:
|
|
404
|
+
"""
|
|
405
|
+
Compute a collection of technologies from a grouped DataFrame.
|
|
406
|
+
|
|
407
|
+
Processes input DataFrame by grouping technologies and extracting their parameters,
|
|
408
|
+
creating Technology instances for each unique group.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
dataframe : pandas.DataFrame
|
|
413
|
+
Input DataFrame containing technology parameters.
|
|
414
|
+
Expected columns include:
|
|
415
|
+
- 'est': Estimation or case identifier
|
|
416
|
+
- 'year': Year of the technology
|
|
417
|
+
- 'ws': Workspace or technology identifier
|
|
418
|
+
- 'Technology': Detailed technology name
|
|
419
|
+
- 'par': Parameter name
|
|
420
|
+
- 'val': Parameter value
|
|
421
|
+
- 'unit': Parameter units
|
|
422
|
+
sources_path: pathlib.Path
|
|
423
|
+
Output path for storing the SourceCollection object
|
|
424
|
+
store_source: Optional[bool]
|
|
425
|
+
Flag to decide whether to store the source object on the Wayback Machine. Default False.
|
|
426
|
+
output_schema : Optional[bool]
|
|
427
|
+
Flag to decide whether to export the source collection schema. Default False.
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
TechnologyCollection
|
|
432
|
+
A collection of Technology instances, each representing a unique
|
|
433
|
+
technology group with its associated parameters.
|
|
434
|
+
|
|
435
|
+
Notes
|
|
436
|
+
-----
|
|
437
|
+
- The function groups the DataFrame by 'est', 'year', 'ws', and 'Technology'
|
|
438
|
+
- For each group, it creates a dictionary of Parameters
|
|
439
|
+
- Each Technology is instantiated with group-specific attributes
|
|
440
|
+
|
|
441
|
+
"""
|
|
442
|
+
list_techs = []
|
|
443
|
+
|
|
444
|
+
if store_source:
|
|
445
|
+
source = Source(
|
|
446
|
+
title="Technology Data for Energy storage (May 2025)",
|
|
447
|
+
authors="Danish Energy Agency",
|
|
448
|
+
url="https://ens.dk/media/6589/download",
|
|
449
|
+
url_date="2025-10-08 09:24:00",
|
|
450
|
+
)
|
|
451
|
+
source.ensure_in_wayback()
|
|
452
|
+
sources = SourceCollection(sources=[source])
|
|
453
|
+
sources.to_json(sources_path, output_schema=output_schema)
|
|
454
|
+
else:
|
|
455
|
+
sources = SourceCollection.from_json(sources_path)
|
|
456
|
+
|
|
457
|
+
for (est, year, ws, technology_name), group in dataframe.groupby(
|
|
458
|
+
["est", "year", "ws", "Technology"]
|
|
459
|
+
):
|
|
460
|
+
parameters = {}
|
|
461
|
+
for _, row in group.iterrows():
|
|
462
|
+
parameters[row["par"]] = Parameter(
|
|
463
|
+
magnitude=row["val"],
|
|
464
|
+
units=row["unit"],
|
|
465
|
+
sources=sources,
|
|
466
|
+
provenance="Parsed from Excel file",
|
|
467
|
+
)
|
|
468
|
+
list_techs.append(
|
|
469
|
+
Technology(
|
|
470
|
+
name=ws,
|
|
471
|
+
region="EU",
|
|
472
|
+
year=year,
|
|
473
|
+
parameters=parameters,
|
|
474
|
+
case=est,
|
|
475
|
+
detailed_technology=technology_name,
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
return TechnologyCollection(technologies=list_techs)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@pydantic.validate_call
|
|
482
|
+
def parse_input_arguments() -> argparse.Namespace:
|
|
483
|
+
"""
|
|
484
|
+
Parse command line arguments.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
argparse.Namespace
|
|
489
|
+
Parsed command line arguments containing:
|
|
490
|
+
- Number of significant digits
|
|
491
|
+
- Store source flag
|
|
492
|
+
|
|
493
|
+
"""
|
|
494
|
+
# Create the parser
|
|
495
|
+
parser = argparse.ArgumentParser(
|
|
496
|
+
description="Parse the DEA technology storage dataset",
|
|
497
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Define arguments
|
|
501
|
+
parser.add_argument(
|
|
502
|
+
"--num_digits",
|
|
503
|
+
type=int,
|
|
504
|
+
default=4,
|
|
505
|
+
help="Name of significant digits to round the values. ",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
parser.add_argument(
|
|
509
|
+
"--store_source",
|
|
510
|
+
action="store_true",
|
|
511
|
+
help="store_source, store the source object on the wayback machine. Default: false",
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
parser.add_argument(
|
|
515
|
+
"--filter_params",
|
|
516
|
+
action="store_true",
|
|
517
|
+
help="filter_params. Filter the parameters stored to technologies.json. Default: false",
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
parser.add_argument(
|
|
521
|
+
"--export_schema",
|
|
522
|
+
action="store_true",
|
|
523
|
+
help="export_schema. Export the Source/TechnologyCollection schemas. Default: false",
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Parse arguments
|
|
527
|
+
args = parser.parse_args()
|
|
528
|
+
|
|
529
|
+
return args
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
if __name__ == "__main__":
|
|
533
|
+
# Parse input arguments
|
|
534
|
+
input_args = parse_input_arguments()
|
|
535
|
+
logger.info("Command line arguments parsed.")
|
|
536
|
+
|
|
537
|
+
# Read the raw data
|
|
538
|
+
dea_energy_storage_file_path = pathlib.Path(
|
|
539
|
+
path_cwd,
|
|
540
|
+
"src",
|
|
541
|
+
"technologydata",
|
|
542
|
+
"package_data",
|
|
543
|
+
"raw",
|
|
544
|
+
"Technology_datasheet_for_energy_storage.xlsx",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
dea_energy_storage_df = pd.read_excel(
|
|
548
|
+
dea_energy_storage_file_path,
|
|
549
|
+
sheet_name="alldata_flat",
|
|
550
|
+
engine="calamine",
|
|
551
|
+
dtype=str,
|
|
552
|
+
)
|
|
553
|
+
logger.info("Input file read-in.")
|
|
554
|
+
|
|
555
|
+
# Drop unnecessary rows
|
|
556
|
+
cleaned_df = drop_invalid_rows(dea_energy_storage_df)
|
|
557
|
+
logger.info("Unnecessary rows dropped.")
|
|
558
|
+
|
|
559
|
+
# Clean technology (Technology) column
|
|
560
|
+
cleaned_df["Technology"] = cleaned_df["Technology"].apply(clean_technology_string)
|
|
561
|
+
|
|
562
|
+
# Clean ws column
|
|
563
|
+
cleaned_df["ws"] = cleaned_df["ws"].apply(clean_technology_string)
|
|
564
|
+
logger.info("`Technology` and `ws` cleaned.")
|
|
565
|
+
|
|
566
|
+
# Clean year column
|
|
567
|
+
cleaned_df["year"] = cleaned_df["year"].apply(extract_year)
|
|
568
|
+
logger.info("`year` column cleaned.")
|
|
569
|
+
|
|
570
|
+
# Clean parameter (par) column
|
|
571
|
+
cleaned_df["par"] = cleaned_df["par"].apply(clean_parameter_string)
|
|
572
|
+
logger.info("`par` column cleaned.")
|
|
573
|
+
|
|
574
|
+
# Complete missing units based on parameter names and replace incorrect units.
|
|
575
|
+
cleaned_df[["par", "unit"]] = cleaned_df[["par", "unit"]].apply(
|
|
576
|
+
standardize_units, axis=1
|
|
577
|
+
)
|
|
578
|
+
logger.info("Missing units added and wrong units replaced.")
|
|
579
|
+
|
|
580
|
+
# Include priceyear in unit if applicable
|
|
581
|
+
cleaned_df["unit"] = cleaned_df.apply(
|
|
582
|
+
lambda row: Commons.update_unit_with_currency_year(
|
|
583
|
+
row["unit"], row["priceyear"]
|
|
584
|
+
),
|
|
585
|
+
axis=1,
|
|
586
|
+
)
|
|
587
|
+
logger.info("`priceyear` included in `unit` column.")
|
|
588
|
+
|
|
589
|
+
# Format value (val) column
|
|
590
|
+
cleaned_df["val"] = cleaned_df["val"].apply(
|
|
591
|
+
lambda x: format_val_number(x, input_args.num_digits)
|
|
592
|
+
)
|
|
593
|
+
logger.info("`val` column formatted.")
|
|
594
|
+
|
|
595
|
+
# Replace "MEUR_2020" with "EUR_2020" and multiply val by 1_000_000
|
|
596
|
+
mask_meur = cleaned_df["unit"].str.contains("MEUR_2020")
|
|
597
|
+
cleaned_df.loc[mask_meur, "unit"] = cleaned_df.loc[mask_meur, "unit"].str.replace(
|
|
598
|
+
"MEUR_2020", "EUR_2020"
|
|
599
|
+
)
|
|
600
|
+
cleaned_df.loc[mask_meur, "val"] = (
|
|
601
|
+
cleaned_df.loc[mask_meur, "val"] * 1_000_000.0
|
|
602
|
+
).round(input_args.num_digits)
|
|
603
|
+
|
|
604
|
+
# Replace "kEUR_2020" with "EUR_2020" and multiply val by 1_000
|
|
605
|
+
mask_lower_keur = cleaned_df["unit"].str.contains("kEUR_2020")
|
|
606
|
+
cleaned_df.loc[mask_lower_keur, "unit"] = cleaned_df.loc[
|
|
607
|
+
mask_lower_keur, "unit"
|
|
608
|
+
].str.replace("kEUR_2020", "EUR_2020")
|
|
609
|
+
cleaned_df.loc[mask_lower_keur, "val"] = (
|
|
610
|
+
cleaned_df.loc[mask_lower_keur, "val"] * 1_000.0
|
|
611
|
+
).round(input_args.num_digits)
|
|
612
|
+
|
|
613
|
+
# Replace "KEUR_2020" with "EUR_2020" and multiply val by 1_000
|
|
614
|
+
mask_upper_keur = cleaned_df["unit"].str.contains("KEUR_2020")
|
|
615
|
+
cleaned_df.loc[mask_upper_keur, "unit"] = cleaned_df.loc[
|
|
616
|
+
mask_upper_keur, "unit"
|
|
617
|
+
].str.replace("KEUR_2020", "EUR_2020")
|
|
618
|
+
cleaned_df.loc[mask_upper_keur, "val"] = (
|
|
619
|
+
cleaned_df.loc[mask_upper_keur, "val"] * 1_000.0
|
|
620
|
+
).round(input_args.num_digits)
|
|
621
|
+
|
|
622
|
+
# Replace "mol/s/m/MPa1/2" with "mol/s/m/Pa" and multiply val by 1_000_000
|
|
623
|
+
mask_mols = cleaned_df["unit"].str.contains("mol/s/m/MPa1/2")
|
|
624
|
+
cleaned_df.loc[mask_mols, "unit"] = cleaned_df.loc[mask_mols, "unit"].str.replace(
|
|
625
|
+
"mol/s/m/MPa1/2", "mol/s/m/Pa"
|
|
626
|
+
)
|
|
627
|
+
cleaned_df.loc[mask_mols, "val"] = cleaned_df.loc[mask_mols, "val"] * 1_000_000.0
|
|
628
|
+
|
|
629
|
+
# Replace, in column `par`, `energy storage capacity for one unit` and `tank volume of example` with `capacity`
|
|
630
|
+
mask_capacity = cleaned_df["par"].isin(
|
|
631
|
+
[
|
|
632
|
+
"energy storage capacity for one unit",
|
|
633
|
+
"tank volume of example",
|
|
634
|
+
]
|
|
635
|
+
)
|
|
636
|
+
cleaned_df.loc[mask_capacity, "par"] = "capacity"
|
|
637
|
+
|
|
638
|
+
# Clean est column
|
|
639
|
+
cleaned_df["est"] = cleaned_df["est"].apply(clean_est_string)
|
|
640
|
+
logger.info("`est` column cleaned.")
|
|
641
|
+
|
|
642
|
+
# Drop unnecessary columns
|
|
643
|
+
columns_to_drop = ["cat", "priceyear", "ref", "note"]
|
|
644
|
+
cleaned_df = cleaned_df.drop(columns=columns_to_drop, errors="ignore")
|
|
645
|
+
logger.info("Unnecessary columns dropped.")
|
|
646
|
+
|
|
647
|
+
filtered_df = filter_parameters(cleaned_df, input_args.filter_params)
|
|
648
|
+
|
|
649
|
+
# Build TechnologyCollection
|
|
650
|
+
dea_storage_path = pathlib.Path(
|
|
651
|
+
path_cwd,
|
|
652
|
+
"src",
|
|
653
|
+
"technologydata",
|
|
654
|
+
"package_data",
|
|
655
|
+
"dea_energy_storage",
|
|
656
|
+
)
|
|
657
|
+
output_technologies_path = pathlib.Path(
|
|
658
|
+
dea_storage_path,
|
|
659
|
+
"technologies.json",
|
|
660
|
+
)
|
|
661
|
+
output_sources_path = pathlib.Path(
|
|
662
|
+
dea_storage_path,
|
|
663
|
+
"sources.json",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
tech_col = build_technology_collection(
|
|
667
|
+
filtered_df,
|
|
668
|
+
output_sources_path,
|
|
669
|
+
store_source=input_args.store_source,
|
|
670
|
+
output_schema=input_args.export_schema,
|
|
671
|
+
)
|
|
672
|
+
logger.info("TechnologyCollection object instantiated.")
|
|
673
|
+
tech_col.to_json(output_technologies_path, output_schema=input_args.export_schema)
|
|
674
|
+
logger.info("TechnologyCollection object exported to json.")
|
|
675
|
+
|
|
676
|
+
if input_args.export_schema:
|
|
677
|
+
# Move schema files if they exist
|
|
678
|
+
schema_folder = pathlib.Path(
|
|
679
|
+
path_cwd, "src", "technologydata", "package_data", "schemas"
|
|
680
|
+
)
|
|
681
|
+
sources_schema = pathlib.Path(dea_storage_path, "sources.schema.json")
|
|
682
|
+
technologies_schema = pathlib.Path(dea_storage_path, "technologies.schema.json")
|
|
683
|
+
|
|
684
|
+
schema_folder.mkdir(parents=True, exist_ok=True)
|
|
685
|
+
|
|
686
|
+
if sources_schema.exists():
|
|
687
|
+
sources_schema.rename(schema_folder / "sources.schema.json")
|
|
688
|
+
logger.info(f"Moved {sources_schema} to {schema_folder}")
|
|
689
|
+
if technologies_schema.exists():
|
|
690
|
+
technologies_schema.rename(schema_folder / "technologies.schema.json")
|
|
691
|
+
logger.info(f"Moved {technologies_schema} to {schema_folder}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sources": [
|
|
3
|
+
{
|
|
4
|
+
"title": "Technology Data for Energy storage (May 2025)",
|
|
5
|
+
"authors": "Danish Energy Agency",
|
|
6
|
+
"url": "https://ens.dk/media/6589/download",
|
|
7
|
+
"url_archive": "https://web.archive.org/web/20251008092400/https://ens.dk/media/6589/download",
|
|
8
|
+
"url_date": "2025-10-08 09:24:00",
|
|
9
|
+
"url_date_archive": "2025-10-08 09:24:00"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|