Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ converting one of them.
Quantities store the value in a base unit, and then provide methods to get that
quantity as a particular unit.

## Documentation

For more information on how to use this library and examples, please check the
[Documentation website](https://frequenz-floss.github.io/frequenz-quantities-python/).

## Supported Platforms

The following platforms are officially supported (tested):
Expand Down
14 changes: 1 addition & 13 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,4 @@

## Summary

<!-- Here goes a general summary of what this release is about -->

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->

## New Features

<!-- Here goes the main new features and examples or instructions on how to use them -->

## Bug Fixes

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
This is the initial release, extracted from the [SDK v1.0.0rc601](https://github.com/frequenz-floss/frequenz-sdk-python/releases/tag/v1.0.0-rc601).
10 changes: 9 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
--8<-- "README.md"
# Frequenz Quantities Library

::: frequenz.quantities
options:
members: []
show_bases: false
show_root_heading: false
show_root_toc_entry: false
show_source: false
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ site_author: "Frequenz Energy-as-a-Service GmbH"
copyright: "Copyright © 2024 Frequenz Energy-as-a-Service GmbH"
repo_name: "frequenz-quantities-python"
repo_url: "https://github.com/frequenz-floss/frequenz-quantities-python"
edit_uri: "edit/v0.x.x/docs/"
edit_uri: "edit/v1.x.x/docs/"
strict: true # Treat warnings as errors

# Build directories
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ dev-pytest = [
"pytest-mock == 3.14.0",
"pytest-asyncio == 0.23.7",
"async-solipsism == 0.6",
"hypothesis == 6.100.2",
]
dev = [
"frequenz-quantities[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
Expand Down
103 changes: 88 additions & 15 deletions src/frequenz/quantities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,96 @@

"""Types for holding quantities with units.

TODO(cookiecutter): Add a more descriptive module description.
"""
This library provide types for holding quantities with units. The main goal is to avoid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe remove the first sentence as it is equal to the headline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a title/summary, like with git commits. There are some options or places where only the summary line is shown.

mistakes while working with different types of quantities, for example avoiding adding
a length to a time.

It also prevents mistakes when operating between the same quantity but in different
units, like adding a power in Joules to a power in Watts without converting one of them.

Quantities store the value in a base unit, and then provide methods to get that quantity
as a particular unit. They can only be constructed using special constructors with the
form `Quantity.from_<unit>`, for example
[`Power.from_watts(10.0)`][frequenz.quantities.Power.from_watts].

Internally quantities store values as `float`s, so regular [float issues and limitations
apply](https://docs.python.org/3/tutorial/floatingpoint.html), although some of them are
tried to be mitigated.

Quantities are also immutable, so operations between quantities return a new instance of
the quantity.

This library provides the following types:

- [Current][frequenz.quantities.Current]: A quantity representing an electric current.
- [Energy][frequenz.quantities.Energy]: A quantity representing energy.
- [Frequency][frequenz.quantities.Frequency]: A quantity representing frequency.
- [Percentage][frequenz.quantities.Percentage]: A quantity representing a percentage.
- [Power][frequenz.quantities.Power]: A quantity representing power.
- [Temperature][frequenz.quantities.Temperature]: A quantity representing temperature.
- [Voltage][frequenz.quantities.Voltage]: A quantity representing electric voltage.

There is also the unitless [Quantity][frequenz.quantities.Quantity] class. All
quantities are subclasses of this class and it can be used as a base to create new
quantities. Using the `Quantity` class directly is discouraged, as it doesn't provide
any unit conversion methods.

# TODO(cookiecutter): Remove this function
def delete_me(*, blow_up: bool = False) -> bool:
"""Do stuff for demonstration purposes.
Example:
```python
from datetime import timedelta
from frequenz.quantities import Power, Voltage, Current, Energy

# Create a power quantity
power = Power.from_watts(230.0)

# Printing uses a unit to make the string as short as possible
print(f"Power: {power}") # Power: 230.0 W
# The precision can be changed
print(f"Power: {power:0.3}") # Power: 230.000 W
# The conversion methods can be used to get the value in a particular unit
print(f"Power in MW: {power.as_megawatt()}") # Power in MW: 0.00023 MW

# Create a voltage quantity
voltage = Voltage.from_volts(230.0)

# Calculate the current
current = power / voltage
assert isinstance(current, Current)
print(f"Current: {current}") # Current: 1.0 A
assert current.isclose(Current.from_amperes(1.0))

# Calculate the energy
energy = power * timedelta(hours=1)
assert isinstance(energy, Energy)
print(f"Energy: {energy}") # Energy: 230.0 Wh
print(f"Energy in kWh: {energy.as_kilowatt_hours()}") # Energy in kWh: 0.23

# Invalid operations are not permitted
# (when using a type hinting linter like mypy, this will be caught at linting time)
try:
power + voltage
except TypeError as e:
print(f"Error: {e}") # Error: unsupported operand type(s) for +: 'Power' and 'Voltage'
```
"""

Args:
blow_up: If True, raise an exception.

Returns:
True if no exception was raised.
from ._current import Current
from ._energy import Energy
from ._frequency import Frequency
from ._percentage import Percentage
from ._power import Power
from ._quantity import Quantity
from ._temperature import Temperature
from ._voltage import Voltage

Raises:
RuntimeError: if blow_up is True.
"""
if blow_up:
raise RuntimeError("This function should be removed!")
return True
__all__ = [
"Current",
"Energy",
"Frequency",
"Percentage",
"Power",
"Quantity",
"Temperature",
"Voltage",
]
131 changes: 131 additions & 0 deletions src/frequenz/quantities/_current.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# License: MIT
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH

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


from __future__ import annotations

from typing import TYPE_CHECKING, Self, overload

from ._quantity import NoDefaultConstructible, Quantity

if TYPE_CHECKING:
from ._percentage import Percentage
from ._power import Power
from ._voltage import Voltage


class Current(
Quantity,
metaclass=NoDefaultConstructible,
exponent_unit_map={
-3: "mA",
0: "A",
},
):
"""A current quantity.

Objects of this type are wrappers around `float` values and are immutable.

The constructors accept a single `float` value, the `as_*()` methods return a
`float` value, and each of the arithmetic operators supported by this type are
actually implemented using floating-point arithmetic.

So all considerations about floating-point arithmetic apply to this type as well.
"""

@classmethod
def from_amperes(cls, amperes: float) -> Self:
"""Initialize a new current quantity.

Args:
amperes: The current in amperes.

Returns:
A new current quantity.
"""
return cls._new(amperes)

@classmethod
def from_milliamperes(cls, milliamperes: float) -> Self:
"""Initialize a new current quantity.

Args:
milliamperes: The current in milliamperes.

Returns:
A new current quantity.
"""
return cls._new(milliamperes, exponent=-3)

def as_amperes(self) -> float:
"""Return the current in amperes.

Returns:
The current in amperes.
"""
return self._base_value

def as_milliamperes(self) -> float:
"""Return the current in milliamperes.

Returns:
The current in milliamperes.
"""
return self._base_value * 1e3

# See comment for Power.__mul__ for why we need the ignore here.
@overload # type: ignore[override]
def __mul__(self, scalar: float, /) -> Self:
"""Scale this current by a scalar.

Args:
scalar: The scalar by which to scale this current.

Returns:
The scaled current.
"""

@overload
def __mul__(self, percent: Percentage, /) -> Self:
"""Scale this current by a percentage.

Args:
percent: The percentage by which to scale this current.

Returns:
The scaled current.
"""

@overload
def __mul__(self, other: Voltage, /) -> Power:
"""Multiply the current by a voltage to get a power.

Args:
other: The voltage.

Returns:
The calculated power.
"""

def __mul__(self, other: float | Percentage | Voltage, /) -> Self | Power:
"""Return a current or power from multiplying this current by the given value.

Args:
other: The scalar, percentage or voltage to multiply by.

Returns:
A current or power.
"""
from ._percentage import Percentage # pylint: disable=import-outside-toplevel
from ._power import Power # pylint: disable=import-outside-toplevel
from ._voltage import Voltage # pylint: disable=import-outside-toplevel

match other:
case float() | Percentage():
return super().__mul__(other)
case Voltage():
return Power._new(self._base_value * other._base_value)
case _:
return NotImplemented
Loading