Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
856981e
Update resampler time at the start of resampling
shsms Aug 25, 2025
c4d4abb
Rename timer field to resampler_timer for clarity
shsms Aug 25, 2025
bb82ad5
Refactor resampling logic into a dedicated function
shsms Aug 25, 2025
196e222
Add `kind` method to `Error` for retrieving error kind
shsms Aug 25, 2025
e260df0
Refactor resampler cleanup
shsms Aug 25, 2025
fa24e91
Rename `do_next` to `evaluate_formulas`
shsms Aug 25, 2025
7b666b6
Add quantity module with support for various physical quantities
shsms Aug 25, 2025
98613ed
Add tests for the quantity types
shsms Sep 30, 2025
ab13386
Make `Sample` struct generic over value type
shsms Aug 25, 2025
aa2d311
Add QuantityType to Metric trait as an associated type
shsms Aug 25, 2025
112c200
Update Formula trait to support generic Quantity type
shsms Aug 25, 2025
0b61acb
Update LogicalMeterFormula to support generic Quantity type
shsms Aug 25, 2025
7201570
Update `evaluate_formulas` to support generic transformation
shsms Aug 25, 2025
fd57f5e
Add TypedFormulaResponseSender enum and TryFrom implementation
shsms Aug 25, 2025
9ce335f
Add `Formulas` struct to manage logical meter formulas
shsms Aug 25, 2025
cb25391
Change log level to debug for missing metric data in LogicalMeterActor
shsms Aug 25, 2025
082ee30
Refactor resampler initialization into a dedicated method
shsms Aug 25, 2025
af95e6e
Implement `Quantity` types in logical meter streams
shsms Aug 25, 2025
254f0bf
Remove invalid metric checks from formula operations
shsms Aug 25, 2025
a181981
Remove default generic type for `Formula` and `Sample`
shsms Aug 26, 2025
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
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub use client::MicrogridClientHandle;
mod error;
pub use error::{Error, ErrorKind};

mod quantity;

mod proto;

mod sample;
Expand Down
215 changes: 215 additions & 0 deletions src/quantity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines various physical quantities and their operations.

/// A trait for physical quantities that supports basic arithmetic operations.
pub trait Quantity:
std::ops::Add<Output = Self>
+ std::ops::Sub<Output = Self>
+ std::ops::Mul<Percentage, Output = Self>
+ std::ops::Mul<f32, Output = Self>
+ std::ops::Div<f32, Output = Self>
+ std::ops::Div<Self, Output = f32>
+ std::cmp::PartialOrd
+ std::fmt::Display
+ Copy
+ Clone
+ std::fmt::Debug
+ Default
+ Sized
+ Send
+ Sync
{
fn zero() -> Self {
Self::default()
}
Comment thread
shsms marked this conversation as resolved.
}

impl std::ops::Mul<Percentage> for f32 {
type Output = f32;

fn mul(self, other: Percentage) -> Self::Output {
self * other.as_fraction()
}
}

impl Quantity for f32 {}

/// Formats an f32 with a given precision and removes trailing zeros
fn format_float(value: f32, precision: usize) -> String {
let mut s = format!("{:.1$}", value, precision);
if s.contains('.') {
s = s.trim_end_matches('0').to_string();
}
if s.ends_with('.') {
s.pop();
}
s
}

