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.
@@ -0,0 +1,466 @@
1
+ # SPDX-FileCopyrightText: technologydata contributors
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """TechnologyCollection class for representing an iterable of Technology Objects."""
6
+
7
+ import csv
8
+ import json
9
+ import logging
10
+ import pathlib
11
+ import re
12
+ from collections.abc import Iterator
13
+ from typing import Annotated, Self
14
+
15
+ import pandas
16
+ import pydantic
17
+ import pydantic_core
18
+
19
+ from technologydata.parameter import Parameter
20
+ from technologydata.technologies.growth_models import GrowthModel, LinearGrowth
21
+ from technologydata.technology import Technology
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class TechnologyCollection(pydantic.BaseModel):
27
+ """
28
+ Represent a collection of technologies.
29
+
30
+ Attributes
31
+ ----------
32
+ technologies : List[Technology]
33
+ List of Technology objects.
34
+
35
+ """
36
+
37
+ technologies: Annotated[
38
+ list[Technology], pydantic.Field(description="List of Technology objects.")
39
+ ]
40
+
41
+ def __iter__(self) -> Iterator[Technology]: # type: ignore
42
+ """
43
+ Return an iterator over the list of Technology objects.
44
+
45
+ Returns
46
+ -------
47
+ Iterator[Technology]
48
+ An iterator over the Technology objects contained in the collection.
49
+
50
+ """
51
+ return iter(self.technologies)
52
+
53
+ def __len__(self) -> int:
54
+ """
55
+ Return the number of technologies in this collection.
56
+
57
+ Returns
58
+ -------
59
+ int
60
+ The number of Technology objects in the technologies list.
61
+
62
+ """
63
+ return len(self.technologies)
64
+
65
+ def get(
66
+ self, name: str, region: str, year: int, case: str, detailed_technology: str
67
+ ) -> Self:
68
+ """
69
+ Filter technologies based on regex patterns for non-optional attributes.
70
+
71
+ Parameters
72
+ ----------
73
+ name : str
74
+ Regex pattern to filter technology names.
75
+ region : str
76
+ Regex pattern to filter region identifiers.
77
+ year : int
78
+ Regex pattern to filter the year of the data.
79
+ case : str
80
+ Regex pattern to filter case or scenario identifiers.
81
+ detailed_technology : str
82
+ Regex pattern to filter detailed technology names.
83
+
84
+ Returns
85
+ -------
86
+ TechnologyCollection
87
+ A new TechnologyCollection with filtered technologies.
88
+
89
+ """
90
+ filtered_technologies = self.technologies
91
+
92
+ if name is not None:
93
+ pattern_name = re.compile(name, re.IGNORECASE)
94
+ filtered_technologies = [
95
+ t for t in filtered_technologies if pattern_name.search(t.name)
96
+ ]
97
+
98
+ if region is not None:
99
+ pattern_region = re.compile(region, re.IGNORECASE)
100
+ filtered_technologies = [
101
+ t for t in filtered_technologies if pattern_region.search(t.region)
102
+ ]
103
+
104
+ if year is not None:
105
+ pattern_year = re.compile(str(year), re.IGNORECASE)
106
+ filtered_technologies = [
107
+ t for t in filtered_technologies if pattern_year.search(str(t.year))
108
+ ]
109
+
110
+ if case is not None:
111
+ pattern_case = re.compile(case, re.IGNORECASE)
112
+ filtered_technologies = [
113
+ t for t in filtered_technologies if pattern_case.search(t.case)
114
+ ]
115
+
116
+ if detailed_technology is not None:
117
+ pattern_detailed_technology = re.compile(detailed_technology, re.IGNORECASE)
118
+ filtered_technologies = [
119
+ t
120
+ for t in filtered_technologies
121
+ if pattern_detailed_technology.search(t.detailed_technology)
122
+ ]
123
+
124
+ return TechnologyCollection(technologies=filtered_technologies) # type: ignore
125
+
126
+ def to_dataframe(self) -> pandas.DataFrame:
127
+ """
128
+ Convert the TechnologyCollection to a pandas DataFrame.
129
+
130
+ Returns
131
+ -------
132
+ pd.DataFrame
133
+ A DataFrame containing the technology data.
134
+
135
+ """
136
+ return pandas.DataFrame(
137
+ [technology.model_dump() for technology in self.technologies]
138
+ )
139
+
140
+ def to_csv(self, **kwargs: pathlib.Path | str | bool) -> None:
141
+ """
142
+ Export the TechnologyCollection to a CSV file.
143
+
144
+ Parameters
145
+ ----------
146
+ **kwargs : dict
147
+ Additional keyword arguments passed to pandas.DataFrame.to_csv().
148
+ Common options include:
149
+ - path_or_buf : str or pathlib.Path or file-like object, optional
150
+ File path or object, if None, the result is returned as a string.
151
+ Default is None.
152
+ - sep : str
153
+ String of length 1. Field delimiter for the output file.
154
+ Default is ','.
155
+ - index : bool
156
+ Write row names (index). Default is True.
157
+ - encoding : str
158
+ String representing the encoding to use in the output file.
159
+ Default is 'utf-8'.
160
+
161
+ Notes
162
+ -----
163
+ The method converts the collection to a pandas DataFrame using
164
+ `self.to_dataframe()` and then writes it to a CSV file using the provided
165
+ kwargs.
166
+
167
+ """
168
+ default_kwargs = {
169
+ "sep": ",",
170
+ "index": False,
171
+ "encoding": "utf-8",
172
+ "quoting": csv.QUOTE_ALL,
173
+ }
174
+
175
+ # Merge default_kwargs with user-provided kwargs, giving precedence to user kwargs
176
+ merged_kwargs = {**default_kwargs, **kwargs}
177
+ output_dataframe = self.to_dataframe()
178
+ output_dataframe.to_csv(**merged_kwargs)
179
+
180
+ def to_json(
181
+ self,
182
+ file_path: pathlib.Path,
183
+ schema_path: pathlib.Path | None = None,
184
+ output_schema: bool = False,
185
+ ) -> None:
186
+ """
187
+ Export the TechnologyCollection to a JSON file, together with a data schema.
188
+
189
+ Parameters
190
+ ----------
191
+ file_path : pathlib.Path
192
+ The path to the JSON file to be created.
193
+ schema_path : pathlib.Path
194
+ The path to the JSON schema file to be created. By default, created with a `schema` suffix next to `file_path`.
195
+ output_schema : bool, default False
196
+ If True, generates a JSON schema file describing the data structure.
197
+ The schema will include field descriptions and type information.
198
+
199
+ """
200
+ if output_schema:
201
+ if schema_path is None:
202
+ schema_path = file_path.with_suffix(".schema.json")
203
+
204
+ # Export the model's schema with descriptions to a dict
205
+ schema = self.model_json_schema()
206
+
207
+ # Save the schema (which includes descriptions) to a JSON file
208
+ with open(schema_path, "w") as f:
209
+ json.dump(schema, f, indent=4)
210
+
211
+ with open(file_path, mode="w", encoding="utf-8") as jsonfile:
212
+ json_data = self.model_dump_json(indent=4) # Convert to JSON string
213
+ jsonfile.write(json_data)
214
+
215
+ @classmethod
216
+ def from_json(cls, file_path: pathlib.Path | str) -> Self:
217
+ """
218
+ Load a TechnologyCollection instance from a JSON file.
219
+
220
+ Parameters
221
+ ----------
222
+ file_path : pathlib.Path or str
223
+ Path to the JSON file containing the data. Can be a pathlib.Path object or a string path.
224
+
225
+ Returns
226
+ -------
227
+ TechnologyCollection
228
+ An instance of TechnologyCollection initialized with the data from the JSON file.
229
+
230
+ Raises
231
+ ------
232
+ TypeError
233
+ If `file_path` is not a pathlib.Path or str.
234
+
235
+ """
236
+ if isinstance(file_path, (pathlib.Path | str)):
237
+ file_path = pathlib.Path(file_path)
238
+ else:
239
+ raise TypeError("file_path must be a pathlib.Path or str")
240
+
241
+ with open(file_path, encoding="utf-8") as jsonfile:
242
+ json_data = jsonfile.read()
243
+
244
+ # pydantic_core.from_json return Any. Therefore, typing.cast makes sure that
245
+ # the output is indeed a TechnologyCollection
246
+ return cls.model_validate(
247
+ pydantic_core.from_json(json_data, allow_partial=True)
248
+ )
249
+
250
+ def to_currency(
251
+ self,
252
+ target_currency: str,
253
+ overwrite_country: None | str = None,
254
+ source: str = "worldbank",
255
+ ) -> Self:
256
+ """
257
+ Adjust the currency of all parameters of all contained Technology objects to the target currency.
258
+
259
+ The conversion includes inflation and exchange rates based on each Technology objects's region.
260
+ If a different country should be used for inflation adjustment, use `overwrite_country`.
261
+
262
+ Parameters
263
+ ----------
264
+ target_currency : str
265
+ The target currency (e.g., 'EUR_2020').
266
+ overwrite_country : str, optional
267
+ ISO 3166 alpha-3 country code to use for inflation adjustment instead of the object's region.
268
+ source: str, optional
269
+ The source of the inflation data, either "worldbank"/"wb" or "international_monetary_fund"/"imf".
270
+ Defaults to "worldbank".
271
+ Depending on the source, different years to adjust for inflation may be available.
272
+
273
+ Returns
274
+ -------
275
+ TechnologyCollection
276
+ A new TechnologyCollection object with all its parameters adjusted to the target currency.
277
+
278
+ """
279
+ new_techs = []
280
+
281
+ for i, tech in enumerate(self.technologies):
282
+ new_techs.append(
283
+ tech.to_currency(
284
+ target_currency=target_currency,
285
+ overwrite_country=overwrite_country,
286
+ source=source,
287
+ )
288
+ )
289
+
290
+ return TechnologyCollection(technologies=new_techs) # type: ignore
291
+
292
+ def fit(
293
+ self, parameter: str, model: GrowthModel, p0: dict[str, float] | None = None
294
+ ) -> GrowthModel:
295
+ """
296
+ Fit a growth model to a specified parameter across all technologies in the collection.
297
+
298
+ This method aggregates data points for the specified parameter from all technologies
299
+ in the collection, adds them to the provided growth model, and fits the model using
300
+ the initial parameter guesses provided in `p0`.
301
+
302
+ Parameters
303
+ ----------
304
+ parameter : str
305
+ The name of the parameter to fit the model to (e.g., "installed capacity").
306
+ model : GrowthModel
307
+ An instance of a growth model (e.g., LinearGrowth, ExponentialGrowth) to be fitted.
308
+ May already be partially initialized with some parameters and/or data points.
309
+ p0 : dict[str, float], optional
310
+ Initial guess for the model parameters.
311
+
312
+ Returns
313
+ -------
314
+ GrowthModel
315
+ The fitted growth model with optimized parameters.
316
+
317
+ Raises
318
+ ------
319
+ ValueError
320
+ If the collection contains incompatible parameters with different units, heating values, or carriers.
321
+
322
+ """
323
+ first_param = None
324
+ # Aggregate data points for the specified parameter from all technologies
325
+ for tech in self.technologies:
326
+ param = tech.parameters[parameter]
327
+ if first_param is None:
328
+ first_param = param
329
+
330
+ try:
331
+ first_param._check_parameter_compatibility(param)
332
+ except ValueError as e:
333
+ raise ValueError(
334
+ f"The collection contains one or more parameters with incompatible units/heating values/carriers:\n"
335
+ f"* {first_param}, and\n"
336
+ f"* {param}."
337
+ ) from e
338
+
339
+ model.add_data((tech.year, param.magnitude))
340
+
341
+ # Fit the model using the provided initial parameter guesses
342
+ model.fit(p0=p0)
343
+
344
+ return model
345
+
346
+ def project(
347
+ self,
348
+ to_years: list[int],
349
+ parameters: dict[str, GrowthModel | str],
350
+ ) -> Self:
351
+ """
352
+ Project specified parameters for all technologies in the collection to future years.
353
+
354
+ This method uses the provided growth models to project the specified parameters
355
+ for each technology in the collection to the given future years.
356
+
357
+ To keep other parameters that should not be projected, add them to the dictionary as well
358
+ without a growth model. Instead, there are other options available:
359
+ 'mean', 'closest' and 'NaN'.
360
+ 'mean' will set the parameter to the mean of all existing values in the collection,
361
+ while 'NaN' will add the parameter with NaN values as a placeholder.
362
+ 'closest' will set the parameter to the value of the closest year in the original data,
363
+ with a preference for past years if equidistant. (Not yet implemented.)
364
+
365
+ The method creates new Technology objects for each combination of original technology
366
+ and future year, applying the appropriate growth model projections.
367
+
368
+ Parameters
369
+ ----------
370
+ to_years : list[int]
371
+ List of future years to which the parameters should be projected.
372
+ parameters : dict[str, GrowthModel | str]
373
+ A dictionary mapping parameter names to their respective growth models for projection.
374
+ If provided, `parameter` and `model` cannot be used.
375
+ To keep other parameters without projecting, available options are 'mean', 'closest' and 'NaN'.
376
+
377
+ Returns
378
+ -------
379
+ TechnologyCollection
380
+ A new TechnologyCollection with technologies projected to the specified future years.
381
+
382
+ Raises
383
+ ------
384
+ ValueError
385
+ If neither `parameter` and `model`, or `parameters` are not or all provided.
386
+
387
+ Examples
388
+ --------
389
+ >>> tc.project(
390
+ ... to_years=[2030, 2040],
391
+ ... parameters={
392
+ ... "installed capacity": LinearGrowth(m=0.5, A=10),
393
+ ... "lifetime": "mean",
394
+ ... "efficiency": "NaN"
395
+ ... }
396
+ ... )
397
+
398
+ """
399
+ logger.debug(f"Projecting parameters as follows: {parameters}")
400
+
401
+ projected_technologies = []
402
+ for to_year in to_years:
403
+ # Create a new Technology object for the projected year
404
+ new_tech = Technology(
405
+ name=self.technologies[0].name,
406
+ region=self.technologies[0].region,
407
+ year=to_year,
408
+ case=self.technologies[0].case,
409
+ detailed_technology=self.technologies[0].detailed_technology,
410
+ parameters={},
411
+ )
412
+
413
+ for param, model in parameters.items():
414
+ new_param: Parameter
415
+
416
+ # Trick: A linear growth with m=0 returns the mean of the provided data points
417
+ # this way we can reuse the logic already implemented for fitting and projecting below
418
+ if model == "mean":
419
+ model = LinearGrowth(m=0)
420
+
421
+ if isinstance(model, GrowthModel):
422
+ # Fit the model to the parameter data
423
+ model = self.fit(param, model.model_copy())
424
+
425
+ # Project the model to the specified future years
426
+ param_value = model.project(to_year)
427
+
428
+ logger.debug(
429
+ f"Resulting model for {param} in year {to_year}: {model}"
430
+ )
431
+ # Add the projected parameter to the new technology
432
+ new_param = (
433
+ self.technologies[0]
434
+ .parameters[param]
435
+ .model_copy(
436
+ deep=True,
437
+ update={
438
+ "magnitude": param_value,
439
+ "provenance": f"Projected to {to_year} using {model}.",
440
+ "note": None, # Clear any existing note
441
+ "sources": None, # Clear any existing sources
442
+ },
443
+ )
444
+ )
445
+
446
+ elif model == "NaN":
447
+ new_param = Parameter(
448
+ magnitude=float("nan"),
449
+ note="Placeholder parameters with NaN value.",
450
+ )
451
+ elif model == "closest":
452
+ raise NotImplementedError(
453
+ "'closest' option for '{param}' not yet implemented."
454
+ ) # TODO
455
+ else:
456
+ raise ValueError(
457
+ f"Unexpected model type for parameter '{param}': {model}"
458
+ )
459
+
460
+ new_tech.parameters[param] = new_param
461
+
462
+ logger.debug(f"Projected technology for year {to_year}: {new_tech}")
463
+
464
+ projected_technologies.append(new_tech)
465
+
466
+ return TechnologyCollection(technologies=projected_technologies) # type: ignore
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: technologydata contributors
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """Provide classes and utilities for handling techno-economic data for energy system modeling."""
6
+
7
+ from technologydata.utils.commons import Commons, DateFormatEnum, FileExtensionEnum
8
+
9
+ __all__ = [
10
+ "Commons",
11
+ "DateFormatEnum",
12
+ "FileExtensionEnum",
13
+ ]
@@ -0,0 +1,33 @@
1
+ # SPDX-FileCopyrightText: technologydata contributors
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ # Definitions for the default carriers supported by the package.
6
+ # Each carrier is defined as an independent dimension.
7
+ # This way, pint allows for operations like division or multiplication,
8
+ # and at the same time prevents incompatible operations on these dimensions.
9
+ #
10
+ # See also: https://pint.readthedocs.io/en/latest/defining.html
11
+
12
+
13
+ # Format:
14
+ # <carrier_name> = [<carrier_name>] [= <alias>]
15
+
16
+ ammonia = [ammonia] = NH3
17
+ carbon = [carbon] = C
18
+ carbon_dioxide = [carbon_dioxide] = CO2
19
+ carbon_monoxide = [carbon_monoxide] = CO
20
+ coal = [coal] = anthracite = hard_coal = black_coal
21
+ diesel = [diesel]
22
+ gasoline = [gasoline] = petrol
23
+ jet_fuel_a1 = [jet_fuel_a1] = JETA1
24
+ electricity = [electricity] = e = el
25
+ hydrogen = [hydrogen] = H2
26
+ lignite = [lignite]
27
+ methane = [methane] = CH4
28
+ methanol = [methanol] = CH3OH = MeOH
29
+ natural_gas = [natural_gas] = NG
30
+ nitrogen = [nitrogen] = N2
31
+ oxygen = [oxygen] = O2
32
+ water = [water] = H2O
33
+ wood = [wood]