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,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