macro_rules! qty_format {
(@impl $self:ident, $f:ident, $prec:ident,
($ctor:ident, $getter:ident, $unit:literal, $exp:literal),
) => {
write!($f, "{} {}", format_float( $self.$getter(), $prec), $unit)
};

(@impl $self:ident, $f:ident, $prec:ident,
($ctor1:ident, $getter1:ident, $unit1:literal, $exp1:literal),
($ctor2:ident, $getter2:ident, $unit2:literal, $exp2:literal), $($rest:tt)*
) => {{
const {assert!($exp1 < $exp2, "Units must be in increasing order of magnitude.")};

if $exp1 <= $self.value.abs() && $self.value.abs() < $exp2 {
write!($f, "{} {}", format_float( $self.$getter1(), $prec), $unit1)
} else {
qty_format!(@impl $self, $f, $prec, ($ctor2, $getter2, $unit2, $exp2), $($rest)*)
}}
};

(@impl $self:ident, $f:ident, $prec:ident,
($ctor1:ident, $getter1:ident, $unit1:literal, $exp1:literal),
($ctor2:ident, $getter2:ident, None, $exp2:literal),
) => {
write!($f, "{} {}", format_float( $self.$getter1(), $prec), $unit1)
};

(@start $self:ident, $f:ident, $prec:ident,
($ctor:ident, $getter:ident, $unit:literal, $exp:literal), $($rest:tt)*
) => {
if $self.value.abs() <= $exp {
write!($f, "{} {}", format_float( $self.$getter(), $prec), $unit)
} else {
qty_format!(@impl $self, $f, $prec, ($ctor, $getter, $unit, $exp), $($rest)*)
}
};

($typename:ident => {$($rest:tt)*}) => {
use super::format_float;
impl std::fmt::Display for $typename {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let prec = if let Some(prec) = f.precision() {
prec
} else {
3
};
qty_format!(@start self, f, prec, $($rest)*)
}

}
};
}

macro_rules! qty_ctor {
(@impl ($ctor:ident, $getter:ident, $unit:tt, $exp:literal) $(,)?) => {
pub fn $ctor(value: f32) -> Self {
Self { value: value * $exp }
}
pub fn $getter(&self) -> f32 {
self.value / $exp
}
};
(@impl ($ctor:ident, $getter:ident, $unit:tt, $exp:literal), $($rest:tt)*) => {
qty_ctor!(@impl ($ctor, $getter, $unit, $exp));
qty_ctor!(@impl $($rest)*);
};
(@impl_arith_ops $typename:ident) => {
impl std::ops::Add for $typename {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
Self {
value: self.value + rhs.value,
}
}
}

impl std::ops::Sub for $typename {
type Output = Self;

fn sub(self, rhs: Self) -> Self::Output {
Self {
value: self.value - rhs.value,
}
}
}

impl std::ops::Mul<super::Percentage> for $typename {
type Output = Self;

fn mul(self, other: super::Percentage) -> Self::Output {
Self {
value: self.value * other.as_fraction(),
}
}
}

impl std::ops::Mul<f32> for $typename {
type Output = Self;

fn mul(self, other: f32) -> Self::Output {
Self {
value: self.value * other,
}
}
}

impl std::ops::Div<f32> for $typename {
type Output = Self;

fn div(self, other: f32) -> Self::Output {
Self {
value: self.value / other,
}
}
}

impl std::ops::Div<$typename> for $typename {
type Output = f32;

fn div(self, other: Self) -> Self::Output {
self.value / other.value
}
}

impl std::cmp::PartialOrd for $typename {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.value.partial_cmp(&other.value)
}
}

};
(#[$meta:meta] $typename:ident => {$($rest:tt)*}) => {
#[$meta]
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct $typename {
value: f32,
}

impl $typename {
qty_ctor!(@impl $($rest)*);
}

qty_ctor!{@impl_arith_ops $typename}
qty_format!{$typename => {$($rest)*}}

impl super::Quantity for $typename {}
};
}

mod current;
mod energy;
mod frequency;
mod percentage;
mod power;
mod reactive_power;
mod voltage;

pub use current::Current;
pub use energy::Energy;
pub use frequency::Frequency;
pub use percentage::Percentage;
pub use power::Power;
pub use reactive_power::ReactivePower;
pub use voltage::Voltage;
22 changes: 22 additions & 0 deletions src/quantity/current.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines the `Current` quantity and its operations.

use super::{Power, Voltage};

qty_ctor! {
#[doc = "A physical quantity representing electric current."]
Current => {
(from_milliamperes, as_milliamperes, "mA", 1e-3),
(from_amperes, as_amperes, "A", 1e0),
}
}

impl std::ops::Mul<Voltage> for Current {
type Output = Power;

fn mul(self, voltage: Voltage) -> Self::Output {
Power::from_watts(self.as_amperes() * voltage.as_volts())
}
}
34 changes: 34 additions & 0 deletions src/quantity/energy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines the `Energy` quantity and its operations.

use super::Power;

