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,662 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: technologydata contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Parameter class for encapsulating a value, its unit, provenance, notes, and sources."""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Annotated, Self
|
|
9
|
+
|
|
10
|
+
import pint
|
|
11
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
12
|
+
|
|
13
|
+
import technologydata
|
|
14
|
+
from technologydata.source_collection import SourceCollection
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Parameter(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Encapsulate a value with its unit, provenance, notes, sources, and more optional attributes required to describe technology parameters, like carrier, and heating value.
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
magnitude : int | float
|
|
26
|
+
The numerical value of the parameter.
|
|
27
|
+
units : Optional[str]
|
|
28
|
+
The unit of the parameter.
|
|
29
|
+
carrier : Optional[str]
|
|
30
|
+
The energy carrier.
|
|
31
|
+
heating_value : Optional[str]
|
|
32
|
+
The heating value type.
|
|
33
|
+
provenance : Optional[str]
|
|
34
|
+
Description of the data's provenance.
|
|
35
|
+
note : Optional[str]
|
|
36
|
+
Additional notes about the parameter.
|
|
37
|
+
sources : Optional[SourceCollection]
|
|
38
|
+
List of sources for the parameter.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
magnitude: Annotated[
|
|
43
|
+
int | float, Field(description="The numerical value of the parameter.")
|
|
44
|
+
]
|
|
45
|
+
units: Annotated[str | None, Field(description="The unit of the parameter.")] = None
|
|
46
|
+
carrier: Annotated[
|
|
47
|
+
str | None,
|
|
48
|
+
Field(description="Carriers of the units, e.g. 'H2', 'el', 'H2O'."),
|
|
49
|
+
] = None
|
|
50
|
+
heating_value: Annotated[
|
|
51
|
+
str | None,
|
|
52
|
+
Field(description="Heating value type for energy carriers ('LHV' or 'HHV')."),
|
|
53
|
+
] = None
|
|
54
|
+
provenance: Annotated[str | None, Field(description="The data's provenance.")] = (
|
|
55
|
+
None
|
|
56
|
+
)
|
|
57
|
+
note: Annotated[str | None, Field(description="Additional notes.")] = None
|
|
58
|
+
sources: Annotated[
|
|
59
|
+
SourceCollection,
|
|
60
|
+
Field(description="List of sources for this parameter."),
|
|
61
|
+
] = SourceCollection(sources=[])
|
|
62
|
+
|
|
63
|
+
# Private attributes for derived pint objects
|
|
64
|
+
_pint_quantity: pint.Quantity = PrivateAttr(None)
|
|
65
|
+
_pint_carrier: pint.Unit = PrivateAttr(None)
|
|
66
|
+
_pint_heating_value: pint.Unit = PrivateAttr(None)
|
|
67
|
+
|
|
68
|
+
def __init__(self, **data: float | str | SourceCollection | None) -> None:
|
|
69
|
+
"""Initialize Parameter and update pint attributes."""
|
|
70
|
+
# pint uses canonical names for units, carriers, and heating values
|
|
71
|
+
# Ensure the Parameter object is always created with these consistent names from pint
|
|
72
|
+
if "units" in data and data["units"] is not None:
|
|
73
|
+
technologydata.ureg.ensure_currency_is_unit(str(data["units"]))
|
|
74
|
+
data["units"] = str(technologydata.ureg.Unit(data["units"]))
|
|
75
|
+
if "carrier" in data and data["carrier"] is not None:
|
|
76
|
+
data["carrier"] = str(technologydata.creg.Unit(data["carrier"]))
|
|
77
|
+
if "heating_value" in data and data["heating_value"] is not None:
|
|
78
|
+
data["heating_value"] = str(
|
|
79
|
+
technologydata.hvreg.Unit(data["heating_value"])
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
super().__init__(**data)
|
|
83
|
+
self._update_pint_attributes()
|
|
84
|
+
|
|
85
|
+
def _update_pint_attributes(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Update internal pint attributes based on current object fields.
|
|
88
|
+
|
|
89
|
+
This method initializes or updates the following attributes:
|
|
90
|
+
- `_pint_quantity`: a pint Quantity created from `magnitude` and `units`.
|
|
91
|
+
- `_pint_carrier`: a pint Unit created from `carrier`.
|
|
92
|
+
- `_pint_heating_value`: a pint Unit created from `heating_value`, if applicable.
|
|
93
|
+
|
|
94
|
+
Notes
|
|
95
|
+
-----
|
|
96
|
+
- Ensures that `units` are valid, especially for currency units.
|
|
97
|
+
- Raises a ValueError if `heating_value` is set without a valid `carrier`.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
# Create a pint quantity from magnitude and units
|
|
101
|
+
if self.units:
|
|
102
|
+
# `units` may contain an undefined currency unit - ensure the ureg can handle it
|
|
103
|
+
technologydata.ureg.ensure_currency_is_unit(self.units)
|
|
104
|
+
|
|
105
|
+
self._pint_quantity = technologydata.ureg.Quantity(
|
|
106
|
+
self.magnitude, self.units
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
self._pint_quantity = technologydata.ureg.Quantity(self.magnitude)
|
|
110
|
+
# Create the carrier as pint unit
|
|
111
|
+
if self.carrier:
|
|
112
|
+
self._pint_carrier = technologydata.creg.Unit(self.carrier)
|
|
113
|
+
else:
|
|
114
|
+
self._pint_carrier = None
|
|
115
|
+
|
|
116
|
+
# Create the heating value as pint unit
|
|
117
|
+
if self.heating_value and self.carrier:
|
|
118
|
+
self._pint_heating_value = technologydata.hvreg.Unit(self.heating_value)
|
|
119
|
+
elif self.heating_value and not self.carrier:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"Heating value cannot be set without a carrier. Please provide a valid carrier."
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
self._pint_heating_value = None
|
|
125
|
+
|
|
126
|
+
def to(self, units: str) -> Self:
|
|
127
|
+
"""Convert the parameter's quantity to new units."""
|
|
128
|
+
self._update_pint_attributes()
|
|
129
|
+
|
|
130
|
+
# Do not allow for currency conversion here, as it requires additional information
|
|
131
|
+
if technologydata.extract_currency_units(
|
|
132
|
+
self._pint_quantity.units
|
|
133
|
+
) != technologydata.extract_currency_units(units):
|
|
134
|
+
raise NotImplementedError(
|
|
135
|
+
"Currency conversion is not supported in the `to` method. "
|
|
136
|
+
"Use `to_currency` for currency conversions."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self._pint_quantity = self._pint_quantity.to(units)
|
|
140
|
+
return Parameter(
|
|
141
|
+
magnitude=self._pint_quantity.magnitude,
|
|
142
|
+
units=str(self._pint_quantity.units),
|
|
143
|
+
carrier=self.carrier,
|
|
144
|
+
heating_value=self.heating_value,
|
|
145
|
+
provenance=self.provenance,
|
|
146
|
+
note=self.note,
|
|
147
|
+
sources=self.sources,
|
|
148
|
+
) # type: ignore
|
|
149
|
+
|
|
150
|
+
def to_currency(
|
|
151
|
+
self, target_currency: str, country: str, source: str = "worldbank"
|
|
152
|
+
) -> Self:
|
|
153
|
+
"""
|
|
154
|
+
Change the currency of the parameter.
|
|
155
|
+
|
|
156
|
+
This allows for conversion to a different currency as well as for inflation adjustments.
|
|
157
|
+
To properly adjust for inflation, the function requires the `country` for which the inflation
|
|
158
|
+
adjustment should be applied for.
|
|
159
|
+
|
|
160
|
+
Note that this will harmonise all currencies used in the parameter's units,
|
|
161
|
+
i.e. if the parameter `units` contains multiple different currencies,
|
|
162
|
+
all of them will be converted to the target currency.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
target_currency : str
|
|
167
|
+
The target currency unit to convert to, e.g. "USD_2020", "EUR_2024", "CNY_2022".
|
|
168
|
+
country : str
|
|
169
|
+
The country for which the inflation adjustment should be made for.
|
|
170
|
+
Must be the official ISO 3166-1 alpha-3 country code, e.g. "USA", "DEU", "CHN".
|
|
171
|
+
source : str, optional
|
|
172
|
+
The source of the inflation data, either "worldbank"/"wb" or "international_monetary_fund"/"imf".
|
|
173
|
+
Defaults to "worldbank".
|
|
174
|
+
Depending on the source, different years to adjust for inflation may be available.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
Parameter
|
|
179
|
+
A new Parameter object with the converted currency.
|
|
180
|
+
|
|
181
|
+
Examples
|
|
182
|
+
--------
|
|
183
|
+
>>> param.to_currency("USD_2024", "USA")
|
|
184
|
+
>>> param.to_currency("EUR_2020", "DEU", source="imf")
|
|
185
|
+
>>> param.to_currency("EUR_2023", "USA", source="worldbank")
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
self._update_pint_attributes()
|
|
189
|
+
|
|
190
|
+
# Ensure the target currency is a valid unit
|
|
191
|
+
technologydata.ureg.ensure_currency_is_unit(target_currency)
|
|
192
|
+
|
|
193
|
+
# Current unit and currency/currencies
|
|
194
|
+
from_units = self._pint_quantity.units
|
|
195
|
+
from_currencies = technologydata.extract_currency_units(from_units)
|
|
196
|
+
# Replace all currency units in the from_units with the target currency
|
|
197
|
+
to_units = technologydata.CURRENCY_UNIT_PATTERN.sub(
|
|
198
|
+
target_currency, str(from_units)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Create a temporary context to which we add the conversion rates
|
|
202
|
+
# We use a temporary context to avoid polluting the global unit registry
|
|
203
|
+
# with potentially invalid or incomplete conversion rates that do not
|
|
204
|
+
# match the `country` and `source` parameters.
|
|
205
|
+
context = technologydata.ureg.Context()
|
|
206
|
+
|
|
207
|
+
# Conversion rates are all relative to the reference currency
|
|
208
|
+
ref_currency = technologydata.ureg.get_reference_currency()
|
|
209
|
+
ref_currency_p = technologydata.CURRENCY_UNIT_PATTERN.match(ref_currency)
|
|
210
|
+
if ref_currency_p:
|
|
211
|
+
ref_iso3 = technologydata.get_iso3_from_currency_code(
|
|
212
|
+
ref_currency_p.group("cu_iso3")
|
|
213
|
+
)
|
|
214
|
+
ref_year = ref_currency_p.group("year")
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Reference currency '{ref_currency}' does not match expected pattern."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Get conversion rates for all involved currencies
|
|
221
|
+
currencies = set(from_currencies).union({target_currency})
|
|
222
|
+
# Avoid recursion error in pint definition by re-adding the reference currency
|
|
223
|
+
currencies = currencies - {ref_currency}
|
|
224
|
+
|
|
225
|
+
for currency in currencies:
|
|
226
|
+
from_currency_p = technologydata.CURRENCY_UNIT_PATTERN.match(currency)
|
|
227
|
+
if from_currency_p:
|
|
228
|
+
from_iso3 = technologydata.get_iso3_from_currency_code(
|
|
229
|
+
from_currency_p.group("cu_iso3")
|
|
230
|
+
)
|
|
231
|
+
from_year = from_currency_p.group("year")
|
|
232
|
+
else:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Currency '{currency}' does not match expected pattern."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
conversion_rate = technologydata.get_conversion_rate(
|
|
238
|
+
from_iso3=from_iso3,
|
|
239
|
+
from_year=from_year,
|
|
240
|
+
to_iso3=ref_iso3,
|
|
241
|
+
to_year=int(ref_year),
|
|
242
|
+
country=country,
|
|
243
|
+
source=source,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
context.redefine(f"{currency} = {conversion_rate} * {ref_currency}")
|
|
247
|
+
|
|
248
|
+
# Actual conversion using pint
|
|
249
|
+
quantity = self._pint_quantity.to(to_units, context)
|
|
250
|
+
|
|
251
|
+
return Parameter(
|
|
252
|
+
magnitude=quantity.magnitude,
|
|
253
|
+
units=str(quantity.units),
|
|
254
|
+
carrier=self.carrier,
|
|
255
|
+
heating_value=self.heating_value,
|
|
256
|
+
provenance=self.provenance,
|
|
257
|
+
note=self.note,
|
|
258
|
+
sources=self.sources,
|
|
259
|
+
) # type: ignore
|
|
260
|
+
|
|
261
|
+
def change_heating_value(self, to_heating_value: str) -> Self:
|
|
262
|
+
"""
|
|
263
|
+
Change the heating value of the parameter.
|
|
264
|
+
|
|
265
|
+
This converts the parameter's heating value to another heating value,
|
|
266
|
+
e.g. from "LHV" to "HHV", by taking into account the parameter's carrier.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
to_heating_value : str
|
|
271
|
+
The target heating value to convert to, e.g. "LHV", "HHV".
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
Parameter
|
|
276
|
+
A new Parameter object with the converted heating value.
|
|
277
|
+
|
|
278
|
+
Raises
|
|
279
|
+
------
|
|
280
|
+
ValueError
|
|
281
|
+
If the current parameter does not have a carrier or a heating value set,
|
|
282
|
+
|
|
283
|
+
Examples
|
|
284
|
+
--------
|
|
285
|
+
>>> Parameter(magnitude=1, units="kWh", carrier="H2", heating_value="LHV").change_heating_value("HHV")
|
|
286
|
+
<Parameter magnitude=1.5, units='kWh', carrier='H2', heating_value='HHV'>
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
if not self.carrier:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
"Cannot change heating value without a carrier. Please provide a valid carrier."
|
|
292
|
+
)
|
|
293
|
+
if not self.heating_value:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
"Cannot change heating value without a current heating value. "
|
|
296
|
+
"Please provide a valid heating value."
|
|
297
|
+
)
|
|
298
|
+
if to_heating_value == self.heating_value:
|
|
299
|
+
# No change needed, return the same parameter
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
self._update_pint_attributes()
|
|
303
|
+
|
|
304
|
+
from technologydata.constants import EnergyDensityHHV, EnergyDensityLHV
|
|
305
|
+
|
|
306
|
+
# Create a dictionary of heating value ratios based on energy densities
|
|
307
|
+
# The units of heating values are harmonized to "hv_units".
|
|
308
|
+
# hv_units is the units attribute of the first element of EnergyDensityLHV
|
|
309
|
+
hv_ratios = dict()
|
|
310
|
+
|
|
311
|
+
# Access the key of the first element of the EnergyDensityLHV dictionary
|
|
312
|
+
first_pair_key = next(iter(EnergyDensityLHV))
|
|
313
|
+
|
|
314
|
+
# Get the units attribute of the first element of the EnergyDensityLHV dictionary
|
|
315
|
+
hv_units = str(EnergyDensityLHV[first_pair_key].units)
|
|
316
|
+
|
|
317
|
+
lhvs = {
|
|
318
|
+
str(technologydata.creg.get_dimensionality(k)): v.to(hv_units)
|
|
319
|
+
for k, v in EnergyDensityLHV.items()
|
|
320
|
+
}
|
|
321
|
+
hhvs = {
|
|
322
|
+
str(technologydata.creg.get_dimensionality(k)): v.to(hv_units)
|
|
323
|
+
for k, v in EnergyDensityHHV.items()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for dimension in self._pint_carrier.dimensionality.keys():
|
|
327
|
+
if dimension in lhvs and dimension in hhvs:
|
|
328
|
+
hv_ratios[dimension] = (
|
|
329
|
+
hhvs[dimension].magnitude / lhvs[dimension].magnitude
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
logger.error(
|
|
333
|
+
f"No heating values found for '{dimension}' in EnergyDensityLHV or EnergyDensityHHV. "
|
|
334
|
+
f"Assuming a ratio of 1."
|
|
335
|
+
)
|
|
336
|
+
hv_ratios[dimension] = 1.0
|
|
337
|
+
|
|
338
|
+
# When converting from HHV -> LHV, we need to multiply by the ratios
|
|
339
|
+
# When converting from LHV -> HHV, we need to divide by the ratios
|
|
340
|
+
# We modify the hv_ratios dictionary to match the conversion direction
|
|
341
|
+
if technologydata.hvreg.Unit(to_heating_value).is_compatible_with("HHV"):
|
|
342
|
+
hv_ratios = hv_ratios
|
|
343
|
+
elif technologydata.hvreg.Unit(to_heating_value).is_compatible_with("LHV"):
|
|
344
|
+
hv_ratios = {k: 1 / v for k, v in hv_ratios.items()}
|
|
345
|
+
|
|
346
|
+
multiplier = 1
|
|
347
|
+
for dim, exponent in self._pint_carrier.dimensionality.items():
|
|
348
|
+
if dim not in hv_ratios:
|
|
349
|
+
raise NotImplementedError(
|
|
350
|
+
f"Heating value conversion not implemented for carrier dimension '{dim}'."
|
|
351
|
+
)
|
|
352
|
+
# Adjust the hv_ratios for the exponent of the carrier
|
|
353
|
+
multiplier *= hv_ratios[dim] ** exponent
|
|
354
|
+
|
|
355
|
+
return Parameter(
|
|
356
|
+
magnitude=self.magnitude * multiplier,
|
|
357
|
+
units=self.units,
|
|
358
|
+
carrier=self.carrier,
|
|
359
|
+
heating_value=to_heating_value,
|
|
360
|
+
provenance=self.provenance, # TODO implement for this function
|
|
361
|
+
note=self.note,
|
|
362
|
+
sources=self.sources,
|
|
363
|
+
) # type: ignore
|
|
364
|
+
|
|
365
|
+
def _check_parameter_compatibility(self, other: Self) -> None:
|
|
366
|
+
"""
|
|
367
|
+
Check if two parameters are compatible in terms of units, carrier, and heating value.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
other : Parameter
|
|
372
|
+
The other Parameter instance to compare against.
|
|
373
|
+
|
|
374
|
+
Raises
|
|
375
|
+
------
|
|
376
|
+
ValueError
|
|
377
|
+
If the carriers or heating values of the two parameters are not compatible.
|
|
378
|
+
The error message specifies which attribute differs.
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
if self._pint_carrier != other._pint_carrier:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"Operation not permitted on parameters with different carriers: "
|
|
384
|
+
f"'{self._pint_carrier}' and '{other._pint_carrier}'."
|
|
385
|
+
)
|
|
386
|
+
if self._pint_heating_value != other._pint_heating_value:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"Operation not permitted on parameters with different heating values: "
|
|
389
|
+
f"'{self._pint_heating_value}' and '{other._pint_heating_value}'."
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def __add__(self, other: Self) -> Self:
|
|
393
|
+
"""
|
|
394
|
+
Add this Parameter to another Parameter.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
other : Parameter
|
|
399
|
+
The Parameter instance to add.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
Parameter
|
|
404
|
+
A new Parameter instance representing the sum of the two parameters.
|
|
405
|
+
|
|
406
|
+
Notes
|
|
407
|
+
-----
|
|
408
|
+
This method checks for parameter compatibility before performing the addition.
|
|
409
|
+
The resulting Parameter retains the carrier, heating value, and combines provenance,
|
|
410
|
+
notes, and sources from both operands.
|
|
411
|
+
|
|
412
|
+
"""
|
|
413
|
+
self._check_parameter_compatibility(other)
|
|
414
|
+
new_quantity = self._pint_quantity + other._pint_quantity
|
|
415
|
+
return Parameter(
|
|
416
|
+
magnitude=new_quantity.magnitude,
|
|
417
|
+
units=new_quantity.units,
|
|
418
|
+
carrier=self.carrier,
|
|
419
|
+
heating_value=self.heating_value,
|
|
420
|
+
provenance=(self.provenance or "")
|
|
421
|
+
+ (other.provenance or ""), # TODO make nicer
|
|
422
|
+
note=(self.note or "") + (other.note or ""), # TODO make nicer
|
|
423
|
+
sources=SourceCollection(
|
|
424
|
+
sources=(self.sources.sources + other.sources.sources)
|
|
425
|
+
),
|
|
426
|
+
) # type: ignore
|
|
427
|
+
|
|
428
|
+
def __sub__(self, other: Self) -> Self:
|
|
429
|
+
"""
|
|
430
|
+
Subtract another Parameter from this Parameter.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
other : Parameter
|
|
435
|
+
The Parameter instance to subtract.
|
|
436
|
+
|
|
437
|
+
Returns
|
|
438
|
+
-------
|
|
439
|
+
Parameter
|
|
440
|
+
A new Parameter instance representing the result of the subtraction.
|
|
441
|
+
|
|
442
|
+
Notes
|
|
443
|
+
-----
|
|
444
|
+
This method checks for parameter compatibility before performing the subtraction.
|
|
445
|
+
The resulting Parameter retains the carrier, heating value, and combines provenance, notes, and sources.
|
|
446
|
+
|
|
447
|
+
"""
|
|
448
|
+
self._check_parameter_compatibility(other)
|
|
449
|
+
new_quantity = self._pint_quantity - other._pint_quantity
|
|
450
|
+
return Parameter(
|
|
451
|
+
magnitude=new_quantity.magnitude,
|
|
452
|
+
units=str(new_quantity.units),
|
|
453
|
+
carrier=self.carrier,
|
|
454
|
+
heating_value=self.heating_value,
|
|
455
|
+
provenance=(self.provenance or "")
|
|
456
|
+
+ (other.provenance or ""), # TODO make nicer
|
|
457
|
+
note=(self.note or "") + (other.note or ""), # TODO make nicer
|
|
458
|
+
sources=SourceCollection(
|
|
459
|
+
sources=(self.sources.sources + other.sources.sources)
|
|
460
|
+
),
|
|
461
|
+
) # type: ignore
|
|
462
|
+
|
|
463
|
+
def __truediv__(self, other: int | float | Self) -> Self:
|
|
464
|
+
"""
|
|
465
|
+
Divide this Parameter by another Parameter.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
other : float | Parameter
|
|
470
|
+
A scalar or a Parameter instance to divide by.
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
Parameter
|
|
475
|
+
A new Parameter instance representing the division result.
|
|
476
|
+
|
|
477
|
+
Raises
|
|
478
|
+
------
|
|
479
|
+
ValueError
|
|
480
|
+
If the heating values of the two parameters are different.
|
|
481
|
+
|
|
482
|
+
Notes
|
|
483
|
+
-----
|
|
484
|
+
The method divides the quantities of the parameters and constructs a new Parameter.
|
|
485
|
+
It also handles the division of carriers and heating values if present.
|
|
486
|
+
|
|
487
|
+
"""
|
|
488
|
+
if isinstance(other, (int | float)):
|
|
489
|
+
return Parameter(
|
|
490
|
+
magnitude=self.magnitude / other,
|
|
491
|
+
units=self.units,
|
|
492
|
+
carrier=self.carrier,
|
|
493
|
+
heating_value=self.heating_value,
|
|
494
|
+
provenance=self.provenance,
|
|
495
|
+
note=self.note,
|
|
496
|
+
sources=self.sources,
|
|
497
|
+
) # type: ignore
|
|
498
|
+
|
|
499
|
+
# We don't check general compatibility here, as division is not a common operation for parameters.
|
|
500
|
+
# Only ensure that the heating values are compatible.
|
|
501
|
+
if self._pint_heating_value != other._pint_heating_value:
|
|
502
|
+
raise ValueError(
|
|
503
|
+
f"Cannot divide parameters with different heating values: "
|
|
504
|
+
f"{self._pint_heating_value} and {other._pint_heating_value}."
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
new_quantity = self._pint_quantity / other._pint_quantity
|
|
508
|
+
new_carrier = (
|
|
509
|
+
self._pint_carrier / other._pint_carrier
|
|
510
|
+
if self._pint_carrier and other._pint_carrier
|
|
511
|
+
else None
|
|
512
|
+
)
|
|
513
|
+
new_heating_value = (
|
|
514
|
+
self._pint_heating_value / other._pint_heating_value
|
|
515
|
+
if self._pint_heating_value and other._pint_heating_value
|
|
516
|
+
else None
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return Parameter(
|
|
520
|
+
magnitude=new_quantity.magnitude,
|
|
521
|
+
units=str(new_quantity.units),
|
|
522
|
+
carrier=new_carrier,
|
|
523
|
+
heating_value=new_heating_value,
|
|
524
|
+
provenance=(self.provenance or "")
|
|
525
|
+
+ (other.provenance or ""), # TODO make nicer
|
|
526
|
+
note=(self.note or "") + (other.note or ""), # TODO make nicer
|
|
527
|
+
sources=SourceCollection(
|
|
528
|
+
sources=(self.sources.sources + other.sources.sources)
|
|
529
|
+
),
|
|
530
|
+
) # type: ignore
|
|
531
|
+
|
|
532
|
+
def __mul__(self, other: int | float | Self) -> Self:
|
|
533
|
+
"""
|
|
534
|
+
Multiply two Parameter instances.
|
|
535
|
+
|
|
536
|
+
Parameters
|
|
537
|
+
----------
|
|
538
|
+
other : int | float | Parameter
|
|
539
|
+
A scalar or a Parameter instance to multiply with.
|
|
540
|
+
|
|
541
|
+
Returns
|
|
542
|
+
-------
|
|
543
|
+
Parameter
|
|
544
|
+
A new Parameter instance representing the product of the two parameters.
|
|
545
|
+
|
|
546
|
+
Raises
|
|
547
|
+
------
|
|
548
|
+
ValueError
|
|
549
|
+
If the heating values of the two parameters are not compatible (i.e., not equal).
|
|
550
|
+
|
|
551
|
+
Notes
|
|
552
|
+
-----
|
|
553
|
+
- Multiplication is only performed if the heating values are compatible.
|
|
554
|
+
- The method multiplies the underlying quantities and carriers (if present).
|
|
555
|
+
- The heating value of the resulting parameter is the product of the input heating values.
|
|
556
|
+
- Provenance, notes, and sources are combined from both parameters.
|
|
557
|
+
- Compatibility checks beyond heating values are not performed.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
if isinstance(other, int | float):
|
|
561
|
+
return Parameter(
|
|
562
|
+
magnitude=self.magnitude * other,
|
|
563
|
+
units=self.units,
|
|
564
|
+
carrier=self.carrier,
|
|
565
|
+
heating_value=self.heating_value,
|
|
566
|
+
provenance=self.provenance,
|
|
567
|
+
note=self.note,
|
|
568
|
+
sources=self.sources,
|
|
569
|
+
) # type: ignore
|
|
570
|
+
|
|
571
|
+
# We don't check general compatibility here, as multiplication is not a common operation for parameters.
|
|
572
|
+
# Only ensure that the heating values are compatible.
|
|
573
|
+
if self._pint_heating_value != other._pint_heating_value:
|
|
574
|
+
raise ValueError(
|
|
575
|
+
f"Cannot multiply parameters with different heating values: "
|
|
576
|
+
f"{self._pint_heating_value} and {other._pint_heating_value}."
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
new_quantity = self._pint_quantity * other._pint_quantity
|
|
580
|
+
new_carrier = (
|
|
581
|
+
self._pint_carrier * other._pint_carrier
|
|
582
|
+
if self._pint_carrier and other._pint_carrier
|
|
583
|
+
else None
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
new_heating_value = self._pint_heating_value * other._pint_heating_value
|
|
587
|
+
return Parameter(
|
|
588
|
+
magnitude=new_quantity.magnitude,
|
|
589
|
+
units=str(new_quantity.units),
|
|
590
|
+
carrier=str(new_carrier),
|
|
591
|
+
heating_value=str(new_heating_value),
|
|
592
|
+
provenance=(self.provenance or "")
|
|
593
|
+
+ (other.provenance or ""), # TODO make nicer
|
|
594
|
+
note=(self.note or "") + (other.note or ""), # TODO make nicer
|
|
595
|
+
sources=SourceCollection(
|
|
596
|
+
sources=(self.sources.sources + other.sources.sources)
|
|
597
|
+
),
|
|
598
|
+
) # type: ignore
|
|
599
|
+
|
|
600
|
+
def __eq__(self, other: object) -> bool:
|
|
601
|
+
"""
|
|
602
|
+
Check for equality with another Parameter object.
|
|
603
|
+
|
|
604
|
+
Compares all attributes of the current instance with those of the other object.
|
|
605
|
+
|
|
606
|
+
Parameters
|
|
607
|
+
----------
|
|
608
|
+
other : object
|
|
609
|
+
The object to compare with. Expected to be an instance of Parameter.
|
|
610
|
+
|
|
611
|
+
Returns
|
|
612
|
+
-------
|
|
613
|
+
bool
|
|
614
|
+
True if all attributes are equal between self and other, False otherwise.
|
|
615
|
+
Returns False if other is not a Parameter instance.
|
|
616
|
+
|
|
617
|
+
"""
|
|
618
|
+
if not isinstance(other, Parameter):
|
|
619
|
+
return NotImplemented
|
|
620
|
+
|
|
621
|
+
self._update_pint_attributes()
|
|
622
|
+
other._update_pint_attributes()
|
|
623
|
+
|
|
624
|
+
for field in self.__class__.model_fields.keys():
|
|
625
|
+
value_self = getattr(self, field)
|
|
626
|
+
value_other = getattr(other, field)
|
|
627
|
+
if value_self != value_other:
|
|
628
|
+
return False
|
|
629
|
+
return True
|
|
630
|
+
|
|
631
|
+
def __pow__(self, exponent: float | int) -> Self:
|
|
632
|
+
"""
|
|
633
|
+
Raise the parameter's value to a specified power.
|
|
634
|
+
|
|
635
|
+
Parameters
|
|
636
|
+
----------
|
|
637
|
+
exponent : float or int
|
|
638
|
+
The exponent to raise the parameter's value to.
|
|
639
|
+
|
|
640
|
+
Returns
|
|
641
|
+
-------
|
|
642
|
+
Parameter
|
|
643
|
+
A new Parameter instance with the value raised to the specified power.
|
|
644
|
+
|
|
645
|
+
Notes
|
|
646
|
+
-----
|
|
647
|
+
This method updates the internal pint attributes before applying the power operation.
|
|
648
|
+
If the parameter has a carrier, it is also raised to the specified power.
|
|
649
|
+
|
|
650
|
+
"""
|
|
651
|
+
self._update_pint_attributes()
|
|
652
|
+
|
|
653
|
+
new_quantity = self._pint_quantity**exponent
|
|
654
|
+
return Parameter(
|
|
655
|
+
magnitude=new_quantity.magnitude,
|
|
656
|
+
units=str(new_quantity.units),
|
|
657
|
+
carrier=self._pint_carrier**exponent if self._pint_carrier else None,
|
|
658
|
+
heating_value=self.heating_value,
|
|
659
|
+
provenance=self.provenance,
|
|
660
|
+
note=self.note,
|
|
661
|
+
sources=self.sources,
|
|
662
|
+
) # type: ignore
|