Skip to content

Commit

Permalink
Config parameters for the component graph (#3)
Browse files Browse the repository at this point in the history
This PR adds support for configuring the validation requirements of the
component graph.
  • Loading branch information
shsms authored Jan 14, 2025
2 parents 5e04cee + 9bc80f1 commit 4c4ee64
Show file tree
Hide file tree
Showing 21 changed files with 519 additions and 247 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ path = "src/lib.rs"

[dependencies]
petgraph = "0.6.5"
tracing = "0.1.40"
20 changes: 16 additions & 4 deletions src/component_category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! category of a component.
use crate::graph_traits::Node;
use crate::ComponentGraphConfig;
use std::fmt::Display;

/// Represents the type of an inverter.
Expand Down Expand Up @@ -135,8 +136,14 @@ pub(crate) trait CategoryPredicates: Node {
matches!(self.category(), ComponentCategory::Inverter(_))
}

fn is_battery_inverter(&self) -> bool {
self.category() == ComponentCategory::Inverter(InverterType::Battery)
fn is_battery_inverter(&self, config: &ComponentGraphConfig) -> bool {
match self.category() {
ComponentCategory::Inverter(InverterType::Battery) => true,
ComponentCategory::Inverter(InverterType::Unspecified) => {
config.allow_unspecified_inverters
}
_ => false,
}
}

fn is_pv_inverter(&self) -> bool {
Expand All @@ -147,8 +154,13 @@ pub(crate) trait CategoryPredicates: Node {
self.category() == ComponentCategory::Inverter(InverterType::Hybrid)
}

fn is_unspecified_inverter(&self) -> bool {
self.category() == ComponentCategory::Inverter(InverterType::Unspecified)
fn is_unspecified_inverter(&self, config: &ComponentGraphConfig) -> bool {
match self.category() {
ComponentCategory::Inverter(InverterType::Unspecified) => {
!config.allow_unspecified_inverters
}
_ => false,
}
}

fn is_ev_charger(&self) -> bool {
Expand Down
26 changes: 26 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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, Default, 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 allow_component_validation_failures: bool,

/// Whether to allow unconnected components in the graph, that are not
/// reachable from the root.
pub 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 allow_unspecified_inverters: bool,

/// Whether to disable fallback components in generated formulas. When this
/// is `true`, the formulas will not include fallback components.
pub disable_fallback_components: bool,
}
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ macro_rules! ErrorKind {
($kind:ident, $ctor:ident)
),*) => {
/// The kind of error that occurred.
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ErrorKind {
$(
$kind,
Expand Down Expand Up @@ -57,7 +57,7 @@ ErrorKind!(

/// An error that can occur during the creation or traversal of a
/// [ComponentGraph][crate::ComponentGraph].
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Error {
kind: ErrorKind,
desc: String,
Expand Down
3 changes: 2 additions & 1 deletion src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod validation;
mod formulas;
pub mod iterators;

use crate::{Edge, Node};
use crate::{ComponentGraphConfig, Edge, Node};
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::HashMap;

Expand Down Expand Up @@ -40,6 +40,7 @@ where
node_indices: NodeIndexMap,
root_id: u64,
edges: EdgeMap<E>,
config: ComponentGraphConfig,
}

#[cfg(test)]
Expand Down
178 changes: 88 additions & 90 deletions src/graph/creation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use petgraph::graph::DiGraph;

use crate::{component_category::CategoryPredicates, Edge, Error, Node};
use crate::{component_category::CategoryPredicates, ComponentGraphConfig, Edge, Error, Node};

use super::{ComponentGraph, EdgeMap, NodeIndexMap};

Expand All @@ -22,15 +22,17 @@ where
pub fn try_new<NodeIterator: IntoIterator<Item = N>, EdgeIterator: IntoIterator<Item = E>>(
components: NodeIterator,
connections: EdgeIterator,
config: ComponentGraphConfig,
) -> Result<Self, Error> {
let (graph, indices) = Self::create_graph(components)?;
let (graph, indices) = Self::create_graph(components, &config)?;
let root_id = Self::find_root(&graph)?.component_id();

let mut cg = Self {
graph,
node_indices: indices,
root_id,
edges: EdgeMap::new(),
config,
};
cg.add_connections(connections)?;

Expand All @@ -56,6 +58,7 @@ where

fn create_graph(
components: impl IntoIterator<Item = N>,
config: &ComponentGraphConfig,
) -> Result<(DiGraph<N, ()>, NodeIndexMap), Error> {
let mut graph = DiGraph::new();
let mut indices = NodeIndexMap::new();
Expand All @@ -68,7 +71,7 @@ where
"ComponentCategory not specified for component: {cid}"
)));
}
if component.is_unspecified_inverter() {
if component.is_unspecified_inverter(config) {
return Err(Error::invalid_component(format!(
"InverterType not specified for inverter: {cid}"
)));
Expand Down Expand Up @@ -117,104 +120,99 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::component_category::BatteryType;
use crate::graph::test_utils::{TestComponent, TestConnection};
use crate::graph::test_utils::{ComponentGraphBuilder, ComponentHandle};
use crate::ComponentCategory;
use crate::InverterType;

fn nodes_and_edges() -> (Vec<TestComponent>, Vec<TestConnection>) {
let components = vec![
TestComponent::new(6, ComponentCategory::Meter),
TestComponent::new(7, ComponentCategory::Inverter(InverterType::Battery)),
TestComponent::new(3, ComponentCategory::Meter),
TestComponent::new(5, ComponentCategory::Battery(BatteryType::LiIon)),
TestComponent::new(8, ComponentCategory::Battery(BatteryType::Unspecified)),
TestComponent::new(4, ComponentCategory::Inverter(InverterType::Battery)),
TestComponent::new(2, ComponentCategory::Meter),
];
let connections = vec![
TestConnection::new(3, 4),
TestConnection::new(7, 8),
TestConnection::new(4, 5),
TestConnection::new(2, 3),
TestConnection::new(6, 7),
TestConnection::new(2, 6),
];

(components, connections)
fn nodes_and_edges() -> (ComponentGraphBuilder, ComponentHandle) {
let mut builder = ComponentGraphBuilder::new();

let grid_meter = builder.meter();
let meter_bat_chain = builder.meter_bat_chain(1, 1);
builder.connect(grid_meter, meter_bat_chain);

let meter_bat_chain = builder.meter_bat_chain(1, 1);
builder.connect(grid_meter, meter_bat_chain);

(builder, grid_meter)
}

#[test]
fn test_component_validation() {
let (mut components, mut connections) = nodes_and_edges();

assert!(
ComponentGraph::try_new(components.clone(), connections.clone())
.is_err_and(|e| e == Error::invalid_graph("No grid component found.")),
);

components.push(TestComponent::new(1, ComponentCategory::Grid));
connections.push(TestConnection::new(1, 2));
assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok());

components.push(TestComponent::new(2, ComponentCategory::Meter));
assert!(
ComponentGraph::try_new(components.clone(), connections.clone())
.is_err_and(|e| e == Error::invalid_graph("Duplicate component ID found: 2"))
);

components.pop();
components.push(TestComponent::new(9, ComponentCategory::Unspecified));
assert!(
ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| e
== Error::invalid_component("ComponentCategory not specified for component: 9"))
);

components.pop();
components.push(TestComponent::new(
9,
ComponentCategory::Inverter(InverterType::Unspecified),
let (mut builder, grid_meter) = nodes_and_edges();

assert!(builder
.build(None)
.is_err_and(|e| e == Error::invalid_graph("No grid component found.")),);

let grid = builder.grid();
builder.connect(grid, grid_meter);

assert!(builder.build(None).is_ok());

builder.add_component_with_id(2, ComponentCategory::Meter);
assert!(builder
.build(None)
.is_err_and(|e| e == Error::invalid_graph("Duplicate component ID found: 2")));

builder.pop_component();
builder.add_component(ComponentCategory::Unspecified);
assert!(builder
.build(None)
.is_err_and(|e| e
== Error::invalid_component("ComponentCategory not specified for component: 8")));

builder.pop_component();
let unspec_inv =
builder.add_component(ComponentCategory::Inverter(InverterType::Unspecified));
builder.connect(grid_meter, unspec_inv);

// With default config, unspecified inverter types are not accepted.
assert!(builder.build(None).is_err_and(
|e| e == Error::invalid_component("InverterType not specified for inverter: 9")
));
assert!(
ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(
|e| e == Error::invalid_component("InverterType not specified for inverter: 9")
)
);

components.pop();
components.push(TestComponent::new(9, ComponentCategory::Grid));
assert!(
ComponentGraph::try_new(components.clone(), connections.clone())
.is_err_and(|e| e == Error::invalid_graph("Multiple grid components found."))
);

components.pop();
assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok());
// With `allow_unspecified_inverters=true`, unspecified inverter types
// are treated as battery inverters.
let unspec_inv_config = ComponentGraphConfig {
allow_unspecified_inverters: true,
..Default::default()
};

assert!(builder.build(Some(unspec_inv_config.clone())).is_ok());

assert!(builder
.pop_component()
.unwrap()
.is_battery_inverter(&unspec_inv_config));
builder.pop_connection();
builder.add_component(ComponentCategory::Grid);
assert!(builder
.build(None)
.is_err_and(|e| e == Error::invalid_graph("Multiple grid components found.")));

builder.pop_component();
assert!(builder.build(None).is_ok());
}

#[test]
fn test_connection_validation() {
let (mut components, mut connections) = nodes_and_edges();

components.push(TestComponent::new(1, ComponentCategory::Grid));
connections.push(TestConnection::new(1, 2));

connections.push(TestConnection::new(2, 2));
assert!(
ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| e
== Error::invalid_connection(
"Connection:(2, 2) Can't connect a component to itself."
))
);

connections.pop();
connections.push(TestConnection::new(2, 9));
assert!(
ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| e
== Error::invalid_connection("Connection:(2, 9) Can't find a component with ID 9"))
);

connections.pop();
assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok());
let (mut builder, grid_meter) = nodes_and_edges();

let grid = builder.grid();
builder.connect(grid, grid_meter);

builder.connect(grid, grid);
assert!(builder.build(None).is_err_and(|e| e
== Error::invalid_connection(
"Connection:(7, 7) Can't connect a component to itself."
)));
builder.pop_connection();

builder.connect(grid_meter, ComponentHandle::new(9));
assert!(builder.build(None).is_err_and(|e| e
== Error::invalid_connection("Connection:(0, 9) Can't find a component with ID 9")));

builder.pop_connection();
assert!(builder.build(None).is_ok());
}
}
Loading

0 comments on commit 4c4ee64

Please sign in to comment.