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,503 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Growth models for projecting technology parameters over time."""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import typing
|
|
10
|
+
from abc import abstractmethod
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Annotated, Self
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
from scipy.optimize import curve_fit
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GrowthModel(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Abstract base for growth models used in projections.
|
|
24
|
+
|
|
25
|
+
To implement a new growth model that inherits from this class, the following must be provided:
|
|
26
|
+
1. A mathematical function representing the growth model by implementing the abstract method `function(self, x: float, **parameters) -> float`.
|
|
27
|
+
The parameters of the function (besides `x`) will be automatically detected and used for fitting and projection.
|
|
28
|
+
2. The parameters should be defined as attributes of the class, initialized to `None` if they are to be fitted.
|
|
29
|
+
|
|
30
|
+
pydantic configuration:
|
|
31
|
+
- `validate_assignment = True`: Ensures that any assignment to model attributes is validated,
|
|
32
|
+
as growth models may be created first with missing parameters (set to None) and then fitted later
|
|
33
|
+
and data points may be added after creation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
37
|
+
|
|
38
|
+
data_points: Annotated[
|
|
39
|
+
list[tuple[float, float]],
|
|
40
|
+
Field(description="Data points (x, y) for fitting the model, where f(x) = y."),
|
|
41
|
+
] = list()
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def function(self, *args: typing.Any, **kwargs: typing.Any) -> float | np.ndarray:
|
|
45
|
+
"""Represent the growth model function."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def model_parameters(self) -> list[str]:
|
|
50
|
+
"""Return the set of model parameters that have been provided (are not None)."""
|
|
51
|
+
return [f for f in type(self).model_fields.keys() if f != "data_points"]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def provided_parameters(self) -> list[str]:
|
|
55
|
+
"""Return the set of model parameters that have been provided (are not None)."""
|
|
56
|
+
return list(
|
|
57
|
+
self.model_dump(
|
|
58
|
+
include=set(self.model_parameters), exclude_none=True
|
|
59
|
+
).keys()
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def missing_parameters(self) -> list[str]:
|
|
64
|
+
"""Return the set of model parameters that are missing (have not been provided)."""
|
|
65
|
+
return [p for p in self.model_parameters if p not in self.provided_parameters]
|
|
66
|
+
|
|
67
|
+
def add_data(self, data_point: tuple[float, float]) -> Self:
|
|
68
|
+
"""Add a data point to the model for fitting."""
|
|
69
|
+
self.data_points.append(data_point)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def project(
|
|
73
|
+
self,
|
|
74
|
+
to_year: int,
|
|
75
|
+
) -> float:
|
|
76
|
+
"""
|
|
77
|
+
Project using the model to the specified year.
|
|
78
|
+
|
|
79
|
+
This function uses the parameters that have been provided for the model either directly or through fitting to data points.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
to_year : int
|
|
84
|
+
The year to which to project the values.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
float
|
|
89
|
+
The projected value for the specified year.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
if len(self.missing_parameters) > 0:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Cannot project. The following parameters have not been specified yet and are missing: {self.missing_parameters}."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return self.function(
|
|
98
|
+
to_year, **self.model_dump(include=set(self.provided_parameters))
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def _kwpartial(
|
|
103
|
+
cls, f: Callable[..., float], **fixed_params: dict[str, float]
|
|
104
|
+
) -> Callable[..., float]:
|
|
105
|
+
"""
|
|
106
|
+
Like functools.partial, but for keyword arguments.
|
|
107
|
+
|
|
108
|
+
Enables us to wrap a function in a way that is compatible with scipy.optimize.curve_fit,
|
|
109
|
+
which does not play nicely with standard functools.partial.
|
|
110
|
+
For details see: https://stackoverflow.com/questions/79749129/use-curve-fit-with-partial-using-named-parameters-instead-of-positional-para/79749198#79749198
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
f_sig = inspect.signature(f)
|
|
114
|
+
positional_params = (
|
|
115
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
116
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
117
|
+
)
|
|
118
|
+
args = [
|
|
119
|
+
p.name for p in f_sig.parameters.values() if p.kind in positional_params
|
|
120
|
+
]
|
|
121
|
+
new_args = [
|
|
122
|
+
inspect.Parameter(arg, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
123
|
+
for arg in args
|
|
124
|
+
if arg not in fixed_params
|
|
125
|
+
]
|
|
126
|
+
new_sig = inspect.Signature(new_args)
|
|
127
|
+
|
|
128
|
+
def wrapper(*f_args: float, **f_kwargs: dict[str, float]) -> float:
|
|
129
|
+
bound_args = new_sig.bind(*f_args, **f_kwargs)
|
|
130
|
+
bound_args.apply_defaults()
|
|
131
|
+
return f(**bound_args.arguments, **fixed_params)
|
|
132
|
+
|
|
133
|
+
wrapper.__signature__ = new_sig # type: ignore[attr-defined]
|
|
134
|
+
wrapper.__name__ = f"kwpartial({f.__name__}, {fixed_params})"
|
|
135
|
+
|
|
136
|
+
return wrapper
|
|
137
|
+
|
|
138
|
+
def fit(self, p0: dict[str, float] | None = None) -> Self:
|
|
139
|
+
"""
|
|
140
|
+
Fit the growth model using the parameters and data points provided to the model.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
p0 : dict[str, float], optional
|
|
145
|
+
Initial guesses for the missing parameters to be fitted.
|
|
146
|
+
May contain all or a subset of the missing parameters.
|
|
147
|
+
Any parameter not provided will be initialized with a starting guess of 1.0 (scipy's default).
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
Self
|
|
152
|
+
The model instance with the fitted parameters set.
|
|
153
|
+
|
|
154
|
+
Raises
|
|
155
|
+
------
|
|
156
|
+
ValueError
|
|
157
|
+
If there are not enough data points to fit the model.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
# if all parameters of the model are already fixed, then we cannot fit anything
|
|
161
|
+
if len(self.provided_parameters) == len(self.model_parameters):
|
|
162
|
+
logger.info("All parameters are already fixed, cannot fit anything.")
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
# The number of data points must be at least equal to the number of parameters to fit
|
|
166
|
+
if len(self.data_points) < len(self.missing_parameters):
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"Not enough data points to fit the model. Need at least {len(self.missing_parameters)}, got {len(self.data_points)}."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Fit the model to the data points:
|
|
172
|
+
# build a partial function that includes the already fixed parameters
|
|
173
|
+
func = self._kwpartial(
|
|
174
|
+
self.function,
|
|
175
|
+
**self.model_dump(include=set(self.provided_parameters), exclude_none=True),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# p0 optionally allows to provide initial guesses for the parameters to fit
|
|
179
|
+
if p0 is None:
|
|
180
|
+
p0 = {}
|
|
181
|
+
|
|
182
|
+
# the dict needs to be transformed into a list with the parameters in the correct order
|
|
183
|
+
# if a parameter is missing from p0, we use 1 as a default initial guess (scipy's default)
|
|
184
|
+
p0_ = [p0.get(param, 1) for param in self.missing_parameters]
|
|
185
|
+
|
|
186
|
+
# fit the function to the data points
|
|
187
|
+
xdata, ydata = zip(*self.data_points)
|
|
188
|
+
popt, pcov = curve_fit(f=func, xdata=xdata, ydata=ydata, p0=p0_)
|
|
189
|
+
|
|
190
|
+
logger.debug(f"Fitted parameters: {popt}")
|
|
191
|
+
logger.debug(f"Covariance of the parameters: {pcov}")
|
|
192
|
+
|
|
193
|
+
# assign the fitted parameters to the model
|
|
194
|
+
for param, value in zip(self.missing_parameters, popt):
|
|
195
|
+
logger.debug(f"Setting parameter {param} to fitted value {value}")
|
|
196
|
+
setattr(self, param, value)
|
|
197
|
+
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class LinearGrowth(GrowthModel):
|
|
202
|
+
"""Project with linear growth model."""
|
|
203
|
+
|
|
204
|
+
x0: Annotated[
|
|
205
|
+
float | None,
|
|
206
|
+
Field(
|
|
207
|
+
description="The reference x-value (e.g., starting year) for the linear function.",
|
|
208
|
+
),
|
|
209
|
+
] = None
|
|
210
|
+
m: Annotated[
|
|
211
|
+
float | None,
|
|
212
|
+
Field(description="Annual growth rate for the linear function."),
|
|
213
|
+
] = None
|
|
214
|
+
A: Annotated[
|
|
215
|
+
float | None,
|
|
216
|
+
Field(description="Starting value for the linear function."),
|
|
217
|
+
] = None
|
|
218
|
+
|
|
219
|
+
def function(
|
|
220
|
+
self, x: float | np.ndarray, x0: float, m: float, A: float
|
|
221
|
+
) -> float | np.ndarray:
|
|
222
|
+
"""
|
|
223
|
+
Linear function for the growth model.
|
|
224
|
+
|
|
225
|
+
f(x) = m * (x - x0) + a
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
x : float | numpy Array
|
|
230
|
+
The input value(s) on which to evaluate the function, e.g. a year '2025'.
|
|
231
|
+
x0 : float
|
|
232
|
+
The reference x-value (e.g., starting year) for the linear function.
|
|
233
|
+
m : float, optional
|
|
234
|
+
The slope of the linear function.
|
|
235
|
+
A : float, optional
|
|
236
|
+
The constant offset of the linear function.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
float | numpy Array
|
|
241
|
+
The result(s) of the linear function evaluation at x.
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
return m * (x - x0) + A
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ExponentialGrowth(GrowthModel):
|
|
248
|
+
"""Project with exponential growth model."""
|
|
249
|
+
|
|
250
|
+
x0: Annotated[
|
|
251
|
+
float | None,
|
|
252
|
+
Field(
|
|
253
|
+
description="The reference x-value (e.g., starting year) for the exponential function.",
|
|
254
|
+
),
|
|
255
|
+
] = None
|
|
256
|
+
A: Annotated[
|
|
257
|
+
float | None,
|
|
258
|
+
Field(description="Initial value for the exponential function."),
|
|
259
|
+
] = None
|
|
260
|
+
m: Annotated[
|
|
261
|
+
float | None,
|
|
262
|
+
Field(description="The multiplier for the exponential function."),
|
|
263
|
+
] = None
|
|
264
|
+
k: Annotated[
|
|
265
|
+
float | None,
|
|
266
|
+
Field(description="Growth rate for the exponential function."),
|
|
267
|
+
] = None
|
|
268
|
+
|
|
269
|
+
def function(
|
|
270
|
+
self, x: float | np.ndarray, x0: float, A: float, m: float, k: float
|
|
271
|
+
) -> float | np.ndarray:
|
|
272
|
+
"""
|
|
273
|
+
Exponential function for the growth model.
|
|
274
|
+
|
|
275
|
+
f(x) = A + m * exp(k * (x - x0))
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
x : float | numpy Array
|
|
280
|
+
The input value(s) on which to evaluate the function, e.g. a year '2025'.
|
|
281
|
+
x0 : float
|
|
282
|
+
The reference x-value (e.g., starting year) for the exponential function.
|
|
283
|
+
A : float
|
|
284
|
+
The lower horizontal asymptote of the exponential function.
|
|
285
|
+
m : float
|
|
286
|
+
The initial value of the exponential function.
|
|
287
|
+
k : float
|
|
288
|
+
The growth rate of the exponential function.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
float | numpy Array
|
|
293
|
+
The result(s) of the exponential function evaluation at x.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
return A + m * np.exp(k * (x - x0))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class GeneralLogisticGrowth(GrowthModel):
|
|
300
|
+
"""Project with a generalized logistic growth model."""
|
|
301
|
+
|
|
302
|
+
x0: Annotated[
|
|
303
|
+
float | None,
|
|
304
|
+
Field(
|
|
305
|
+
description="The x-value of the sigmoid's midpoint (inflection point/midpoint year).",
|
|
306
|
+
),
|
|
307
|
+
] = None
|
|
308
|
+
A: Annotated[
|
|
309
|
+
float | None,
|
|
310
|
+
Field(
|
|
311
|
+
description="The lower horizontal asymptote of the logistic function.",
|
|
312
|
+
),
|
|
313
|
+
] = None
|
|
314
|
+
K: Annotated[
|
|
315
|
+
float | None,
|
|
316
|
+
Field(
|
|
317
|
+
description="The upper horizontal asymptote of the logistic function for C=1.",
|
|
318
|
+
),
|
|
319
|
+
] = None
|
|
320
|
+
B: Annotated[
|
|
321
|
+
float | None,
|
|
322
|
+
Field(
|
|
323
|
+
description="The growth rate of the logistic function.",
|
|
324
|
+
),
|
|
325
|
+
] = None
|
|
326
|
+
Q: Annotated[
|
|
327
|
+
float | None,
|
|
328
|
+
Field(
|
|
329
|
+
description="Parameter related to the value of f(0).",
|
|
330
|
+
),
|
|
331
|
+
] = None
|
|
332
|
+
C: Annotated[
|
|
333
|
+
float | None,
|
|
334
|
+
Field(
|
|
335
|
+
description="Parameter related to the upper horizontal asymptote, often set to 1.",
|
|
336
|
+
),
|
|
337
|
+
] = None
|
|
338
|
+
nu: Annotated[
|
|
339
|
+
float | None,
|
|
340
|
+
Field(
|
|
341
|
+
description="Parameter affecting near which asymptote the maximum growth occurs.",
|
|
342
|
+
),
|
|
343
|
+
] = None
|
|
344
|
+
|
|
345
|
+
def function(
|
|
346
|
+
self,
|
|
347
|
+
x: float | np.ndarray,
|
|
348
|
+
x0: float,
|
|
349
|
+
A: float,
|
|
350
|
+
K: float,
|
|
351
|
+
B: float,
|
|
352
|
+
Q: float,
|
|
353
|
+
C: float,
|
|
354
|
+
nu: float,
|
|
355
|
+
) -> float | np.ndarray:
|
|
356
|
+
"""
|
|
357
|
+
Generalized logistic function for the growth model.
|
|
358
|
+
|
|
359
|
+
f(x) = A + (K - A) / (C + Q * exp(-B * (x - M)))^(1/nu)
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
x : float | numpy Array
|
|
364
|
+
The input value(s) on which to evaluate the function, e.g. a year '2025'.
|
|
365
|
+
x0 : float
|
|
366
|
+
The x-value of the sigmoid's midpoint (inflection point).
|
|
367
|
+
A : float
|
|
368
|
+
The lower horizontal asymptote of the logistic function.
|
|
369
|
+
K : float
|
|
370
|
+
The upper horizontal asymptote of the logistic function for C=1.
|
|
371
|
+
If A=0 and C=1, then K is the carrying capacity.
|
|
372
|
+
B : float
|
|
373
|
+
The growth rate of the logistic function.
|
|
374
|
+
Q : float
|
|
375
|
+
Related to the value of f(0).
|
|
376
|
+
C : float
|
|
377
|
+
Parameter related to the upper horizontal asymptote, often set to 1.
|
|
378
|
+
nu : float
|
|
379
|
+
Parameter affecting near which asymptote the maximum growth occurs.
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
float | numpy Array
|
|
384
|
+
The result(s) of the generalized logistic function evaluation at x.
|
|
385
|
+
|
|
386
|
+
"""
|
|
387
|
+
return A + (K - A) / (C + Q * np.exp(-B * (x - x0))) ** (1.0 / nu)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class LogisticGrowth(GrowthModel):
|
|
391
|
+
"""Project with a logistic growth model."""
|
|
392
|
+
|
|
393
|
+
x0: Annotated[
|
|
394
|
+
float | None,
|
|
395
|
+
Field(
|
|
396
|
+
description="The x-value of the sigmoid's midpoint (inflection point/midpoint year).",
|
|
397
|
+
),
|
|
398
|
+
] = None
|
|
399
|
+
A: Annotated[
|
|
400
|
+
float | None,
|
|
401
|
+
Field(
|
|
402
|
+
description="The lower horizontal asymptote of the logistic function.",
|
|
403
|
+
),
|
|
404
|
+
] = None
|
|
405
|
+
L: Annotated[
|
|
406
|
+
float | None,
|
|
407
|
+
Field(
|
|
408
|
+
description="Carrying capacity of the logistic function.",
|
|
409
|
+
),
|
|
410
|
+
] = None
|
|
411
|
+
k: Annotated[
|
|
412
|
+
float | None,
|
|
413
|
+
Field(
|
|
414
|
+
description="Growth rate of the logistic function.",
|
|
415
|
+
),
|
|
416
|
+
] = None
|
|
417
|
+
|
|
418
|
+
def function(
|
|
419
|
+
self, x: float | np.ndarray, x0: float, A: float, L: float, k: float
|
|
420
|
+
) -> float | np.ndarray:
|
|
421
|
+
"""
|
|
422
|
+
Logistic function for the growth model.
|
|
423
|
+
|
|
424
|
+
f(x) = A + L / (1 + exp(-k * (x - x0)))
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
x : float | numpy Array
|
|
429
|
+
The input value(s) on which to evaluate the function, e.g. a year '2025'.
|
|
430
|
+
x0 : float
|
|
431
|
+
The x-value of the sigmoid's midpoint (inflection point).
|
|
432
|
+
A : float
|
|
433
|
+
The lower horizontal asymptote of the logistic function.
|
|
434
|
+
L : float
|
|
435
|
+
The carrying capacity of the logistic function.
|
|
436
|
+
k : float
|
|
437
|
+
The growth rate of the logistic function.
|
|
438
|
+
|
|
439
|
+
Returns
|
|
440
|
+
-------
|
|
441
|
+
float | numpy Array
|
|
442
|
+
The result(s) of the logistic function evaluation at x.
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
return A + L / (1 + np.exp(-k * (x - x0)))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class GompertzGrowth(GrowthModel):
|
|
449
|
+
"""Project with a Gompertz growth model."""
|
|
450
|
+
|
|
451
|
+
A: Annotated[
|
|
452
|
+
float | None,
|
|
453
|
+
Field(
|
|
454
|
+
description="The upper asymptote (maximum value) of the Gompertz function.",
|
|
455
|
+
),
|
|
456
|
+
] = None
|
|
457
|
+
k: Annotated[
|
|
458
|
+
float | None,
|
|
459
|
+
Field(
|
|
460
|
+
description="The growth rate of the Gompertz function.",
|
|
461
|
+
),
|
|
462
|
+
] = None
|
|
463
|
+
x0: Annotated[
|
|
464
|
+
float | None,
|
|
465
|
+
Field(
|
|
466
|
+
description="The x-value of the inflection point (midpoint year) of the Gompertz function.",
|
|
467
|
+
),
|
|
468
|
+
] = None
|
|
469
|
+
b: Annotated[
|
|
470
|
+
float | None,
|
|
471
|
+
Field(
|
|
472
|
+
description="The displacement along the x-axis of the Gompertz function.",
|
|
473
|
+
),
|
|
474
|
+
] = None
|
|
475
|
+
|
|
476
|
+
def function(
|
|
477
|
+
self, x: float | np.ndarray, A: float, k: float, x0: float, b: float
|
|
478
|
+
) -> float | np.ndarray:
|
|
479
|
+
"""
|
|
480
|
+
Gompertz function for the growth model.
|
|
481
|
+
|
|
482
|
+
f(x) = A * exp(-b * exp(-k * (x - x0)))
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
x : float | numpy Array
|
|
487
|
+
The input value(s) on which to evaluate the function, e.g. a year '2025'.
|
|
488
|
+
A : float
|
|
489
|
+
The upper asymptote (maximum value) of the Gompertz function.
|
|
490
|
+
k : float
|
|
491
|
+
The growth rate of the Gompertz function.
|
|
492
|
+
x0 : float
|
|
493
|
+
The x-value of the inflection point (midpoint year) of the Gompertz function.
|
|
494
|
+
b : float
|
|
495
|
+
The displacement along the x-axis of the Gompertz function.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
float | numpy Array
|
|
500
|
+
The result(s) of the Gompertz function evaluation at x.
|
|
501
|
+
|
|
502
|
+
"""
|
|
503
|
+
return A * np.exp(-b * np.exp(-k * (x - x0)))
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Technology class for representing a technology with parameters and transformation methods."""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated, Any, Self
|
|
8
|
+
|
|
9
|
+
import pydantic
|
|
10
|
+
|
|
11
|
+
from technologydata.parameter import Parameter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Technology(pydantic.BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Represent a technology with region, year, and a flexible set of parameters.
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
name : str
|
|
21
|
+
Name of the technology.
|
|
22
|
+
detailed_technology : str
|
|
23
|
+
More detailed technology name.
|
|
24
|
+
case : str
|
|
25
|
+
Case or scenario identifier.
|
|
26
|
+
region : str
|
|
27
|
+
Region identifier.
|
|
28
|
+
year : int
|
|
29
|
+
Year of the data.
|
|
30
|
+
parameters : Dict[str, Parameter]
|
|
31
|
+
Dictionary of parameter names to Parameter objects.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name: Annotated[str, pydantic.Field(description="Name of the technology.")]
|
|
36
|
+
detailed_technology: Annotated[
|
|
37
|
+
str, pydantic.Field(description="Detailed technology name.")
|
|
38
|
+
]
|
|
39
|
+
case: Annotated[str, pydantic.Field(description="Case or scenario identifier.")]
|
|
40
|
+
region: Annotated[str, pydantic.Field(description="Region identifier.")]
|
|
41
|
+
year: Annotated[int, pydantic.Field(description="Year of the data.")]
|
|
42
|
+
parameters: Annotated[
|
|
43
|
+
dict[str, Parameter],
|
|
44
|
+
pydantic.Field(default_factory=dict, description="Parameters."),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def __getitem__(self, key: str) -> Parameter:
|
|
48
|
+
"""
|
|
49
|
+
Access a parameter by name.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
key : str
|
|
54
|
+
Parameter name.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
Parameter
|
|
59
|
+
The requested parameter.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
return self.parameters[key]
|
|
63
|
+
|
|
64
|
+
def __setitem__(self, key: str, value: Parameter) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Set a parameter by name.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
key : str
|
|
71
|
+
Parameter name.
|
|
72
|
+
value : Parameter
|
|
73
|
+
The parameter to set.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
self.parameters[key] = value
|
|
77
|
+
|
|
78
|
+
def check_consistency(self) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Check for consistency and completeness of parameters.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
bool
|
|
85
|
+
True if consistent, False otherwise.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
# Example: check required parameters
|
|
89
|
+
required = ["specific_investment", "investment", "lifetime"]
|
|
90
|
+
missing = [p for p in required if p not in self.parameters]
|
|
91
|
+
return len(missing) == 0
|
|
92
|
+
|
|
93
|
+
def calculate_parameters(self, parameters: Any | None = None) -> Self:
|
|
94
|
+
"""
|
|
95
|
+
Calculate missing or derived parameters.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
parameters : Optional[Any]
|
|
100
|
+
List of parameter names to calculate, or "<missing>" for all missing.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
Technology
|
|
105
|
+
A new Technology object with calculated parameters.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
# Placeholder: implement calculation logic as needed
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def to_currency(
|
|
112
|
+
self,
|
|
113
|
+
target_currency: str,
|
|
114
|
+
overwrite_country: None | str = None,
|
|
115
|
+
source: str = "worldbank",
|
|
116
|
+
) -> Self:
|
|
117
|
+
"""
|
|
118
|
+
Adjust the currency of all parameters of the technology to the target currency.
|
|
119
|
+
|
|
120
|
+
The conversion includes inflation and exchange rates based on the object's region.
|
|
121
|
+
If a different country should be used for inflation adjustment, use `overwrite_country`.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
target_currency : str
|
|
126
|
+
The target currency (e.g., 'EUR_2020').
|
|
127
|
+
overwrite_country : str, optional
|
|
128
|
+
ISO 3166 alpha-3 country code to use for inflation adjustment instead of the object's region.
|
|
129
|
+
source: str, optional
|
|
130
|
+
The source of the inflation data, either "worldbank"/"wb" or "international_monetary_fund"/"imf".
|
|
131
|
+
Defaults to "worldbank".
|
|
132
|
+
Depending on the source, different years to adjust for inflation may be available.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
Technology
|
|
137
|
+
A new Technology object with all its parameters adjusted to the target currency.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
country = self.region
|
|
141
|
+
if overwrite_country:
|
|
142
|
+
country = overwrite_country
|
|
143
|
+
|
|
144
|
+
# Copy the Technology object
|
|
145
|
+
new_tech: Self = self.model_copy(deep=True)
|
|
146
|
+
|
|
147
|
+
# Iterate over parameters and convert their currency
|
|
148
|
+
for name, param in new_tech.parameters.items():
|
|
149
|
+
new_tech.parameters[name] = param.to_currency(
|
|
150
|
+
target_currency=target_currency,
|
|
151
|
+
country=country,
|
|
152
|
+
source=source,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return new_tech
|
|
156
|
+
|
|
157
|
+
def adjust_region(self, target_region: str) -> Self:
|
|
158
|
+
"""
|
|
159
|
+
Adjust technology parameters to match a different region.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
target_region : str
|
|
164
|
+
The target region.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
Technology
|
|
169
|
+
A new Technology object with adjusted region.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
# Placeholder: implement region adjustment logic
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def adjust_scale(self, scaling_factor: float) -> Self:
|
|
176
|
+
"""
|
|
177
|
+
Scale parameter values by a scaling factor.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
scaling_factor : float
|
|
182
|
+
The scaling factor to apply.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
Technology
|
|
187
|
+
A new Technology object with scaled parameters.
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
# Placeholder: implement scaling logic
|
|
191
|
+
return self
|