Skip to content

Commit 1a6d3fd

Browse files
authored
Disable default constructor in specialized Quantity types (#465)
2 parents 7c531c9 + 7ac01de commit 1a6d3fd

22 files changed

+292
-148
lines changed

RELEASE_NOTES.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,32 @@
66

77
## Upgrading
88

9+
- `Sample` objects no longer hold `float`s, but rather `Quantity` or one of its subclasses, like `Power`, `Current`, `Energy`, etc. based on the type of values being streamed.
10+
11+
```python
12+
sample: Sample[Power] = await battery_pool.power.new_receiver().receive()
13+
power: float = sample.value.as_watts()
14+
```
15+
16+
- `BatteryPool.soc` now streams values of type `Sample[Quantity]`, and `BatteryPool.capacity` now streams values of type `Sample[Energy]`.
17+
18+
```python
19+
battery_pool = microgrid.battery_pool()
20+
soc_sample: Sample[Quantity] = await battery_pool.soc.new_receiver().receive()
21+
soc: float = soc_sample.value.base_value
22+
23+
capacity_sample: Sample[Energy] = await battery_pool.capacity.new_receiver().receive()
24+
capacity: float = soc_sample.value.as_watt_hours()
25+
```
26+
927
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
1028

1129
## New Features
1230

1331
- The logical meter has a new method that returns producer power, that is the sum of all energy producers.
1432

33+
- `Quantity` types (`Power`, `Current`, `Energy`, `Voltage`) for providing type- and unit-safety when dealing with physical quantities.
34+
1535
<!-- Here goes the main new features and examples or instructions on how to use them -->
1636

1737
## Bug Fixes

src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from datetime import datetime
1313
from math import isinf, isnan
1414
from typing import (
15+
Callable,
1516
Dict,
1617
Generic,
1718
List,
@@ -63,23 +64,23 @@ def __init__(
6364
name: str,
6465
steps: List[FormulaStep],
6566
metric_fetchers: Dict[str, MetricFetcher[QuantityT]],
66-
output_type: Type[QuantityT],
67+
create_method: Callable[[float], QuantityT],
6768
) -> None:
6869
"""Create a `FormulaEngine` instance.
6970
7071
Args:
7172
name: A name for the formula.
7273
steps: Steps for the engine to execute, in post-fix order.
7374
metric_fetchers: Fetchers for each metric stream the formula depends on.
74-
output_type: A type object to generate the output `Sample` with. If the
75-
formula is for generating power values, this would be `Power`, for
76-
example.
75+
create_method: A method to generate the output `Sample` value with. If the
76+
formula is for generating power values, this would be
77+
`Power.from_watts`, for example.
7778
"""
7879
self._name = name
7980
self._steps = steps
8081
self._metric_fetchers: Dict[str, MetricFetcher[QuantityT]] = metric_fetchers
8182
self._first_run = True
82-
self._output_type: type[QuantityT] = output_type
83+
self._create_method: Callable[[float], QuantityT] = create_method
8384

8485
async def _synchronize_metric_timestamps(
8586
self, metrics: Set[asyncio.Task[Optional[Sample[QuantityT]]]]
@@ -172,7 +173,7 @@ async def apply(self) -> Sample[QuantityT]:
172173
if isnan(res) or isinf(res):
173174
return Sample(metric_ts, None)
174175

175-
return Sample(metric_ts, self._output_type(res))
176+
return Sample(metric_ts, self._create_method(res))
176177

177178

178179
_CompositionType = Union[
@@ -218,7 +219,7 @@ class _ComposableFormulaEngine(
218219
):
219220
"""A base class for formula engines."""
220221

221-
_output_type: Type[QuantityT]
222+
_create_method: Callable[[float], QuantityT]
222223
_higher_order_builder: Type[_GenericHigherOrderBuilder]
223224
_task: asyncio.Task[None] | None = None
224225

@@ -242,7 +243,7 @@ def __add__(
242243
A formula builder that can take further expressions, or can be built
243244
into a formula engine.
244245
"""
245-
return self._higher_order_builder(self, self._output_type) + other # type: ignore
246+
return self._higher_order_builder(self, self._create_method) + other # type: ignore
246247

247248
def __sub__(
248249
self, other: _GenericEngine | _GenericHigherOrderBuilder
@@ -257,7 +258,7 @@ def __sub__(
257258
A formula builder that can take further expressions, or can be built
258259
into a formula engine.
259260
"""
260-
return self._higher_order_builder(self, self._output_type) - other # type: ignore
261+
return self._higher_order_builder(self, self._create_method) - other # type: ignore
261262

262263
def __mul__(
263264
self, other: _GenericEngine | _GenericHigherOrderBuilder
@@ -272,7 +273,7 @@ def __mul__(
272273
A formula builder that can take further expressions, or can be built
273274
into a formula engine.
274275
"""
275-
return self._higher_order_builder(self, self._output_type) * other # type: ignore
276+
return self._higher_order_builder(self, self._create_method) * other # type: ignore
276277

277278
def __truediv__(
278279
self, other: _GenericEngine | _GenericHigherOrderBuilder
@@ -287,7 +288,7 @@ def __truediv__(
287288
A formula builder that can take further expressions, or can be built
288289
into a formula engine.
289290
"""
290-
return self._higher_order_builder(self, self._output_type) / other # type: ignore
291+
return self._higher_order_builder(self, self._create_method) / other # type: ignore
291292

292293

293294
class FormulaEngine(
@@ -307,28 +308,28 @@ class FormulaEngine(
307308
def __init__(
308309
self,
309310
builder: FormulaBuilder[QuantityT],
310-
output_type: Type[QuantityT],
311+
create_method: Callable[[float], QuantityT],
311312
) -> None:
312313
"""Create a `FormulaEngine` instance.
313314
314315
Args:
315316
builder: A `FormulaBuilder` instance to get the formula steps and metric
316317
fetchers from.
317-
output_type: A type object to generate the output `Sample` with. If the
318-
formula is for generating power values, this would be `Power`, for
319-
example.
318+
create_method: A method to generate the output `Sample` value with. If the
319+
formula is for generating power values, this would be
320+
`Power.from_watts`, for example.
320321
"""
321322
self._higher_order_builder = HigherOrderFormulaBuilder
322323
self._name: str = builder.name
323324
self._builder: FormulaBuilder[QuantityT] = builder
324-
self._output_type: Type[QuantityT] = output_type # type: ignore
325+
self._create_method = create_method
325326
self._channel: Broadcast[Sample[QuantityT]] = Broadcast(self._name)
326327

327328
async def _run(self) -> None:
328329
await self._builder.subscribe()
329330
steps, metric_fetchers = self._builder.finalize()
330331
evaluator = FormulaEvaluator[QuantityT](
331-
self._name, steps, metric_fetchers, self._output_type
332+
self._name, steps, metric_fetchers, self._create_method
332333
)
333334
sender = self._channel.new_sender()
334335
while True:
@@ -378,7 +379,7 @@ class FormulaEngine3Phase(
378379
def __init__(
379380
self,
380381
name: str,
381-
output_type: Type[QuantityT],
382+
create_method: Callable[[float], QuantityT],
382383
phase_streams: Tuple[
383384
FormulaEngine[QuantityT],
384385
FormulaEngine[QuantityT],
@@ -389,14 +390,14 @@ def __init__(
389390
390391
Args:
391392
name: A name for the formula.
392-
output_type: A type object to generate the output `Sample` with. If the
393-
formula is for generating power values, this would be `Power`, for
394-
example.
393+
create_method: A method to generate the output `Sample` value with. If the
394+
formula is for generating power values, this would be
395+
`Power.from_watts`, for example.
395396
phase_streams: output streams of formula engines running per-phase formulas.
396397
"""
397398
self._higher_order_builder = HigherOrderFormulaBuilder3Phase
398399
self._name: str = name
399-
self._output_type = output_type
400+
self._create_method = create_method
400401
self._channel: Broadcast[Sample3Phase[QuantityT]] = Broadcast(self._name)
401402
self._task: asyncio.Task[None] | None = None
402403
self._streams: tuple[
@@ -474,17 +475,17 @@ class FormulaBuilder(Generic[QuantityT]):
474475
add the values and return the result.
475476
"""
476477

477-
def __init__(self, name: str, output_type: Type[QuantityT]) -> None:
478+
def __init__(self, name: str, create_method: Callable[[float], QuantityT]) -> None:
478479
"""Create a `FormulaBuilder` instance.
479480
480481
Args:
481482
name: A name for the formula being built.
482-
output_type: A type object to generate the output `Sample` with. If the
483-
formula is for generating power values, this would be `Power`, for
484-
example.
483+
create_method: A method to generate the output `Sample` value with. If the
484+
formula is for generating power values, this would be
485+
`Power.from_watts`, for example.
485486
"""
486487
self._name = name
487-
self._output_type: Type[QuantityT] = output_type
488+
self._create_method: Callable[[float], QuantityT] = create_method
488489
self._build_stack: List[FormulaStep] = []
489490
self._steps: List[FormulaStep] = []
490491
self._metric_fetchers: Dict[str, MetricFetcher[QuantityT]] = {}
@@ -648,7 +649,7 @@ def build(self) -> FormulaEngine[QuantityT]:
648649
A `FormulaEngine` instance.
649650
"""
650651
self.finalize()
651-
return FormulaEngine(self, output_type=self._output_type)
652+
return FormulaEngine(self, create_method=self._create_method)
652653

653654

654655
class _BaseHOFormulaBuilder(ABC, Generic[QuantityT]):
@@ -657,16 +658,16 @@ class _BaseHOFormulaBuilder(ABC, Generic[QuantityT]):
657658
def __init__(
658659
self,
659660
engine: FormulaEngine[QuantityT] | FormulaEngine3Phase[QuantityT],
660-
output_type: Type[QuantityT],
661+
create_method: Callable[[float], QuantityT],
661662
) -> None:
662663
"""Create a `GenericHigherOrderFormulaBuilder` instance.
663664
664665
Args:
665666
engine: A first input stream to create a builder with, so that python
666667
operators `+, -, *, /` can be used directly on newly created instances.
667-
output_type: A type object to generate the output `Sample` with. If the
668-
formula is for generating power values, this would be `Power`, for
669-
example.
668+
create_method: A method to generate the output `Sample` value with. If the
669+
formula is for generating power values, this would be
670+
`Power.from_watts`, for example.
670671
"""
671672
self._steps: deque[
672673
tuple[
@@ -675,7 +676,7 @@ def __init__(
675676
]
676677
] = deque()
677678
self._steps.append((TokenType.COMPONENT_METRIC, engine))
678-
self._output_type: Type[QuantityT] = output_type
679+
self._create_method: Callable[[float], QuantityT] = create_method
679680

680681
@overload
681682
def _push(
@@ -854,7 +855,7 @@ def build(
854855
Returns:
855856
A `FormulaEngine` instance.
856857
"""
857-
builder = FormulaBuilder(name, self._output_type)
858+
builder = FormulaBuilder(name, self._create_method)
858859
for typ, value in self._steps:
859860
if typ == TokenType.COMPONENT_METRIC:
860861
assert isinstance(value, FormulaEngine)
@@ -888,9 +889,9 @@ def build(
888889
A `FormulaEngine3Phase` instance.
889890
"""
890891
builders = [
891-
FormulaBuilder(name, self._output_type),
892-
FormulaBuilder(name, self._output_type),
893-
FormulaBuilder(name, self._output_type),
892+
FormulaBuilder(name, self._create_method),
893+
FormulaBuilder(name, self._create_method),
894+
FormulaBuilder(name, self._create_method),
894895
]
895896
for typ, value in self._steps:
896897
if typ == TokenType.COMPONENT_METRIC:
@@ -909,7 +910,7 @@ def build(
909910
builders[phase].push_oper(value)
910911
return FormulaEngine3Phase(
911912
name,
912-
self._output_type,
913+
self._create_method,
913914
(
914915
builders[0].build(),
915916
builders[1].build(),

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_battery_power_formula.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def generate(
4343
in the component graph.
4444
"""
4545
builder = self._get_builder(
46-
"battery-power", ComponentMetricId.ACTIVE_POWER, Power
46+
"battery-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
4747
)
4848
component_ids = self._config.component_ids
4949
if not component_ids:

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_chp_power_formula.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ def generate(self) -> FormulaEngine[Power]:
3838
FormulaGenerationError: If there's no dedicated meter attached to every CHP.
3939
4040
"""
41-
builder = self._get_builder("chp-power", ComponentMetricId.ACTIVE_POWER, Power)
41+
builder = self._get_builder(
42+
"chp-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
43+
)
4244

4345
chp_meter_ids = self._get_chp_meters()
4446
if not chp_meter_ids:

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def generate(self) -> FormulaEngine[Power]:
3535
meter.
3636
"""
3737
builder = self._get_builder(
38-
"consumer-power", ComponentMetricId.ACTIVE_POWER, Power
38+
"consumer-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
3939
)
4040
component_graph = connection_manager.get().component_graph
4141
grid_component = next(

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_ev_charger_current_formula.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,21 @@ def generate(self) -> FormulaEngine3Phase[Current]:
3737
# frequency as the other streams. So we subscribe with a non-existing
3838
# component id, just to get a `None` message at the resampling interval.
3939
builder = self._get_builder(
40-
"ev-current", ComponentMetricId.ACTIVE_POWER, Current
40+
"ev-current", ComponentMetricId.ACTIVE_POWER, Current.from_amperes
4141
)
4242
builder.push_component_metric(
4343
NON_EXISTING_COMPONENT_ID, nones_are_zeros=True
4444
)
4545
engine = builder.build()
4646
return FormulaEngine3Phase(
4747
"ev-current",
48-
Current,
48+
Current.from_amperes,
4949
(engine, engine, engine),
5050
)
5151

5252
return FormulaEngine3Phase(
5353
"ev-current",
54-
Current,
54+
Current.from_amperes,
5555
(
5656
(
5757
self._gen_phase_formula(
@@ -76,7 +76,7 @@ def _gen_phase_formula(
7676
component_ids: abc.Set[int],
7777
metric_id: ComponentMetricId,
7878
) -> FormulaEngine[Current]:
79-
builder = self._get_builder("ev-current", metric_id, Current)
79+
builder = self._get_builder("ev-current", metric_id, Current.from_amperes)
8080

8181
# generate a formula that just adds values from all EV Chargers.
8282
for idx, component_id in enumerate(component_ids):

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_ev_charger_power_formula.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ def generate(self) -> FormulaEngine[Power]:
2222
Returns:
2323
A formula engine that calculates total EV Charger power values.
2424
"""
25-
builder = self._get_builder("ev-power", ComponentMetricId.ACTIVE_POWER, Power)
25+
builder = self._get_builder(
26+
"ev-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts
27+
)
2628

2729
component_ids = self._config.component_ids
2830
if not component_ids:

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_formula_generator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from collections import abc
1111
from dataclasses import dataclass
1212
from enum import Enum
13-
from typing import Generic, Type
13+
from typing import Callable, Generic
1414

1515
from frequenz.channels import Sender
1616

@@ -88,15 +88,15 @@ def _get_builder(
8888
self,
8989
name: str,
9090
component_metric_id: ComponentMetricId,
91-
output_type: Type[QuantityT],
91+
create_method: Callable[[float], QuantityT],
9292
) -> ResampledFormulaBuilder[QuantityT]:
9393
builder = ResampledFormulaBuilder(
9494
self._namespace,
9595
name,
9696
self._channel_registry,
9797
self._resampler_subscription_sender,
9898
component_metric_id,
99-
output_type,
99+
create_method,
100100
)
101101
return builder
102102

src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_grid_current_formula.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def generate(self) -> FormulaEngine3Phase[Current]:
4343

4444
return FormulaEngine3Phase(
4545
"grid-current",
46-
Current,
46+
Current.from_amperes,
4747
(
4848
self._gen_phase_formula(
4949
grid_successors, ComponentMetricId.CURRENT_PHASE_1
@@ -62,7 +62,7 @@ def _gen_phase_formula(
6262
grid_successors: Set[Component],
6363
metric_id: ComponentMetricId,
6464
) -> FormulaEngine[Current]:
65-
builder = self._get_builder("grid-current", metric_id, Current)
65+
builder = self._get_builder("grid-current", metric_id, Current.from_amperes)
6666

6767
# generate a formula that just adds values from all components that are
6868
# directly connected to the grid.

0 commit comments

Comments
 (0)