qty_ctor! {
#[doc = "A physical quantity representing energy."]
Energy => {
(from_milliwatthours, as_milliwatthours, "mWh", 1e-3),
(from_watthours, as_watthours, "Wh", 1e0),
(from_kilowatthours, as_kilowatthours, "kWh", 1e3),
(from_megawatthours, as_megawatthours, "MWh", 1e6),
(from_gigawatthours, as_gigawatthours, "GWh", 1e9),
}
}

impl std::ops::Div<Power> for Energy {
type Output = std::time::Duration;

fn div(self, power: Power) -> Self::Output {
let seconds = (self.as_watthours() / power.as_watts()) * 3600.0;
std::time::Duration::from_secs_f32(seconds)
}
}

impl std::ops::Div<std::time::Duration> for Energy {
type Output = Power;

fn div(self, duration: std::time::Duration) -> Self::Output {
Power::from_watts(self.as_watthours() / duration.as_secs_f32() / 3600.0)
}
}
14 changes: 14 additions & 0 deletions src/quantity/frequency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines the `Frequency` quantity and its operations.

qty_ctor! {
#[doc = "A physical quantity representing frequency."]
Frequency => {
(from_hertz, as_hertz, "Hz", 1.0),
(from_kilohertz, as_kilohertz, "kHz", 1e3),
(from_megahertz, as_megahertz, "MHz", 1e6),
(from_gigahertz, as_gigahertz, "GHz", 1e9),
}
}
12 changes: 12 additions & 0 deletions src/quantity/percentage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines the `Percentage` quantity and its operations.

qty_ctor! {
#[doc = "A quantity representing a percentage (0% to 100%)."]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Typically 0% to 100%"? It can also be 200% and -10% technically.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in #15

Percentage => {
(from_percentage, as_percentage, "%", 1.0),
(from_fraction, as_fraction, None, 100.0),
}
}
41 changes: 41 additions & 0 deletions src/quantity/power.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines the `Power` quantity and its operations.

use super::{Current, Energy, Voltage};

qty_ctor! {
#[doc = "A physical quantity representing active power."]
Power => {
(from_milliwatts, as_milliwatts, "mW", 1e-3),
(from_watts, as_watts, "W", 1e0),
(from_kilowatts, as_kilowatts, "kW", 1e3),
(from_megawatts, as_megawatts, "MW", 1e6),
(from_gigawatts, as_gigawatts, "GW", 1e9),
}
}

impl std::ops::Div<Voltage> for Power {
type Output = Current;

fn div(self, voltage: Voltage) -> Self::Output {
Current::from_amperes(self.as_watts() / voltage.as_volts())
}
}

impl std::ops::Div<Current> for Power {
type Output = Voltage;

fn div(self, current: Current) -> Self::Output {
Voltage::from_volts(self.as_watts() / current.as_amperes())
}
}

impl std::ops::Mul<std::time::Duration> for Power {
type Output = Energy;

fn mul(self, duration: std::time::Duration) -> Self::Output {
Energy::from_watthours(self.as_watts() * duration.as_secs_f32() / 3600.0)
}
}
33 changes: 33 additions & 0 deletions src/quantity/reactive_power.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// License: MIT
// Copyright © 2025 Frequenz Energy-as-a-Service GmbH

//! This module defines the `ReactivePower` quantity and its operations.

use super::{Current, Voltage};

qty_ctor! {
#[doc = "A physical quantity representing reactive power."]
ReactivePower => {
(from_millivolt_amperes_reactive, as_millivolt_amperes_reactive, "mVAR", 1e-3),
(from_volt_amperes_reactive, as_volt_amperes_reactive, "VAR", 1e0),
(from_kilovolt_amperes_reactive, as_kilovolt_amperes_reactive, "kVAR", 1e3),
(from_megavolt_amperes_reactive, as_megavolt_amperes_reactive, "MVAR", 1e6),
(from_gigavolt_amperes_reactive, as_gigavolt_amperes_reactive, "GVAR", 1e9),
}
}

impl std::ops::Div<Voltage> for ReactivePower {
type Output = Current;

fn div(self, voltage: Voltage) -> Self::Output {
Current::from_amperes(self.as_volt_amperes_reactive() / voltage.as_volts())
}
}

impl std::ops::Div<Current> for ReactivePower {
type Output = Voltage;

fn div(self, current: Current) -> Self::Output {
Voltage::from_volts(self.as_volt_amperes_reactive() / current.as_amperes())
}
}
Loading