frequenz-microgrid-component-graph 0.5.0

A library for representing the components of a microgrid and the connections between them as a Directed Acyclic Graph (DAG).
Documentation
// License: MIT
// Copyright © 2024 Frequenz Energy-as-a-Service GmbH

//! This module contains the configuration options for the `ComponentGraph`.

/// Configuration options for the `ComponentGraph`.
#[derive(Clone, Debug)]
pub struct ComponentGraphConfig {
    /// Whether to allow validation errors on components.  When this is `true`,
    /// the graph will be built even if there are validation errors on
    /// components.
    pub(crate) allow_component_validation_failures: bool,

    /// Whether to allow unconnected components in the graph, that are not
    /// reachable from the root.
    pub(crate) allow_unconnected_components: bool,

    /// Whether to allow untyped inverters in the graph.  When this is `true`,
    /// inverters that have `InverterType::Unspecified` will be assumed to be
    /// Battery inverters.
    pub(crate) allow_unspecified_inverters: bool,

    /// Whether to disable fallback components in generated formulas.  When this
    /// is `true`, the formulas will not include fallback components.
    pub(crate) disable_fallback_components: bool,

    /// Meters with successors can still have loads not represented in the
    /// component graph.  These are called phantom loads.
    ///
    /// When this is `true`, phantom loads are included in formulas by excluding
    /// the measurements of successor meters from the measurements of their
    /// predecessor meters.
    ///
    /// When `false`, consumer formula is generated by excluding production
    /// and battery components from the grid measurements.
    pub(crate) include_phantom_loads_in_consumer_formula: bool,

    /// Default policy for the per-category "component" formulas.
    ///
    /// When `true` (the default), the meter measurement is the primary
    /// source and the device measurement is the fallback for the per-
    /// category formulas (`battery_formula`, `chp_formula`, `pv_formula`,
    /// `wind_turbine_formula`, `ev_charger_formula`, `steam_boiler_formula`).
    /// When `false`, the device is primary and the meter is the fallback.
    ///
    /// Per-formula overrides live in [`formula_overrides`][Self::formula_overrides].
    ///
    /// Has no effect on `grid_formula`, `consumer_formula`,
    /// `producer_formula`, or any of the coalesce formulas.
    pub(crate) prefer_meters_in_component_formulas: bool,

    /// Per-formula overrides for the meter/device preference; see
    /// [`FormulaOverrides`].
    pub(crate) formula_overrides: FormulaOverrides,
}

impl Default for ComponentGraphConfig {
    fn default() -> Self {
        Self {
            allow_component_validation_failures: false,
            allow_unconnected_components: false,
            allow_unspecified_inverters: false,
            disable_fallback_components: false,
            include_phantom_loads_in_consumer_formula: false,
            prefer_meters_in_component_formulas: true,
            formula_overrides: FormulaOverrides::default(),
        }
    }
}

impl ComponentGraphConfig {
    /// Effective "prefer meters" setting for [`ComponentGraph::pv_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::pv_formula
    pub(crate) fn prefer_meters_in_pv_formula(&self) -> bool {
        self.formula_overrides
            .prefer_meters_in_pv_formula
            .unwrap_or(self.prefer_meters_in_component_formulas)
    }

    /// Effective "prefer meters" setting for [`ComponentGraph::battery_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::battery_formula
    pub(crate) fn prefer_meters_in_battery_formula(&self) -> bool {
        self.formula_overrides
            .prefer_meters_in_battery_formula
            .unwrap_or(self.prefer_meters_in_component_formulas)
    }

    /// Effective "prefer meters" setting for [`ComponentGraph::chp_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::chp_formula
    pub(crate) fn prefer_meters_in_chp_formula(&self) -> bool {
        self.formula_overrides
            .prefer_meters_in_chp_formula
            .unwrap_or(self.prefer_meters_in_component_formulas)
    }

    /// Effective "prefer meters" setting for [`ComponentGraph::ev_charger_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::ev_charger_formula
    pub(crate) fn prefer_meters_in_ev_charger_formula(&self) -> bool {
        self.formula_overrides
            .prefer_meters_in_ev_charger_formula
            .unwrap_or(self.prefer_meters_in_component_formulas)
    }

    /// Effective "prefer meters" setting for [`ComponentGraph::wind_turbine_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::wind_turbine_formula
    pub(crate) fn prefer_meters_in_wind_turbine_formula(&self) -> bool {
        self.formula_overrides
            .prefer_meters_in_wind_turbine_formula
            .unwrap_or(self.prefer_meters_in_component_formulas)
    }

    /// Effective "prefer meters" setting for [`ComponentGraph::steam_boiler_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::steam_boiler_formula
    pub(crate) fn prefer_meters_in_steam_boiler_formula(&self) -> bool {
        self.formula_overrides
            .prefer_meters_in_steam_boiler_formula
            .unwrap_or(self.prefer_meters_in_component_formulas)
    }

    /// Returns a [`ComponentGraphConfigBuilder`] initialised with all
    /// options set to their default values.
    pub fn builder() -> ComponentGraphConfigBuilder {
        ComponentGraphConfigBuilder::new()
    }
}

/// Builder for [`ComponentGraphConfig`].
///
/// Each method sets the corresponding option and returns `self`, so calls
/// can be chained. Call [`build`][Self::build] to obtain the final
/// `ComponentGraphConfig`.
#[derive(Clone, Debug)]
pub struct ComponentGraphConfigBuilder {
    inner: ComponentGraphConfig,
}

