Skip to content

Commit a8d1545

Browse files
committed
Add methods for composing quantities with each other
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 6c4621f commit a8d1545

File tree

2 files changed

+129
-1
lines changed

2 files changed

+129
-1
lines changed

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
"""Types for holding quantities with units."""
55

6+
from __future__ import annotations
7+
68
import math
7-
from typing import Self
9+
from datetime import timedelta
10+
from typing import Self, overload
811

912

1013
class Quantity:
@@ -353,6 +356,53 @@ def as_megawatts(self) -> float:
353356
"""
354357
return self._base_value / 1e6
355358

359+
def __mul__(self, duration: timedelta) -> Energy:
360+
"""Return an energy from multiplying this power by the given duration.
361+
362+
Args:
363+
duration: The duration to multiply by.
364+
365+
Returns:
366+
An energy from multiplying this power by the given duration.
367+
"""
368+
return Energy(self._base_value * duration.total_seconds() / 3600.0, exponent=0)
369+
370+
@overload
371+
def __truediv__(self, other: Current) -> Voltage:
372+
"""Return a voltage from dividing this power by the given current.
373+
374+
Args:
375+
other: The current to divide by.
376+
"""
377+
378+
@overload
379+
def __truediv__(self, other: Voltage) -> Current:
380+
"""Return a current from dividing this power by the given voltage.
381+
382+
Args:
383+
other: The voltage to divide by.
384+
"""
385+
386+
def __truediv__(self, other: Current | Voltage) -> Voltage | Current:
387+
"""Return a current or voltage from dividing this power by the given value.
388+
389+
Args:
390+
other: The current or voltage to divide by.
391+
392+
Returns:
393+
A current or voltage from dividing this power by the given value.
394+
395+
Raises:
396+
TypeError: If the given value is not a current or voltage.
397+
"""
398+
if isinstance(other, Current):
399+
return Voltage(self._base_value / other._base_value, exponent=0)
400+
if isinstance(other, Voltage):
401+
return Current(self._base_value / other._base_value, exponent=0)
402+
raise TypeError(
403+
f"unsupported operand type(s) for /: '{type(self)}' and '{type(other)}'"
404+
)
405+
356406

357407
class Current(
358408
Quantity,
@@ -403,6 +453,17 @@ def as_milliamperes(self) -> float:
403453
"""
404454
return self._base_value * 1e3
405455

456+
def __mul__(self, voltage: Voltage) -> Power:
457+
"""Multiply the current by a voltage to get a power.
458+
459+
Args:
460+
voltage: The voltage.
461+
462+
Returns:
463+
The power.
464+
"""
465+
return Power(self._base_value * voltage._base_value, exponent=0)
466+
406467

407468
class Voltage(Quantity, exponent_unit_map={0: "V", -3: "mV", 3: "kV"}):
408469
"""A voltage quantity."""
@@ -467,6 +528,17 @@ def as_kilovolts(self) -> float:
467528
"""
468529
return self._base_value / 1e3
469530

531+
def __mul__(self, current: Current) -> Power:
532+
"""Multiply the voltage by the current to get the power.
533+
534+
Args:
535+
current: The current to multiply the voltage with.
536+
537+
Returns:
538+
The calculated power.
539+
"""
540+
return Power(self._base_value * current._base_value, exponent=0)
541+
470542

471543
class Energy(
472544
Quantity,
@@ -537,3 +609,41 @@ def as_megawatt_hours(self) -> float:
537609
The energy in megawatt hours.
538610
"""
539611
return self._base_value / 1e6
612+
613+
@overload
614+
def __truediv__(self, other: timedelta) -> Power:
615+
"""Return a power from dividing this energy by the given duration.
616+
617+
Args:
618+
other: The duration to divide by.
619+
"""
620+
621+
@overload
622+
def __truediv__(self, other: Power) -> timedelta:
623+
"""Return a duration from dividing this energy by the given power.
624+
625+
Args:
626+
other: The power to divide by.
627+
"""
628+
629+
def __truediv__(self, other: timedelta | Power) -> Power | timedelta:
630+
"""Return a power or duration from dividing this energy by the given value.
631+
632+
Args:
633+
other: The power or duration to divide by.
634+
635+
Returns:
636+
A power or duration from dividing this energy by the given value.
637+
638+
Raises:
639+
TypeError: If the given value is not a power or duration.
640+
"""
641+
if isinstance(other, timedelta):
642+
return Power(
643+
self._base_value / (other.total_seconds() / 3600.0), exponent=0
644+
)
645+
if isinstance(other, Power):
646+
return timedelta(seconds=(self._base_value / other._base_value) * 3600.0)
647+
raise TypeError(
648+
f"unsupported operand type(s) for /: '{type(self)}' and '{type(other)}'"
649+
)

tests/timeseries/test_quantities.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
"""Tests for quantity types."""
55

6+
from datetime import timedelta
7+
68
import pytest
79

810
from frequenz.sdk.timeseries._quantities import (
@@ -215,3 +217,19 @@ def test_energy() -> None:
215217
assert energy == Energy.from_megawatt_hours(0.006)
216218
assert energy == Energy.from_kilowatt_hours(6.0)
217219
assert energy != Energy.from_kilowatt_hours(5.0)
220+
221+
222+
def test_quantity_compositions() -> None:
223+
"""Test the composition of quantities."""
224+
power = Power.from_watts(1000.0)
225+
voltage = Voltage.from_volts(230.0)
226+
current = Current.from_amperes(4.3478260869565215)
227+
energy = Energy.from_kilowatt_hours(6.2)
228+
229+
assert power / voltage == current
230+
assert power / current == voltage
231+
assert power == voltage * current
232+
233+
assert energy / power == timedelta(hours=6.2)
234+
assert energy / timedelta(hours=6.2) == power
235+
assert energy == power * timedelta(hours=6.2)

0 commit comments

Comments
 (0)