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,519 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Submodule containing pint.UnitRegistry subclasses and utility functions for handling units, conversions, and currency units."""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import typing
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import pint
|
|
17
|
+
import pydeflate
|
|
18
|
+
from frozendict import frozendict
|
|
19
|
+
from hdx.location.country import Country
|
|
20
|
+
from platformdirs import user_cache_dir
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
pydeflate.set_pydeflate_path("./pydeflate_data")
|
|
25
|
+
|
|
26
|
+
CURRENCY_UNIT_PATTERN = re.compile(r"\b(?P<cu_iso3>[A-Z]{3})_(?P<year>\d{4})\b")
|
|
27
|
+
|
|
28
|
+
# Set up cache directory and file for currency codes
|
|
29
|
+
CACHE_DIR = Path(user_cache_dir("technologydata")) # TODO move to commons?
|
|
30
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True) # TODO move to commons?
|
|
31
|
+
CURRENCY_CODES_CACHE = CACHE_DIR / "iso3_to_currency_codes.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SPECIAL_CASES_CURRENCY_CODE_TO_ISO3 = frozendict(
|
|
35
|
+
{
|
|
36
|
+
# Multi-country currencies (return codes directly)
|
|
37
|
+
"EUR": "EUR", # Eurozone
|
|
38
|
+
# Single primary countries
|
|
39
|
+
"AUD": "AUS", # Australia
|
|
40
|
+
"CHF": "CHE", # Switzerland
|
|
41
|
+
"DKK": "DNK", # Denmark
|
|
42
|
+
"GBP": "GBR", # United Kingdom (largest GBP economy)
|
|
43
|
+
"ILS": "ISR", # Israel
|
|
44
|
+
"MAD": "MAR", # Morocco
|
|
45
|
+
"NOK": "NOR", # Norway
|
|
46
|
+
"NZD": "NZL", # New Zealand
|
|
47
|
+
"USD": "USA", # Dollar-ized economies
|
|
48
|
+
# Special regional cases with proxy selection criteria
|
|
49
|
+
"ANG": "CUW", # Curaçao (GDP-weighted proxy)
|
|
50
|
+
"XAF": "CAF", # Central African Republic (lowest inflation differential)
|
|
51
|
+
"XCD": "GRD", # Grenada (lowest inflation differential)
|
|
52
|
+
"XOF": "NER", # Niger (lowest inflation differential 2015-2023)
|
|
53
|
+
"XPF": "PYF", # French Polynesia (data availability)
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_iso3_to_currency_codes(
|
|
59
|
+
refresh: bool = False, ignore_cache: bool = False
|
|
60
|
+
) -> dict[str, str]:
|
|
61
|
+
"""
|
|
62
|
+
Get all 3-letter currency codes from official UN data.
|
|
63
|
+
|
|
64
|
+
Uses a persistent local cache to avoid unnecessary network requests.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
refresh : bool, optional
|
|
69
|
+
If True, the cache will be updated from the internet, by default False.
|
|
70
|
+
ignore_cache : bool, optional
|
|
71
|
+
If True, the cache will not be used and the data will always be fetched from the live feed.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
dict[str, str]
|
|
76
|
+
A dictionary mapping ISO3 country codes to their corresponding 3-letter currency codes.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
currencies: dict[str, str] = {}
|
|
80
|
+
|
|
81
|
+
if refresh:
|
|
82
|
+
logger.debug("Deleting existing currency codes cache to refresh it.")
|
|
83
|
+
CURRENCY_CODES_CACHE.unlink(missing_ok=True)
|
|
84
|
+
|
|
85
|
+
if ignore_cache:
|
|
86
|
+
logger.debug("Ignoring cache and fetching live currency codes.")
|
|
87
|
+
currencies = Country.countriesdata()["currencies"]
|
|
88
|
+
elif not CURRENCY_CODES_CACHE.exists():
|
|
89
|
+
logger.debug(
|
|
90
|
+
"Cache does not exist. Fetching live currency codes and creating cache."
|
|
91
|
+
)
|
|
92
|
+
currencies = Country.countriesdata()["currencies"]
|
|
93
|
+
with open(CURRENCY_CODES_CACHE, "w") as f:
|
|
94
|
+
json.dump(currencies, f)
|
|
95
|
+
else:
|
|
96
|
+
logger.debug("Reading currency codes from cache.")
|
|
97
|
+
with open(CURRENCY_CODES_CACHE) as f:
|
|
98
|
+
currencies = json.load(f)
|
|
99
|
+
|
|
100
|
+
return currencies
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def extract_currency_units(units: str | pint.Unit) -> list[str]:
|
|
104
|
+
"""
|
|
105
|
+
Extract currency-like strings from a string or pint.Unit.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
units : str or pint.Unit
|
|
110
|
+
The units string or pint.Unit from which to extract currency-like strings.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
list[str]
|
|
115
|
+
A list of currency-like strings found in the input, formatted as "{3-letter currency code}_{year as YYYY}".
|
|
116
|
+
If no matches are found, an empty list is returned.
|
|
117
|
+
|
|
118
|
+
Examples
|
|
119
|
+
--------
|
|
120
|
+
>>> extract_currency_units("USD_2020/kW")
|
|
121
|
+
["USD_2020"]
|
|
122
|
+
|
|
123
|
+
>>> extract_currency_units("EUR_2015/USD_2020")
|
|
124
|
+
["EUR_2015", "USD_2020"]
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
# Ensure that the input is a string
|
|
128
|
+
units = str(units)
|
|
129
|
+
|
|
130
|
+
# Get the 3-letter currency codes for all officially recognized currencies
|
|
131
|
+
logger.debug("Retrieving all 3-letter currency codes from the `hdx-country`.")
|
|
132
|
+
all_currency_codes = set(get_iso3_to_currency_codes().values())
|
|
133
|
+
|
|
134
|
+
# Check if the units contain a currency-like string, defined as "{3-letter currency code}_{year as YYYY}"
|
|
135
|
+
matches = CURRENCY_UNIT_PATTERN.findall(units)
|
|
136
|
+
if len(matches) == 0:
|
|
137
|
+
logger.debug("No currency-like string found in the units.")
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
# Extract the currency codes from the matches using the regex groups
|
|
141
|
+
logger.debug(f"Found currency-like strings in the units: {matches}")
|
|
142
|
+
currency_codes = {code for code, year in matches}
|
|
143
|
+
|
|
144
|
+
# Ensure that all currency codes are legitimate 3-letter currency codes
|
|
145
|
+
invalid_codes = currency_codes - all_currency_codes
|
|
146
|
+
if invalid_codes:
|
|
147
|
+
invalid_currencies = [
|
|
148
|
+
f"{code}_{year}" for code, year in matches if code in invalid_codes
|
|
149
|
+
]
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"The following unit(s) appear to be currency units, but have invalid 3-letter currency codes: {', '.join(invalid_currencies)}. "
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Reconstruct currency units from the matches
|
|
155
|
+
matches = [f"{code}_{year}" for code, year in matches]
|
|
156
|
+
|
|
157
|
+
return matches
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@lru_cache
|
|
161
|
+
def get_conversion_rate(
|
|
162
|
+
from_iso3: str,
|
|
163
|
+
to_iso3: str,
|
|
164
|
+
country: str,
|
|
165
|
+
from_year: int,
|
|
166
|
+
to_year: int,
|
|
167
|
+
source: str = "worldbank",
|
|
168
|
+
) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Get the conversion rate from one currency (year, ISO3) to another currency (year, ISO3) from pydeflate.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
from_iso3 : str
|
|
175
|
+
The ISO3 code of the country of the currency to convert from (e.g., 'USA' if the source currency is USD).
|
|
176
|
+
to_iso3 : str
|
|
177
|
+
The ISO3 code of the country of the currency to convert to (e.g., 'DEU' if the target currency is EUR).
|
|
178
|
+
country : str
|
|
179
|
+
The ISO3 code of the country to adjust for inflation.
|
|
180
|
+
from_year : int
|
|
181
|
+
The julian year (YYYY) of the source currency.
|
|
182
|
+
to_year : int
|
|
183
|
+
The julian year (YYYY) of the target currency.
|
|
184
|
+
source : str
|
|
185
|
+
The source of the inflation data ('worldbank'/'wb' or 'international_monetary_fund'/'imf').
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
# Choose the deflation function based on the source
|
|
189
|
+
deflation_function = {
|
|
190
|
+
"worldbank": pydeflate.wb_gdp_deflate,
|
|
191
|
+
"wb": pydeflate.wb_gdp_deflate,
|
|
192
|
+
"international_monetary_fund": pydeflate.imf_gdp_deflate,
|
|
193
|
+
"imf": pydeflate.imf_gdp_deflate,
|
|
194
|
+
}[source]
|
|
195
|
+
|
|
196
|
+
# Ensure that `country` is a valid ISO3 code; to_iso3 and from_iso3 should already have been parsed before the function was called
|
|
197
|
+
if country not in get_iso3_to_currency_codes().keys():
|
|
198
|
+
raise ValueError(f"Unknown ISO3 code for `country`: {country}.")
|
|
199
|
+
|
|
200
|
+
# pydeflate only operates on pandas.DataFrame
|
|
201
|
+
data = pd.DataFrame(
|
|
202
|
+
{
|
|
203
|
+
"iso3": [country],
|
|
204
|
+
"from_year": [from_year],
|
|
205
|
+
"value": [1],
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Deflate values include currency conversion
|
|
210
|
+
conversion_rates = deflation_function(
|
|
211
|
+
data,
|
|
212
|
+
source_currency=from_iso3,
|
|
213
|
+
target_currency=to_iso3,
|
|
214
|
+
id_column="iso3",
|
|
215
|
+
year_column="from_year",
|
|
216
|
+
base_year=to_year,
|
|
217
|
+
value_column="value",
|
|
218
|
+
target_value_column="new_value",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if conversion_rates.isna().any().any():
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"Conversion rate from {from_iso3} ({from_year}) to {to_iso3} ({to_year}) with inflation rate for {country} not found. "
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return float(conversion_rates.loc[0, "new_value"])
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@lru_cache
|
|
230
|
+
def get_iso3_from_currency_code(
|
|
231
|
+
currency_code: str,
|
|
232
|
+
special_cases: frozendict[str, str] = SPECIAL_CASES_CURRENCY_CODE_TO_ISO3,
|
|
233
|
+
) -> str:
|
|
234
|
+
"""
|
|
235
|
+
Get the ISO3 country code from a 3-letter currency code using official UN data and opinionated assumptions.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
currency_code : str
|
|
240
|
+
The 3-letter currency code (e.g., 'USD', 'EUR').
|
|
241
|
+
special_cases : dict[str, str], optional
|
|
242
|
+
A dictionary mapping specific currency codes to their ISO3 country codes for special cases.
|
|
243
|
+
Defaults to `technologydata.utils.units.SPECIAL_CASES_CURRENCY_CODE_TO_ISO3`.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
str
|
|
248
|
+
The ISO 3166 alpha 3 country code of the `currency_code`.
|
|
249
|
+
|
|
250
|
+
Raises
|
|
251
|
+
------
|
|
252
|
+
ValueError
|
|
253
|
+
If the currency code is not found in the official list of currencies.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
# Build reverse mapping: currency code -> list of ISO3 codes
|
|
257
|
+
iso3_to_currency_codes = pd.DataFrame.from_dict(
|
|
258
|
+
get_iso3_to_currency_codes(), orient="index", columns=["currency"]
|
|
259
|
+
)
|
|
260
|
+
iso3_to_currency_codes = iso3_to_currency_codes.reset_index(drop=False).rename(
|
|
261
|
+
columns={"index": "iso3"}
|
|
262
|
+
)
|
|
263
|
+
currency_codes_to_iso3 = iso3_to_currency_codes.groupby("currency", as_index=False)[
|
|
264
|
+
"iso3"
|
|
265
|
+
].agg(list)
|
|
266
|
+
|
|
267
|
+
# Handle special cases for specific currency codes
|
|
268
|
+
|
|
269
|
+
# Remove all currencies that are in the special cases from the mapping
|
|
270
|
+
currency_codes_to_iso3 = currency_codes_to_iso3.loc[
|
|
271
|
+
~currency_codes_to_iso3["currency"].isin(special_cases.keys())
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
# Mapping should now only contain unique currency codes to ISO3 codes mappings, safe to explode
|
|
275
|
+
currency_codes_to_iso3 = currency_codes_to_iso3.explode("iso3")
|
|
276
|
+
|
|
277
|
+
# Add the special cases to the mapping
|
|
278
|
+
currency_codes_to_iso3 = pd.concat(
|
|
279
|
+
[
|
|
280
|
+
currency_codes_to_iso3,
|
|
281
|
+
pd.DataFrame(
|
|
282
|
+
list(special_cases.items()),
|
|
283
|
+
columns=["currency", "iso3"],
|
|
284
|
+
),
|
|
285
|
+
],
|
|
286
|
+
ignore_index=False,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Special cases should handle all non-unique currency codes, check to make sure
|
|
290
|
+
# and return the ones that are not handled in special cases
|
|
291
|
+
|
|
292
|
+
if (
|
|
293
|
+
duplicated_iso3 := currency_codes_to_iso3.explode("iso3")[
|
|
294
|
+
"currency"
|
|
295
|
+
].duplicated()
|
|
296
|
+
).any():
|
|
297
|
+
raise ValueError(
|
|
298
|
+
"Some currency codes are used by multiple ISO3 codes but are not handled in `special_cases` "
|
|
299
|
+
"and need to be added to the mapping: "
|
|
300
|
+
f"{currency_codes_to_iso3[duplicated_iso3]}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
currency_codes_to_iso3 = currency_codes_to_iso3.set_index("currency")[
|
|
304
|
+
"iso3"
|
|
305
|
+
].to_dict()
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
return str(currency_codes_to_iso3[currency_code])
|
|
309
|
+
except KeyError as e:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Currency code '{currency_code}' not found in the list of currencies. "
|
|
312
|
+
"Please ensure it is a valid 3-letter currency code."
|
|
313
|
+
) from e
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def patch_pint_registry_error_handling(registry: pint.registry.UnitRegistry) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Patch a Pint registry to use CustomUndefinedUnitError.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
registry : pint.registry.UnitRegistry
|
|
323
|
+
The Pint unit registry to patch.
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
# Store the original method
|
|
327
|
+
original_get_name = registry.get_name
|
|
328
|
+
|
|
329
|
+
def patched_get_name(
|
|
330
|
+
self: pint.registry.UnitRegistry, name: str, *args: Any, **kwargs: Any
|
|
331
|
+
) -> Any:
|
|
332
|
+
try:
|
|
333
|
+
return original_get_name(name, *args, **kwargs)
|
|
334
|
+
except pint.errors.UndefinedUnitError as e:
|
|
335
|
+
# Raise the custom error with the same arguments
|
|
336
|
+
raise CustomUndefinedUnitError(e.args[0]) from e
|
|
337
|
+
|
|
338
|
+
# Replace the method
|
|
339
|
+
registry.get_name = patched_get_name.__get__(registry)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class CustomUndefinedUnitError(pint.errors.UndefinedUnitError): # type: ignore
|
|
343
|
+
"""
|
|
344
|
+
Custom message for undefined unit errors.
|
|
345
|
+
|
|
346
|
+
This custom error is raised when a unit is not defined in the unit registry.
|
|
347
|
+
It provides more specific error messages, especially for currency units
|
|
348
|
+
that are missing the currency year.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
unit_names : list of str
|
|
353
|
+
The names of the units that are not defined in the unit registry.
|
|
354
|
+
|
|
355
|
+
Attributes
|
|
356
|
+
----------
|
|
357
|
+
unit_names : list of str
|
|
358
|
+
The names of the units that are not defined in the unit registry.
|
|
359
|
+
|
|
360
|
+
Notes
|
|
361
|
+
-----
|
|
362
|
+
This error is a subclass of `pint.errors.UndefinedUnitError` and is designed
|
|
363
|
+
to provide more specific error messages for currency units that are missing
|
|
364
|
+
the currency year.
|
|
365
|
+
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(self, unit_names: str | typing.Iterable[str]) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Initialize a CustomUndefinedUnitError instance.
|
|
371
|
+
|
|
372
|
+
This constructor creates a new `CustomUndefinedUnitError` object,
|
|
373
|
+
inheriting all default behaviors from `pint.errors.UndefinedUnitError`.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
unit_names : str or iterable of str
|
|
378
|
+
The name or names of the undefined units that caused the error.
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
# Use all defaults definitions from a standard pint.errors.UndefinedUnitError
|
|
382
|
+
super().__init__(unit_names)
|
|
383
|
+
|
|
384
|
+
def __str__(self) -> str:
|
|
385
|
+
"""
|
|
386
|
+
Generate a custom error message string.
|
|
387
|
+
|
|
388
|
+
This method generates a custom error message string based on the
|
|
389
|
+
unit names that are not defined in the unit registry. It provides
|
|
390
|
+
specific messages for currency units that are missing the currency year.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
str
|
|
395
|
+
The custom error message string.
|
|
396
|
+
|
|
397
|
+
"""
|
|
398
|
+
# Retrieve unit names, defaulting to an empty list if not present
|
|
399
|
+
unit_names = getattr(self, "unit_names", [])
|
|
400
|
+
|
|
401
|
+
# Precompute valid currency codes for efficiency
|
|
402
|
+
all_currency_codes = set(get_iso3_to_currency_codes().values())
|
|
403
|
+
|
|
404
|
+
# Identify currency units without a year specification
|
|
405
|
+
currency_errors = [
|
|
406
|
+
code
|
|
407
|
+
for unit in unit_names
|
|
408
|
+
for code in re.findall(r"[A-Z]{3}", str(unit))
|
|
409
|
+
if code in all_currency_codes
|
|
410
|
+
and not CURRENCY_UNIT_PATTERN.search(str(unit))
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
# Generate specific error message for currency units
|
|
414
|
+
if currency_errors:
|
|
415
|
+
missing_code = currency_errors[0]
|
|
416
|
+
return f"Currency unit '{missing_code}' is missing the 4-digit currency year (e.g. {missing_code}_2020)."
|
|
417
|
+
|
|
418
|
+
# Fallback to parent class error message if no specific currency error found
|
|
419
|
+
return super().__str__() # type: ignore
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class SpecialUnitRegistry(pint.UnitRegistry): # type: ignore
|
|
423
|
+
"""A special pint.UnitRegistry subclass that includes methods for handling currency units and conversion using pydeflate."""
|
|
424
|
+
|
|
425
|
+
def __init__(self, *args: tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
|
|
426
|
+
"""
|
|
427
|
+
Initialize a SpecialUnitRegistry instance.
|
|
428
|
+
|
|
429
|
+
This constructor creates a new `SpecialUnitRegistry` object,
|
|
430
|
+
inheriting all default behaviors from `pint.UnitRegistry`.
|
|
431
|
+
It also defines a reference currency unit (`USD_2020`) for
|
|
432
|
+
handling currency conversions and related operations.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
*args : tuple
|
|
437
|
+
Positional arguments passed to the base `pint.UnitRegistry` constructor.
|
|
438
|
+
**kwargs : dict
|
|
439
|
+
Keyword arguments passed to the base `pint.UnitRegistry` constructor.
|
|
440
|
+
|
|
441
|
+
"""
|
|
442
|
+
# Use all defaults definitions from a standard pint.UnitRegistry
|
|
443
|
+
super().__init__(*args, **kwargs)
|
|
444
|
+
|
|
445
|
+
# Define the reference currency unit
|
|
446
|
+
# This is the base currency unit that all other currency units will be defined relative to
|
|
447
|
+
# We use USD_2020 as the base currency unit because it is a currency that we can relate all other currencies to
|
|
448
|
+
self.define("USD_2020 = [currency]")
|
|
449
|
+
|
|
450
|
+
def get_reference_currency(self) -> str:
|
|
451
|
+
"""Get the reference currency from the unit registry."""
|
|
452
|
+
reference_currency = [
|
|
453
|
+
self._units[u].name
|
|
454
|
+
for u in self._units
|
|
455
|
+
if "[currency]" in self._units[u].reference
|
|
456
|
+
]
|
|
457
|
+
if not reference_currency or len(reference_currency) != 1:
|
|
458
|
+
raise ValueError(
|
|
459
|
+
"The unit registry does not have a unique base currency defined as '[currency]'. Please define a base currency unit to proceed."
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return str(reference_currency[0])
|
|
463
|
+
|
|
464
|
+
def ensure_currency_is_unit(self, units: str) -> None:
|
|
465
|
+
"""
|
|
466
|
+
Ensure that all currency units in the given string are valid units in the registry.
|
|
467
|
+
|
|
468
|
+
Extracts all currency-like strings from the input and checks if they are defined
|
|
469
|
+
in the unit registry. If they are not defined, they are added as valid units
|
|
470
|
+
relative to the reference currency but without a conversion factor.
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
units : str
|
|
475
|
+
The units string to check for currency units.
|
|
476
|
+
|
|
477
|
+
Examples
|
|
478
|
+
--------
|
|
479
|
+
>>> ureg.ensure_currency_is_unit("USD_2020/kW")
|
|
480
|
+
>>> ureg.ensure_currency_is_unit("EUR_2015/USD_2020")
|
|
481
|
+
|
|
482
|
+
"""
|
|
483
|
+
logger.debug(f"Ensuring currency units of '{units}' are defined in `ureg`")
|
|
484
|
+
currency_units = extract_currency_units(units)
|
|
485
|
+
logger.debug(f"Found currency-like strings in the units: {currency_units}")
|
|
486
|
+
|
|
487
|
+
if not currency_units:
|
|
488
|
+
# Nothing to do
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
reference_currency = self.get_reference_currency()
|
|
492
|
+
logger.debug(f"Reference currency is '{reference_currency}'.")
|
|
493
|
+
|
|
494
|
+
# Check if the currency unit is already defined in the unit registry
|
|
495
|
+
# if not, define it relative to the base currency USD_2015
|
|
496
|
+
for currency_unit in currency_units:
|
|
497
|
+
if currency_unit in self._units:
|
|
498
|
+
logger.debug(
|
|
499
|
+
f"Currency unit '{currency_unit}' is already defined in the unit registry. Not redefining it."
|
|
500
|
+
)
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
logger.debug(
|
|
504
|
+
f"Currency unit '{currency_unit}' not found in the unit registry. "
|
|
505
|
+
f"Defining it without a conversion factor relative to the base currency '{reference_currency}'."
|
|
506
|
+
)
|
|
507
|
+
self.define(f"{currency_unit} = nan {reference_currency}")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Unit registries used throughout the package for different purposes
|
|
511
|
+
ureg = SpecialUnitRegistry() # For handling units, conversions, and currency units
|
|
512
|
+
creg = pint.UnitRegistry(
|
|
513
|
+
filename=Path(__file__).parent / "carriers.txt"
|
|
514
|
+
) # For tracking carriers and ensuring compatibility between them
|
|
515
|
+
hvreg = pint.UnitRegistry(
|
|
516
|
+
filename=Path(__file__).parent / "heating_values.txt"
|
|
517
|
+
) # For tracking heating values and ensuring compatibility between them
|
|
518
|
+
|
|
519
|
+
patch_pint_registry_error_handling(ureg)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: technologydata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Package for providing common data assumptions for energy system modelling on techno-economics and macro-economics.
|
|
5
|
+
Author-email: Contributors to technologydata <johannes.hampp@openenergytransition.org>, Contributors to technologydata <fabrizio.finozzi.business@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/open-energy-transition/technology-data
|
|
7
|
+
Project-URL: Source, https://github.com/open-energy-transition/technology-data
|
|
8
|
+
Project-URL: BugTracker, https://github.com/open-energy-transition/technology-data/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/open-energy-transition/technology-data/releases
|
|
10
|
+
Keywords: energy,modelling,techno-economics,macroeconomics
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: frozendict>=2.4.6
|
|
22
|
+
Requires-Dist: hdx-python-country>=3.9.6
|
|
23
|
+
Requires-Dist: mypy>=1.15.0
|
|
24
|
+
Requires-Dist: pandas>=2.2.3
|
|
25
|
+
Requires-Dist: pint>=0.24.4
|
|
26
|
+
Requires-Dist: pydantic>=2.11.7
|
|
27
|
+
Requires-Dist: pydeflate>=2.3.3
|
|
28
|
+
Requires-Dist: requests>=2.32.3
|
|
29
|
+
Requires-Dist: savepagenow>=1.3.0
|
|
30
|
+
Requires-Dist: scipy>=1.16.1
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# technologydata
|
|
34
|
+
|
|
35
|
+
<!--
|
|
36
|
+
|
|
37
|
+
TODO: Add badges
|
|
38
|
+
**Suggestions:**
|
|
39
|
+
|
|
40
|
+
- Use http://shields.io or a similar service to create and host the images.
|
|
41
|
+
- Add the [Standard Readme badge](https://github.com/RichardLitt/standard-readme#badge).
|
|
42
|
+
|
|
43
|
+
-->
|
|
44
|
+
|
|
45
|
+
A Python package to manage techno-economic assumptions for energy system models.
|
|
46
|
+
|
|
47
|
+
## Overview
|
|
48
|
+
|
|
49
|
+
`technologydata` is a Python package that supports the management of techno-economic assumptions for energy system models.
|
|
50
|
+
It provides a structured way to store, retrieve, and manipulate data related to various technologies used in energy systems,
|
|
51
|
+
including unit-ful parameters, currency conversions, inflation adjustment, and temporal modelling.
|
|
52
|
+
|
|
53
|
+
The package currently includes a pre-parsed dataset from the DEA's Technology Catalogue focusing on energy storage technologies and a dedicated parser (see [here](docs/examples/dea_storage.md)). In the future it will include other common public data sources such NREL's ATB.
|
|
54
|
+
|
|
55
|
+
The goal of this package is to make energy system modelling easier and more efficient,
|
|
56
|
+
automating common tasks and transformations to reduce errors and allowing for easier data exchange between models.
|
|
57
|
+
|
|
58
|
+
## Table of Contents
|
|
59
|
+
|
|
60
|
+
1. [Background](#background)
|
|
61
|
+
2. [Install](#install)
|
|
62
|
+
3. [Usage](#usage)
|
|
63
|
+
4. [Maintainers](#maintainers)
|
|
64
|
+
5. [Thanks](#thanks)
|
|
65
|
+
6. [Contributing](#contributing)
|
|
66
|
+
7. [License](#license)
|
|
67
|
+
|
|
68
|
+
## Background
|
|
69
|
+
|
|
70
|
+
> Modelling is 10% science, 10% art, and 80% finding the right data and getting it into the right format.
|
|
71
|
+
> — Every energy modeller ever
|
|
72
|
+
|
|
73
|
+
Modelling energy systems requires a lot of data.
|
|
74
|
+
Techno-economic data, i.e. data about the costs for building operating technologies and their technical
|
|
75
|
+
characteristics, is a key input to many energy system models today.
|
|
76
|
+
|
|
77
|
+
Techno-economic data is usually collected from a variety of scattered sources and then manually processed
|
|
78
|
+
into the format required by a specific model.
|
|
79
|
+
This is repeated for every new modelling project, leading to a lot of duplicated effort.
|
|
80
|
+
The manual processing also carries a high risk of errors, which can lead to misleading results.
|
|
81
|
+
|
|
82
|
+
When projects are finished, the processed data is often discarded, leading to a loss of valuable information.
|
|
83
|
+
In better cases, the processed data is also published along with the model results, but usually in a
|
|
84
|
+
non-standardised and non-machine-readable format and without information about the data provenance and processing steps.
|
|
85
|
+
|
|
86
|
+
It sounds abstract, but if someone used cost assumptions for the US in 2015 USD, then there are many wrong ways
|
|
87
|
+
and a few right ways to convert these to e.g. EUR and adjust it for inflation to 2023.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
The package is currently under development. A pre-release is published to `PyPI`. The package is not yet available on `conda-forge`.
|
|
92
|
+
|
|
93
|
+
To install the package
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install technologydata
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Alternatively, to install the package locally from GitHub, first clone the package and then use `uv` to install it in editable mode:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/open-energy-transition/technology-data/tree/prototype-2
|
|
103
|
+
cd technology-data
|
|
104
|
+
git checkout prototype-2
|
|
105
|
+
uv sync --group dev --group docs
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
Detailed usage instructions and examples can be found in the available documentation. To build the documentation locally, follow the steps outlined [here](docs/contributing/instructions.md#building-the-documentation-locally).
|
|
111
|
+
|
|
112
|
+
## Maintainers
|
|
113
|
+
|
|
114
|
+
This repository is currently maintained by [Open Energy Transition](https://openenergytransition.org/) with the maintainers and developers being:
|
|
115
|
+
|
|
116
|
+
- [euronion](https://github.com/euronion)
|
|
117
|
+
- [finozzifa](https://github.com/finozzifa)
|
|
118
|
+
|
|
119
|
+
## Thanks
|
|
120
|
+
|
|
121
|
+
Development of this prototype package would not have been possible without the funding from [Breakthrough Energy](https://www.breakthroughenergy.org/).
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
For contributing instructions, guidelines and our code of conduct, please refer to the [contributing section](docs/contributing) in the documentation.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
This project is licensed under the [MIT License](LICENSES/MIT.txt).
|
|
130
|
+
|
|
131
|
+
Primary data included in the repository may be licensed under specific terms.
|
|
132
|
+
Processed data included in the project is licensed under [Creative Commons Attribution 4.0 International (CC BY 4.0)](LICENSES/CC-BY-4.0.txt).
|
|
133
|
+
|
|
134
|
+
To make it easier to identify which data is licensed under which terms, this repository follows the [REUSE](https://reuse.software/) specification.
|
|
135
|
+
This means you can find the license information for each file either located in its header or in [REUSE.toml](REUSE.toml).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
technologydata/__init__.py,sha256=pPDx9m_E8UIHrjN2NyWNwFUhX6gmr4z0TwEoncXmTk0,1141
|
|
2
|
+
technologydata/datapackage.py,sha256=Ds7rOn1RQyMTmXCW0-gfV7nwVXMNx_7HzdGJxARvJEg,4573
|
|
3
|
+
technologydata/parameter.py,sha256=-8RNs7mgJJtUw607Qvm2xRYOUNN-newiBP1YH7ds_jE,25356
|
|
4
|
+
technologydata/source.py,sha256=ylPTclsoDUwoMmZeYYJe5BtXC_ANdTnpshGrX1ZorJE,15534
|
|
5
|
+
technologydata/source_collection.py,sha256=ikZ6gzYqLYEa38l02h8MlbZPwdjCk7IwJxcqO35NNOM,7408
|
|
6
|
+
technologydata/technology.py,sha256=oKQFGGb-KBWXywW4ezxW7oDG4c9DuiYSXS6nMrbt-mU,5471
|
|
7
|
+
technologydata/technology_collection.py,sha256=_pxgYpPiwAl-hPrWJtWI_OXKzqw2aViSc74FDFhSaNY,16795
|
|
8
|
+
technologydata/constants/__init__.py,sha256=vzOKfJftmW4_vexAVweruWUHUPcQod2kYtrtxiRy09Y,337
|
|
9
|
+
technologydata/constants/energy_density.py,sha256=nKQpYuX0a1pQxPLB_yNZbCsN9_Z5vFArjbjRaA5Dkl8,11343
|
|
10
|
+
technologydata/package_data/dea_energy_storage/dea_energy_storage.py,sha256=J3ZwG6krQb0L5tHuccfOa4SAFy1-9G1dmGMhOPYgxao,21289
|
|
11
|
+
technologydata/package_data/dea_energy_storage/sources.json,sha256=w-Or7idq7OZ3TwaBBBSWyhe94rGgWmgVv8IzlNE2j2w,430
|
|
12
|
+
technologydata/package_data/dea_energy_storage/technologies.json,sha256=tekO_moWQ6BVQ5CKba-ZwhDJXQEw0uVl1Ox0LY-8YQo,847127
|
|
13
|
+
technologydata/package_data/raw/Technology_datasheet_for_energy_storage.xlsx,sha256=w5zl4Vqj_laG1tdhIKutajVEhTNmGEUe4-BhnjV7TzI,329612
|
|
14
|
+
technologydata/technologies/__init__.py,sha256=2gqJCJ_vk84WI-Jx1d0aph-0cykQS1fTPE3C8G1e4Y4,161
|
|
15
|
+
technologydata/technologies/growth_models.py,sha256=qrqwB0abyn_ylUMQpAs4Ena8vu22KyI69HuOWmoCyV4,16400
|
|
16
|
+
technologydata/utils/__init__.py,sha256=gjxOGJZEXABJCJb7kbt_EA5X5Yf02jsm_3fFOYbCt-s,348
|
|
17
|
+
technologydata/utils/carriers.txt,sha256=xGRHyCFTAlrmdqEU8nCTE0ITX_VjsoNL0xmivbTA9Tw,1016
|
|
18
|
+
technologydata/utils/commons.py,sha256=U768ySx7fHkW_4u-fPDAYQ2aINnp3Xd9w4tZqnz2dZI,11276
|
|
19
|
+
technologydata/utils/heating_values.txt,sha256=zkjImePSMf1JO0DdGx2pGanQH-ce05gj8Lva5-qFzeA,668
|
|
20
|
+
technologydata/utils/units.py,sha256=VqxLXOV2YUjwMUO8pFm4XvE6AQXRladCYdC4hyStCMc,18509
|
|
21
|
+
technologydata-0.1.0.dist-info/licenses/LICENSE,sha256=sFeF-fGOZxa6tjQksRRUUTuZQ6IiWVtwQRAJIC_FkrU,1078
|
|
22
|
+
technologydata-0.1.0.dist-info/METADATA,sha256=-MdTVX74zWSbXrrmpC_RWkWM0yQwTURLxxwxlYE2ko0,6000
|
|
23
|
+
technologydata-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
technologydata-0.1.0.dist-info/top_level.txt,sha256=CUb44X9b9SGiX4eT20c8B6Fh9N8iF-bRFXXeApkymRA,15
|
|
25
|
+
technologydata-0.1.0.dist-info/RECORD,,
|