impl ComponentGraphConfigBuilder {
    /// Creates a new builder with all options set to their default values.
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            inner: ComponentGraphConfig::default(),
        }
    }

    /// When `true`, the graph is built even if per-component validation
    /// rules fail; failures are reported as `tracing::warn!` instead of
    /// returning an error.
    pub fn allow_component_validation_failures(mut self, value: bool) -> Self {
        self.inner.allow_component_validation_failures = value;
        self
    }

    /// When `true`, components that are not reachable from the root are
    /// permitted; otherwise the graph fails to build.
    pub fn allow_unconnected_components(mut self, value: bool) -> Self {
        self.inner.allow_unconnected_components = value;
        self
    }

    /// When `true`, inverters with `InverterType::Unspecified` are
    /// treated as battery inverters instead of being rejected.
    pub fn allow_unspecified_inverters(mut self, value: bool) -> Self {
        self.inner.allow_unspecified_inverters = value;
        self
    }

    /// When `true`, generated formulas omit fallback components.
    pub fn disable_fallback_components(mut self, value: bool) -> Self {
        self.inner.disable_fallback_components = value;
        self
    }

    /// Controls how the consumer formula handles meters with successors,
    /// which can carry loads not represented in the graph (phantom loads).
    ///
    /// When `true`, phantom loads are included by subtracting successor
    /// meter measurements from their predecessor meter's measurements.
    /// When `false`, the consumer formula instead excludes production and
    /// battery components from the grid measurements.
    pub fn include_phantom_loads_in_consumer_formula(mut self, value: bool) -> Self {
        self.inner.include_phantom_loads_in_consumer_formula = value;
        self
    }

    /// Sets the global meter-vs-device source preference for the
    /// per-category formulas. See the field-level docs on
    /// [`ComponentGraphConfig`] for the exact list of affected formulas.
    pub fn prefer_meters_in_component_formulas(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_component_formulas = value;
        self
    }

    /// Sets the per-formula overrides for the meter/device preference.
    /// Each override, when `Some(_)`, takes precedence over
    /// [`prefer_meters_in_component_formulas`][Self::prefer_meters_in_component_formulas]
    /// for that formula.
    pub fn formula_overrides(mut self, overrides: FormulaOverrides) -> Self {
        self.inner.formula_overrides = overrides;
        self
    }

    /// Consumes the builder and returns the resulting [`ComponentGraphConfig`].
    pub fn build(self) -> ComponentGraphConfig {
        self.inner
    }
}

/// Per-formula overrides for the meter/device preference in the
/// per-category formulas.
///
/// Each field is `None` by default, meaning the corresponding formula
/// follows the global `prefer_meters_in_component_formulas` setting on
/// [`ComponentGraphConfig`]. Setting an entry to `Some(true)` forces
/// the meter as primary for that formula; `Some(false)` forces the
/// device.
///
/// Construct via [`FormulaOverrides::builder`] or
/// [`FormulaOverrides::default`].
#[derive(Clone, Default, Debug)]
pub struct FormulaOverrides {
    pub(crate) prefer_meters_in_pv_formula: Option<bool>,
    pub(crate) prefer_meters_in_battery_formula: Option<bool>,
    pub(crate) prefer_meters_in_chp_formula: Option<bool>,
    pub(crate) prefer_meters_in_ev_charger_formula: Option<bool>,
    pub(crate) prefer_meters_in_wind_turbine_formula: Option<bool>,
    pub(crate) prefer_meters_in_steam_boiler_formula: Option<bool>,
}

impl FormulaOverrides {
    /// Returns a [`FormulaOverridesBuilder`] with no overrides set.
    pub fn builder() -> FormulaOverridesBuilder {
        FormulaOverridesBuilder::new()
    }
}

/// Builder for [`FormulaOverrides`].
#[derive(Clone, Debug)]
pub struct FormulaOverridesBuilder {
    inner: FormulaOverrides,
}

impl FormulaOverridesBuilder {
    /// Creates a new builder with no overrides set.
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            inner: FormulaOverrides::default(),
        }
    }

    /// Override the meter/device preference for
    /// [`ComponentGraph::pv_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::pv_formula
    pub fn prefer_meters_in_pv_formula(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_pv_formula = Some(value);
        self
    }

    /// Override the meter/device preference for
    /// [`ComponentGraph::battery_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::battery_formula
    pub fn prefer_meters_in_battery_formula(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_battery_formula = Some(value);
        self
    }

    /// Override the meter/device preference for
    /// [`ComponentGraph::chp_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::chp_formula
    pub fn prefer_meters_in_chp_formula(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_chp_formula = Some(value);
        self
    }

    /// Override the meter/device preference for
    /// [`ComponentGraph::ev_charger_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::ev_charger_formula
    pub fn prefer_meters_in_ev_charger_formula(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_ev_charger_formula = Some(value);
        self
    }

    /// Override the meter/device preference for
    /// [`ComponentGraph::wind_turbine_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::wind_turbine_formula
    pub fn prefer_meters_in_wind_turbine_formula(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_wind_turbine_formula = Some(value);
        self
    }

    /// Override the meter/device preference for
    /// [`ComponentGraph::steam_boiler_formula`][cg].
    ///
    /// [cg]: crate::ComponentGraph::steam_boiler_formula
    pub fn prefer_meters_in_steam_boiler_formula(mut self, value: bool) -> Self {
        self.inner.prefer_meters_in_steam_boiler_formula = Some(value);
        self
    }

    /// Consumes the builder and returns the resulting [`FormulaOverrides`].
    pub fn build(self) -> FormulaOverrides {
        self.inner
    }
}