From 57e3574f11a1ddd67f49faf532635b27cdec5968 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 22:10:10 +0200 Subject: [PATCH 1/8] Add `ComponentGraphConfig` type A `ComponentGraphConfig` object now needs to be passed when constructing a `ComponentGraph` instance. Signed-off-by: Sahas Subramanian --- src/config.rs | 8 ++ src/graph.rs | 3 +- src/graph/creation.rs | 60 +++++++---- src/graph/meter_roles.rs | 5 +- src/graph/retrieval.rs | 19 +++- src/graph/test_utils.rs | 9 +- src/graph/validation/validate_graph.rs | 62 ++++++++---- src/graph/validation/validate_neighbors.rs | 112 +++++++++++++-------- src/lib.rs | 3 + 9 files changed, 190 insertions(+), 91 deletions(-) create mode 100644 src/config.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a99e29d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,8 @@ +// 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 {} diff --git a/src/graph.rs b/src/graph.rs index d77b590..1147e97 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -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; @@ -40,6 +40,7 @@ where node_indices: NodeIndexMap, root_id: u64, edges: EdgeMap, + config: ComponentGraphConfig, } #[cfg(test)] diff --git a/src/graph/creation.rs b/src/graph/creation.rs index ab79232..d3b4115 100644 --- a/src/graph/creation.rs +++ b/src/graph/creation.rs @@ -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}; @@ -22,8 +22,9 @@ where pub fn try_new, EdgeIterator: IntoIterator>( components: NodeIterator, connections: EdgeIterator, + config: ComponentGraphConfig, ) -> Result { - 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 { @@ -31,6 +32,7 @@ where node_indices: indices, root_id, edges: EdgeMap::new(), + config, }; cg.add_connections(connections)?; @@ -56,6 +58,7 @@ where fn create_graph( components: impl IntoIterator, + config: &ComponentGraphConfig, ) -> Result<(DiGraph, NodeIndexMap), Error> { let mut graph = DiGraph::new(); let mut indices = NodeIndexMap::new(); @@ -146,28 +149,35 @@ mod tests { #[test] fn test_component_validation() { + let config = ComponentGraphConfig::default(); let (mut components, mut connections) = nodes_and_edges(); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.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()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); components.push(TestComponent::new(2, ComponentCategory::Meter)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.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")) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| e + == Error::invalid_component( + "ComponentCategory not specified for component: 9" + )) ); components.pop(); @@ -176,24 +186,29 @@ mod tests { ComponentCategory::Inverter(InverterType::Unspecified), )); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and( - |e| e == Error::invalid_component("InverterType not specified for inverter: 9") - ) + ComponentGraph::try_new(components.clone(), connections.clone(), config.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()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.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()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); } #[test] fn test_connection_validation() { + let config = ComponentGraphConfig::default(); let (mut components, mut connections) = nodes_and_edges(); components.push(TestComponent::new(1, ComponentCategory::Grid)); @@ -201,20 +216,27 @@ mod tests { 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." - )) + ComponentGraph::try_new(components.clone(), connections.clone(), config.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")) + ComponentGraph::try_new(components.clone(), connections.clone(), config.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()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); } } diff --git a/src/graph/meter_roles.rs b/src/graph/meter_roles.rs index 046797f..ab8f36f 100644 --- a/src/graph/meter_roles.rs +++ b/src/graph/meter_roles.rs @@ -94,6 +94,7 @@ mod tests { use crate::error::Error; use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; + use crate::ComponentGraphConfig; use crate::InverterType; fn nodes_and_edges() -> (Vec, Vec) { @@ -205,7 +206,9 @@ mod tests { connections: Vec, filter: impl Fn(&ComponentGraph, u64) -> Result, ) -> Result, Error> { - let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + let config = ComponentGraphConfig::default(); + + let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?; let mut found_meters = vec![]; for comp in graph.components() { diff --git a/src/graph/retrieval.rs b/src/graph/retrieval.rs index d7d8d3a..2ed8eae 100644 --- a/src/graph/retrieval.rs +++ b/src/graph/retrieval.rs @@ -151,6 +151,7 @@ mod tests { use crate::graph::test_utils::ComponentGraphBuilder; use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; + use crate::ComponentGraphConfig; use crate::InverterType; fn nodes_and_edges() -> (Vec, Vec) { @@ -179,8 +180,9 @@ mod tests { #[test] fn test_component() -> Result<(), Error> { + let config = ComponentGraphConfig::default(); let (components, connections) = nodes_and_edges(); - let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?; assert_eq!( graph.component(1), @@ -203,8 +205,9 @@ mod tests { #[test] fn test_components() -> Result<(), Error> { + let config = ComponentGraphConfig::default(); let (components, connections) = nodes_and_edges(); - let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?; assert!(graph.components().eq(&components)); assert!(graph.components().filter(|x| x.is_battery()).eq(&[ @@ -217,8 +220,9 @@ mod tests { #[test] fn test_connections() -> Result<(), Error> { + let config = ComponentGraphConfig::default(); let (components, connections) = nodes_and_edges(); - let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?; assert!(graph.connections().eq(&connections)); @@ -232,8 +236,9 @@ mod tests { #[test] fn test_neighbors() -> Result<(), Error> { + let config = ComponentGraphConfig::default(); let (components, connections) = nodes_and_edges(); - let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + let graph = ComponentGraph::try_new(components.clone(), connections.clone(), config)?; assert!(graph.predecessors(1).is_ok_and(|x| x.eq(&[]))); @@ -350,7 +355,11 @@ mod tests { #[test] fn test_find_all() -> Result<(), Error> { let (components, connections) = nodes_and_edges(); - let graph = ComponentGraph::try_new(components.clone(), connections.clone())?; + let graph = ComponentGraph::try_new( + components.clone(), + connections.clone(), + ComponentGraphConfig::default(), + )?; let found = graph.find_all( graph.root_id, diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs index 24f0d77..503d394 100644 --- a/src/graph/test_utils.rs +++ b/src/graph/test_utils.rs @@ -10,7 +10,8 @@ //! graph configurations for use in tests. use crate::{ - BatteryType, ComponentCategory, ComponentGraph, Edge, Error, EvChargerType, InverterType, Node, + BatteryType, ComponentCategory, ComponentGraph, ComponentGraphConfig, Edge, Error, + EvChargerType, InverterType, Node, }; #[derive(Clone, Debug, PartialEq)] @@ -205,6 +206,10 @@ impl ComponentGraphBuilder { /// Builds and returns the component graph from the components and /// connections added to the builder. pub(super) fn build(&self) -> Result, Error> { - ComponentGraph::try_new(self.components.clone(), self.connections.clone()) + ComponentGraph::try_new( + self.components.clone(), + self.connections.clone(), + ComponentGraphConfig::default(), + ) } } diff --git a/src/graph/validation/validate_graph.rs b/src/graph/validation/validate_graph.rs index eeba470..86ab1a6 100644 --- a/src/graph/validation/validate_graph.rs +++ b/src/graph/validation/validate_graph.rs @@ -86,6 +86,7 @@ mod tests { use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::ComponentGraph; + use crate::ComponentGraphConfig; use crate::InverterType; fn nodes_and_edges() -> (Vec, Vec) { @@ -118,17 +119,24 @@ mod tests { #[test] fn test_connected_graph_validation() { + let config = ComponentGraphConfig::default(); let (mut components, mut connections) = nodes_and_edges(); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); components.push(TestComponent::new(11, ComponentCategory::Meter)); - let Err(err) = ComponentGraph::try_new(components.clone(), connections.clone()) else { + let Err(err) = + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + else { panic!() }; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and( - |e| e == Error::invalid_graph("Nodes [11] are not connected to the root.") - ), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and( + |e| e == Error::invalid_graph("Nodes [11] are not connected to the root.") + ), "{:?}", err ); @@ -136,82 +144,94 @@ mod tests { components.push(TestComponent::new(12, ComponentCategory::Meter)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and( - |e| e == Error::invalid_graph("Nodes [11, 12] are not connected to the root.") - ) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and( + |e| e == Error::invalid_graph("Nodes [11, 12] are not connected to the root.") + ) ); connections.push(TestConnection::new(11, 12)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and( - |e| e == Error::invalid_graph("Nodes [11, 12] are not connected to the root.") - ) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and( + |e| e == Error::invalid_graph("Nodes [11, 12] are not connected to the root.") + ) ); connections.pop(); components.pop(); components.pop(); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); } #[test] fn test_acyclicity_validation() { + let config = ComponentGraphConfig::default(); let (components, mut connections) = nodes_and_edges(); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); // add cycles at different levels connections.push(TestConnection::new(3, 2)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 2 -> 3 -> 2")), ); connections.pop(); connections.push(TestConnection::new(4, 2)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 2 -> 3 -> 4 -> 2")) ); connections.pop(); connections.push(TestConnection::new(5, 2)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 2 -> 3 -> 4 -> 5 -> 2")) ); connections.pop(); connections.push(TestConnection::new(4, 3)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 3 -> 4 -> 3")) ); connections.pop(); connections.push(TestConnection::new(5, 3)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 3 -> 4 -> 5 -> 3")) ); connections.pop(); connections.push(TestConnection::new(5, 4)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 4 -> 5 -> 4")) ); connections.pop(); connections.push(TestConnection::new(9, 2)); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()) + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| e == Error::invalid_graph("Cycle detected: 2 -> 9 -> 2")) ); connections.pop(); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); } } diff --git a/src/graph/validation/validate_neighbors.rs b/src/graph/validation/validate_neighbors.rs index 7d5af90..4ac3542 100644 --- a/src/graph/validation/validate_neighbors.rs +++ b/src/graph/validation/validate_neighbors.rs @@ -144,21 +144,23 @@ mod tests { use crate::graph::test_utils::{TestComponent, TestConnection}; use crate::ComponentCategory; use crate::ComponentGraph; + use crate::ComponentGraphConfig; use crate::InverterType; #[test] fn test_validate_root() { + let config = ComponentGraphConfig::default(); let components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), ]; let connections = vec![TestConnection::new(1, 2)]; - assert!(ComponentGraph::try_new(components, connections).is_ok()); + assert!(ComponentGraph::try_new(components, connections, config.clone()).is_ok()); let components = vec![TestComponent::new(1, ComponentCategory::Grid)]; let connections: Vec = vec![]; assert!( - ComponentGraph::try_new(components, connections).is_err_and(|e| { + ComponentGraph::try_new(components, connections, config.clone()).is_err_and(|e| { e == Error::invalid_graph("Grid:1 must have at least one successor.") }), ); @@ -175,7 +177,7 @@ mod tests { ]; assert!( - ComponentGraph::try_new(components, connections).is_err_and(|e| { + ComponentGraph::try_new(components, connections, config.clone()).is_err_and(|e| { e == Error::invalid_graph( "Grid:1 can't have successors with multiple predecessors. Found Meter:3.", ) @@ -185,6 +187,7 @@ mod tests { #[test] fn test_validate_meter() { + let config = ComponentGraphConfig::default(); let components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -192,7 +195,7 @@ mod tests { ]; let connections = vec![TestConnection::new(1, 2), TestConnection::new(2, 3)]; assert!( - ComponentGraph::try_new(components, connections).is_err_and(|e| { + ComponentGraph::try_new(components, connections, config.clone()).is_err_and(|e| { e == Error::invalid_graph(concat!( "Meter:2 can only have successors that are not Batteries. ", "Found Battery(LiIon):3." @@ -203,6 +206,7 @@ mod tests { #[test] fn test_validate_battery_inverter() { + let config = ComponentGraphConfig::default(); let mut components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -214,11 +218,13 @@ mod tests { TestConnection::new(2, 3), TestConnection::new(3, 4), ]; - let Err(err) = ComponentGraph::try_new(components.clone(), connections.clone()) else { + let Err(err) = + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + else { panic!() }; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()).is_err_and(|e| { e == Error::invalid_graph( "BatteryInverter:3 can only have successors that are Batteries. Found Electrolyzer:4.", ) @@ -231,9 +237,10 @@ mod tests { connections.pop(); assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { - e == Error::invalid_graph("BatteryInverter:3 must have at least one successor.") - }), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| { + e == Error::invalid_graph("BatteryInverter:3 must have at least one successor.") + }), ); components.push(TestComponent::new( @@ -242,11 +249,12 @@ mod tests { )); connections.push(TestConnection::new(3, 4)); - assert!(ComponentGraph::try_new(components, connections).is_ok()); + assert!(ComponentGraph::try_new(components, connections, config.clone()).is_ok()); } #[test] fn test_validate_pv_inverter() { + let config = ComponentGraphConfig::default(); let mut components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -259,21 +267,23 @@ mod tests { TestConnection::new(3, 4), ]; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { - e == Error::invalid_graph( - "SolarInverter:3 can't have any successors. Found Electrolyzer:4.", - ) - }), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| { + e == Error::invalid_graph( + "SolarInverter:3 can't have any successors. Found Electrolyzer:4.", + ) + }), ); components.pop(); connections.pop(); - assert!(ComponentGraph::try_new(components, connections).is_ok()); + assert!(ComponentGraph::try_new(components, connections, config.clone()).is_ok()); } #[test] fn test_validate_hybrid_inverter() { + let config = ComponentGraphConfig::default(); let mut components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -286,18 +296,22 @@ mod tests { TestConnection::new(3, 4), ]; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { - e == Error::invalid_graph(concat!( - "HybridInverter:3 can only have successors that are Batteries. ", - "Found Electrolyzer:4." - )) - }), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| { + e == Error::invalid_graph(concat!( + "HybridInverter:3 can only have successors that are Batteries. ", + "Found Electrolyzer:4." + )) + }), ); components.pop(); connections.pop(); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); components.push(TestComponent::new( 4, @@ -305,11 +319,12 @@ mod tests { )); connections.push(TestConnection::new(3, 4)); - assert!(ComponentGraph::try_new(components, connections).is_ok()); + assert!(ComponentGraph::try_new(components, connections, config.clone()).is_ok()); } #[test] fn test_validate_batteries() { + let config = ComponentGraphConfig::default(); let mut components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -324,17 +339,21 @@ mod tests { TestConnection::new(4, 5), ]; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { - e == Error::invalid_graph( - "Battery(NaIon):4 can't have any successors. Found Battery(LiIon):5.", - ) - }), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| { + e == Error::invalid_graph( + "Battery(NaIon):4 can't have any successors. Found Battery(LiIon):5.", + ) + }), ); components.pop(); connections.pop(); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); components.pop(); components.pop(); @@ -348,7 +367,10 @@ mod tests { ComponentCategory::Battery(BatteryType::LiIon), )); - assert!(ComponentGraph::try_new(components.clone(), connections.clone()).is_ok()); + assert!( + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_ok() + ); let components = vec![ TestComponent::new(1, ComponentCategory::Grid), @@ -357,7 +379,7 @@ mod tests { let connections = vec![TestConnection::new(1, 2)]; assert!( - ComponentGraph::try_new(components, connections).is_err_and(|e| { + ComponentGraph::try_new(components, connections, config.clone()).is_err_and(|e| { e == Error::invalid_graph(concat!( "Battery(LiIon):2 can only have predecessors that are ", "BatteryInverters or HybridInverters. Found Grid:1." @@ -368,6 +390,7 @@ mod tests { #[test] fn test_validate_ev_chargers() { + let config = ComponentGraphConfig::default(); let mut components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -380,21 +403,23 @@ mod tests { TestConnection::new(3, 4), ]; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { - e == Error::invalid_graph( - "EVCharger(DC):3 can't have any successors. Found Electrolyzer:4.", - ) - }), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| { + e == Error::invalid_graph( + "EVCharger(DC):3 can't have any successors. Found Electrolyzer:4.", + ) + }), ); components.pop(); connections.pop(); - assert!(ComponentGraph::try_new(components, connections).is_ok()); + assert!(ComponentGraph::try_new(components, connections, config.clone()).is_ok()); } #[test] fn test_validate_chps() { + let config = ComponentGraphConfig::default(); let mut components = vec![ TestComponent::new(1, ComponentCategory::Grid), TestComponent::new(2, ComponentCategory::Meter), @@ -407,14 +432,17 @@ mod tests { TestConnection::new(3, 4), ]; assert!( - ComponentGraph::try_new(components.clone(), connections.clone()).is_err_and(|e| { - e == Error::invalid_graph("CHP:3 can't have any successors. Found Electrolyzer:4.") - }), + ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) + .is_err_and(|e| { + e == Error::invalid_graph( + "CHP:3 can't have any successors. Found Electrolyzer:4.", + ) + }), ); components.pop(); connections.pop(); - assert!(ComponentGraph::try_new(components, connections).is_ok()); + assert!(ComponentGraph::try_new(components, connections, config.clone()).is_ok()); } } diff --git a/src/lib.rs b/src/lib.rs index 1140d4d..d844bee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,3 +65,6 @@ pub use graph_traits::{Edge, Node}; mod error; pub use error::Error; + +mod config; +pub use config::ComponentGraphConfig; From dc01462aeabd419800a8f9c3207d149fb4242d12 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 20 Oct 2024 16:59:15 +0200 Subject: [PATCH 2/8] Accept config when building a test graph in `ComponentGraphBuilder` Signed-off-by: Sahas Subramanian --- src/graph/formulas/fallback.rs | 6 ++--- src/graph/formulas/generators/battery.rs | 12 ++++----- src/graph/formulas/generators/chp.rs | 10 ++++---- src/graph/formulas/generators/consumer.rs | 28 ++++++++++----------- src/graph/formulas/generators/ev_charger.rs | 10 ++++---- src/graph/formulas/generators/grid.rs | 6 ++--- src/graph/formulas/generators/producer.rs | 14 +++++------ src/graph/formulas/generators/pv.rs | 10 ++++---- src/graph/retrieval.rs | 4 +-- src/graph/test_utils.rs | 7 ++++-- 10 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs index a0c184d..e3802f5 100644 --- a/src/graph/formulas/fallback.rs +++ b/src/graph/formulas/fallback.rs @@ -187,7 +187,7 @@ mod tests { assert_eq!(grid_meter.component_id(), 1); assert_eq!(meter_bat_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; let expr = graph.fallback_expr(vec![1, 2], false)?; assert_eq!(expr.to_string(), "#1 + COALESCE(#3, #2, 0.0)"); @@ -203,7 +203,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 5); - let graph = builder.build()?; + let graph = builder.build(None)?; let expr = graph.fallback_expr(vec![3, 5], false)?; assert_eq!( expr.to_string(), @@ -252,7 +252,7 @@ mod tests { assert_eq!(chp.component_id(), 13); assert_eq!(pv_inverter.component_id(), 14); - let graph = builder.build()?; + let graph = builder.build(None)?; let expr = graph.fallback_expr(vec![5, 12], true)?; assert_eq!( expr.to_string(), diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs index eddf836..4ef355b 100644 --- a/src/graph/formulas/generators/battery.rs +++ b/src/graph/formulas/generators/battery.rs @@ -101,7 +101,7 @@ mod tests { let grid_meter = builder.meter(); builder.connect(grid, grid_meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.battery_formula(None)?; assert_eq!(formula, "0.0"); @@ -112,7 +112,7 @@ mod tests { assert_eq!(grid_meter.component_id(), 1); assert_eq!(meter_bat_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.battery_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -122,7 +122,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 5); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.battery_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0) + COALESCE(#6, #5, 0.0)"); @@ -143,7 +143,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 9); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.battery_formula(None)?; assert_eq!( formula, @@ -168,7 +168,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 14); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.battery_formula(None)?; assert_eq!( formula, @@ -193,7 +193,7 @@ mod tests { assert_eq!(inv_bat_chain.component_id(), 20); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.battery_formula(None)?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/chp.rs b/src/graph/formulas/generators/chp.rs index 098468f..ebf2f44 100644 --- a/src/graph/formulas/generators/chp.rs +++ b/src/graph/formulas/generators/chp.rs @@ -77,7 +77,7 @@ mod tests { let grid_meter = builder.meter(); builder.connect(grid, grid_meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.chp_formula(None)?; assert_eq!(formula, "0.0"); @@ -88,7 +88,7 @@ mod tests { assert_eq!(grid_meter.component_id(), 1); assert_eq!(meter_chp_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.chp_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -98,7 +98,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 4); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.chp_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -108,7 +108,7 @@ mod tests { assert_eq!(meter_chp_chain.component_id(), 8); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.chp_formula(None)?; assert_eq!( formula, @@ -127,7 +127,7 @@ mod tests { assert_eq!(meter_chp_chain.component_id(), 11); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.chp_formula(None)?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index e441cf7..c387398 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -137,7 +137,7 @@ mod tests { let inv_bat_chain = builder.inv_bat_chain(1); builder.connect(grid, inv_bat_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!(formula, "0.0"); @@ -153,7 +153,7 @@ mod tests { let grid_meter = builder.meter(); builder.connect(grid, grid_meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!(formula, "MAX(0.0, #1)"); @@ -164,7 +164,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; // Formula subtracts the battery meter from the grid meter, and the // battery inverter from the battery meter. @@ -179,7 +179,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 5); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -211,7 +211,7 @@ mod tests { assert_eq!(ev_charger.component_id(), 10); assert_eq!(meter.component_id(), 11); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -240,7 +240,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 12); assert_eq!(dangling_meter.component_id(), 15); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -279,7 +279,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 1); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; // Formula subtracts inverter from battery meter, or shows zero // consumption if either of the components have no data. @@ -297,7 +297,7 @@ mod tests { assert_eq!(dangling_meter_1.component_id(), 6); assert_eq!(dangling_meter_2.component_id(), 7); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -316,7 +316,7 @@ mod tests { let inv_bat_chain = builder.inv_bat_chain(1); builder.connect(grid, inv_bat_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -340,7 +340,7 @@ mod tests { assert_eq!(pv_inv.component_id(), 10); assert_eq!(chp.component_id(), 11); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -370,7 +370,7 @@ mod tests { builder.connect(grid, grid_meter_2); builder.connect(grid, grid_meter_3); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!(formula, "MAX(0.0, #1) + MAX(0.0, #2) + MAX(0.0, #3)"); @@ -385,7 +385,7 @@ mod tests { assert_eq!(meter_pv_chain_1.component_id(), 4); assert_eq!(meter_pv_chain_2.component_id(), 6); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -408,7 +408,7 @@ mod tests { assert_eq!(meter.component_id(), 8); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, @@ -426,7 +426,7 @@ mod tests { let meter_bat_chain = builder.meter_bat_chain(1, 1); builder.connect(grid_meter_1, meter_bat_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.consumer_formula()?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/ev_charger.rs b/src/graph/formulas/generators/ev_charger.rs index 9c0db26..cb00eff 100644 --- a/src/graph/formulas/generators/ev_charger.rs +++ b/src/graph/formulas/generators/ev_charger.rs @@ -80,7 +80,7 @@ mod tests { let grid_meter = builder.meter(); builder.connect(grid, grid_meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.ev_charger_formula(None)?; assert_eq!(formula, "0.0"); @@ -91,7 +91,7 @@ mod tests { assert_eq!(grid_meter.component_id(), 1); assert_eq!(meter_ev_charger_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.ev_charger_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -101,7 +101,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 4); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.ev_charger_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -111,7 +111,7 @@ mod tests { assert_eq!(meter_ev_charger_chain.component_id(), 8); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.ev_charger_formula(None)?; assert_eq!( formula, @@ -132,7 +132,7 @@ mod tests { assert_eq!(meter_ev_charger_chain.component_id(), 11); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.ev_charger_formula(None)?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/grid.rs b/src/graph/formulas/generators/grid.rs index 803e07d..95c8edc 100644 --- a/src/graph/formulas/generators/grid.rs +++ b/src/graph/formulas/generators/grid.rs @@ -58,7 +58,7 @@ mod tests { builder.connect(grid, grid_meter); builder.connect(grid_meter, meter_bat_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.grid_formula()?; assert_eq!(formula, "#1"); @@ -75,7 +75,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 6); assert_eq!(meter_pv_chain.component_id(), 9); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.grid_formula()?; assert_eq!( formula, @@ -88,7 +88,7 @@ mod tests { assert_eq!(pv_inverter.component_id(), 11); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.grid_formula()?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/producer.rs b/src/graph/formulas/generators/producer.rs index 5f13379..12a0db7 100644 --- a/src/graph/formulas/generators/producer.rs +++ b/src/graph/formulas/generators/producer.rs @@ -77,14 +77,14 @@ mod tests { let grid_meter = builder.meter(); builder.connect(grid, grid_meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!(formula, "0.0"); let meter_pv_chain = builder.meter_pv_chain(2); builder.connect(grid_meter, meter_pv_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!( formula, @@ -95,7 +95,7 @@ mod tests { let meter_chp_chain = builder.meter_chp_chain(1); builder.connect(grid, meter_chp_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!( formula, @@ -109,7 +109,7 @@ mod tests { let chp = builder.chp(); builder.connect(grid, chp); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!( formula, @@ -124,7 +124,7 @@ mod tests { let pv_inverter = builder.solar_inverter(); builder.connect(grid_meter, pv_inverter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!( formula, @@ -140,7 +140,7 @@ mod tests { let meter_bat_chain = builder.meter_bat_chain(1, 1); builder.connect(grid_meter, meter_bat_chain); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!( formula, @@ -160,7 +160,7 @@ mod tests { builder.connect(meter, chp); builder.connect(grid_meter, meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.producer_formula()?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/pv.rs b/src/graph/formulas/generators/pv.rs index ec5e5ab..8065b35 100644 --- a/src/graph/formulas/generators/pv.rs +++ b/src/graph/formulas/generators/pv.rs @@ -80,7 +80,7 @@ mod tests { let grid_meter = builder.meter(); builder.connect(grid, grid_meter); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.pv_formula(None)?; assert_eq!(formula, "0.0"); @@ -91,7 +91,7 @@ mod tests { assert_eq!(grid_meter.component_id(), 1); assert_eq!(meter_pv_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.pv_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -101,7 +101,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 4); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.pv_formula(None)?; assert_eq!(formula, "COALESCE(#3, #2, 0.0)"); @@ -111,7 +111,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 8); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.pv_formula(None)?; assert_eq!( formula, @@ -130,7 +130,7 @@ mod tests { assert_eq!(meter_pv_chain.component_id(), 11); - let graph = builder.build()?; + let graph = builder.build(None)?; let formula = graph.pv_formula(None)?; assert_eq!( formula, diff --git a/src/graph/retrieval.rs b/src/graph/retrieval.rs index 2ed8eae..d4e378d 100644 --- a/src/graph/retrieval.rs +++ b/src/graph/retrieval.rs @@ -286,7 +286,7 @@ mod tests { assert_eq!(meter_bat_chain.component_id(), 2); - let graph = builder.build()?; + let graph = builder.build(None)?; assert_eq!( graph .siblings_from_predecessors(3) @@ -337,7 +337,7 @@ mod tests { builder.connect(grid_meter, dangling_meter); assert_eq!(dangling_meter.component_id(), 9); - let graph = builder.build()?; + let graph = builder.build(None)?; assert_eq!( graph .siblings_from_predecessors(8) diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs index 503d394..797970d 100644 --- a/src/graph/test_utils.rs +++ b/src/graph/test_utils.rs @@ -205,11 +205,14 @@ impl ComponentGraphBuilder { /// Builds and returns the component graph from the components and /// connections added to the builder. - pub(super) fn build(&self) -> Result, Error> { + pub(super) fn build( + &self, + config: Option, + ) -> Result, Error> { ComponentGraph::try_new( self.components.clone(), self.connections.clone(), - ComponentGraphConfig::default(), + config.unwrap_or_default(), ) } } From e6eda014b46f03a7d48dfccf09f2abfac2551d41 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 20 Oct 2024 17:26:23 +0200 Subject: [PATCH 3/8] Refactor graph creation tests to use `ComponentGraphBuilder` This would make it easy to test upcoming graph config parameters. Signed-off-by: Sahas Subramanian --- src/graph/creation.rs | 172 +++++++++++++++------------------------- src/graph/test_utils.rs | 26 ++++++ 2 files changed, 92 insertions(+), 106 deletions(-) diff --git a/src/graph/creation.rs b/src/graph/creation.rs index d3b4115..eb43435 100644 --- a/src/graph/creation.rs +++ b/src/graph/creation.rs @@ -120,123 +120,83 @@ 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, Vec) { - 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 config = ComponentGraphConfig::default(); - let (mut components, mut connections) = nodes_and_edges(); - - assert!( - ComponentGraph::try_new(components.clone(), connections.clone(), config.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(), config.clone()) - .is_ok() - ); - - components.push(TestComponent::new(2, ComponentCategory::Meter)); - assert!( - ComponentGraph::try_new(components.clone(), connections.clone(), config.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(), config.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(); + builder.add_component(ComponentCategory::Inverter(InverterType::Unspecified)); + 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(), config.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(), config.clone()) - .is_err_and(|e| e == Error::invalid_graph("Multiple grid components found.")) - ); - - components.pop(); - assert!( - ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) - .is_ok() - ); + + builder.pop_component(); + 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 config = ComponentGraphConfig::default(); - 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(), config.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(), config.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(), config.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()); } } diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs index 797970d..42003ab 100644 --- a/src/graph/test_utils.rs +++ b/src/graph/test_utils.rs @@ -57,6 +57,10 @@ impl Edge for TestConnection { pub(super) struct ComponentHandle(u64); impl ComponentHandle { + pub(super) fn new(id: u64) -> Self { + ComponentHandle(id) + } + /// Returns the component ID of the component. pub(super) fn component_id(&self) -> u64 { self.0 @@ -92,6 +96,28 @@ impl ComponentGraphBuilder { handle } + /// Adds a component with the given id to the graph and returns its handle. + pub(super) fn add_component_with_id( + &mut self, + id: u64, + category: ComponentCategory, + ) -> ComponentHandle { + self.components + .push(TestComponent::new(id, category.clone())); + let handle = ComponentHandle(id); + handle + } + + /// Pops the last component added to the graph. + pub(super) fn pop_component(&mut self) { + self.components.pop(); + } + + /// Pops the last connection added to the graph. + pub(super) fn pop_connection(&mut self) { + self.connections.pop(); + } + /// Adds a grid component to the graph and returns its handle. pub(super) fn grid(&mut self) -> ComponentHandle { self.add_component(ComponentCategory::Grid) From 50ea5fec8cf239eba0b2d229ce6d6feaad10f6bf Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 20 Oct 2024 18:57:14 +0200 Subject: [PATCH 4/8] Collect all validation errors before exiting This means users don't have to fix one issue at a time and recreate the graph to get the next error, but can instead get all the issues in the graph at once. Signed-off-by: Sahas Subramanian --- src/error.rs | 4 +-- src/graph/validation.rs | 35 +++++++++++++++++----- src/graph/validation/validate_neighbors.rs | 11 ++++--- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/error.rs b/src/error.rs index 332e70e..a8a2d6e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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, @@ -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, diff --git a/src/graph/validation.rs b/src/graph/validation.rs index 71a4a73..53ae20d 100644 --- a/src/graph/validation.rs +++ b/src/graph/validation.rs @@ -33,16 +33,35 @@ where let validator = ComponentGraphValidator { cg: self, root }; + // Fail immediately if there are cycles in the graph, as this may cause + // subsequent validations to get stuck in an infinite loop. validator.validate_acyclicity(root, vec![])?; - validator.validate_connected_graph(root)?; - - validator.validate_root()?; - validator.validate_meters()?; - validator.validate_inverters()?; - validator.validate_batteries()?; - validator.validate_ev_chargers()?; - validator.validate_chps()?; + let mut errors = vec![]; + for result in [ + validator.validate_connected_graph(root), + validator.validate_root(), + validator.validate_meters(), + validator.validate_inverters(), + validator.validate_batteries(), + validator.validate_ev_chargers(), + validator.validate_chps(), + ] { + if let Err(e) = result { + errors.push(e); + } + } + if errors.len() == 1 { + return Err(errors[0].clone()); + } else if !errors.is_empty() { + let error_messages = "Multiple validation failures:\n ".to_string() + + &errors + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join("\n "); + return Err(Error::invalid_graph(error_messages)); + } Ok(()) } } diff --git a/src/graph/validation/validate_neighbors.rs b/src/graph/validation/validate_neighbors.rs index 4ac3542..cee0d9f 100644 --- a/src/graph/validation/validate_neighbors.rs +++ b/src/graph/validation/validate_neighbors.rs @@ -196,12 +196,11 @@ mod tests { let connections = vec![TestConnection::new(1, 2), TestConnection::new(2, 3)]; assert!( ComponentGraph::try_new(components, connections, config.clone()).is_err_and(|e| { - e == Error::invalid_graph(concat!( - "Meter:2 can only have successors that are not Batteries. ", - "Found Battery(LiIon):3." - )) - }), - ); + e.to_string() == +r#"InvalidGraph: Multiple validation failures: + InvalidGraph: Meter:2 can only have successors that are not Batteries. Found Battery(LiIon):3. + InvalidGraph: Battery(LiIon):3 can only have predecessors that are BatteryInverters or HybridInverters. Found Meter:2."# + })); } #[test] From 892c0c8908ba9a9eda7b2d968c5cbd0315e4e623 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sat, 19 Oct 2024 22:12:22 +0200 Subject: [PATCH 5/8] Add `allow_unspecified_inverters` config option When true, inverters with `InverterType::Unspecified` are treated as having `InverterType::Battery`. This commit also adds "tracing" as a dependency and logs warnings when inverters with unspecified type are encountered. Signed-off-by: Sahas Subramanian --- Cargo.toml | 1 + src/component_category.rs | 20 +++++++++++++---- src/config.rs | 7 +++++- src/graph/creation.rs | 21 +++++++++++++++--- src/graph/formulas/fallback.rs | 2 +- src/graph/formulas/generators/battery.rs | 25 ++++++++++++++++------ src/graph/formulas/generators/consumer.rs | 2 +- src/graph/meter_roles.rs | 2 +- src/graph/test_utils.rs | 8 +++---- src/graph/validation/validate_neighbors.rs | 20 ++++++++++++----- 10 files changed, 82 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f6e83ca..f13b282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ path = "src/lib.rs" [dependencies] petgraph = "0.6.5" +tracing = "0.1.40" diff --git a/src/component_category.rs b/src/component_category.rs index fec9ff2..8cbf6ad 100644 --- a/src/component_category.rs +++ b/src/component_category.rs @@ -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. @@ -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 { @@ -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 { diff --git a/src/config.rs b/src/config.rs index a99e29d..25c2c53 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,4 +5,9 @@ /// Configuration options for the `ComponentGraph`. #[derive(Clone, Default, Debug)] -pub struct ComponentGraphConfig {} +pub struct ComponentGraphConfig { + /// 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, +} diff --git a/src/graph/creation.rs b/src/graph/creation.rs index eb43435..4f81e10 100644 --- a/src/graph/creation.rs +++ b/src/graph/creation.rs @@ -71,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}" ))); @@ -163,12 +163,27 @@ mod tests { == Error::invalid_component("ComponentCategory not specified for component: 8"))); builder.pop_component(); - builder.add_component(ComponentCategory::Inverter(InverterType::Unspecified)); + 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") )); + // With `allow_unspecified_inverters=true`, unspecified inverter types + // are treated as battery inverters. + let unspec_inv_config = ComponentGraphConfig { + allow_unspecified_inverters: true, + }; - builder.pop_component(); + 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) diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs index e3802f5..8da2bf5 100644 --- a/src/graph/formulas/fallback.rs +++ b/src/graph/formulas/fallback.rs @@ -120,7 +120,7 @@ where component_id: u64, ) -> Result, Error> { let component = self.graph.component(component_id)?; - if !component.is_battery_inverter() + if !component.is_battery_inverter(&self.graph.config) && !component.is_chp() && !component.is_pv_inverter() && !component.is_ev_charger() diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs index 4ef355b..739028c 100644 --- a/src/graph/formulas/generators/battery.rs +++ b/src/graph/formulas/generators/battery.rs @@ -31,7 +31,7 @@ where } else { graph.find_all( graph.root_id, - |node| node.is_battery_inverter(), + |node| node.is_battery_inverter(&graph.config), petgraph::Direction::Outgoing, false, )? @@ -91,7 +91,9 @@ where mod tests { use std::collections::BTreeSet; - use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + use crate::{ + graph::test_utils::ComponentGraphBuilder, ComponentGraphConfig, Error, InverterType, + }; #[test] fn test_battery_formula() -> Result<(), Error> { @@ -188,12 +190,23 @@ mod tests { assert_eq!(meter.component_id(), 17); assert_eq!(inv_bat_chain.component_id(), 18); - let inv_bat_chain = builder.inv_bat_chain(1); - builder.connect(meter, inv_bat_chain); + let unspec_inverter = builder.add_component(crate::ComponentCategory::Inverter( + InverterType::Unspecified, + )); + let battery = builder.battery(); + builder.connect(unspec_inverter, battery); + builder.connect(meter, unspec_inverter); - assert_eq!(inv_bat_chain.component_id(), 20); + assert_eq!(unspec_inverter.component_id(), 20); - let graph = builder.build(None)?; + assert!(builder + .build(None) + .is_err_and(|x| x.to_string() + == "InvalidComponent: InverterType not specified for inverter: 20")); + + let graph = builder.build(Some(ComponentGraphConfig { + allow_unspecified_inverters: true, + }))?; let formula = graph.battery_formula(None)?; assert_eq!( formula, diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index c387398..7e22109 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -49,7 +49,7 @@ where let other_grid_successors = self .graph .successors(self.graph.root_id)? - .filter(|s| !s.is_meter() && !s.is_battery_inverter()) + .filter(|s| !s.is_meter() && !s.is_battery_inverter(&self.graph.config)) .map(|s| self.component_consumption(s.component_id())) .reduce(|a, b| Ok(a? + b?)); diff --git a/src/graph/meter_roles.rs b/src/graph/meter_roles.rs index ab8f36f..a74e1eb 100644 --- a/src/graph/meter_roles.rs +++ b/src/graph/meter_roles.rs @@ -36,7 +36,7 @@ where Ok(self.component(component_id)?.is_meter() && self.successors(component_id)?.all(|n| { has_successors = true; - n.is_battery_inverter() + n.is_battery_inverter(&self.config) }) && has_successors) } diff --git a/src/graph/test_utils.rs b/src/graph/test_utils.rs index 42003ab..dc4822b 100644 --- a/src/graph/test_utils.rs +++ b/src/graph/test_utils.rs @@ -109,13 +109,13 @@ impl ComponentGraphBuilder { } /// Pops the last component added to the graph. - pub(super) fn pop_component(&mut self) { - self.components.pop(); + pub(super) fn pop_component(&mut self) -> Option { + self.components.pop() } /// Pops the last connection added to the graph. - pub(super) fn pop_connection(&mut self) { - self.connections.pop(); + pub(super) fn pop_connection(&mut self) -> Option { + self.connections.pop() } /// Adds a grid component to the graph and returns its handle. diff --git a/src/graph/validation/validate_neighbors.rs b/src/graph/validation/validate_neighbors.rs index cee0d9f..334398f 100644 --- a/src/graph/validation/validate_neighbors.rs +++ b/src/graph/validation/validate_neighbors.rs @@ -79,10 +79,20 @@ where self.ensure_on_successors(inverter, |n| n.is_battery(), "Batteries")?; } InverterType::Unspecified => { - return Err(Error::invalid_graph(format!( - "Inverter {} has an unspecified inverter type.", - inverter.component_id() - ))); + if !self.cg.config.allow_unspecified_inverters { + return Err(Error::invalid_graph(format!( + "Inverter {} has an unspecified inverter type.", + inverter.component_id() + ))); + } else { + tracing::debug!( + concat!( + "Inverter {} has an unspecified inverter type will be ", + "considered a Battery Inverter." + ), + inverter.component_id() + ); + } } } } @@ -98,7 +108,7 @@ where self.ensure_leaf(battery)?; self.ensure_on_predecessors( battery, - |n| n.is_battery_inverter() || n.is_hybrid_inverter(), + |n| n.is_battery_inverter(&self.cg.config) || n.is_hybrid_inverter(), "BatteryInverters or HybridInverters", )?; } From be60e2220980e4a46c199ed64c437e525d97298e Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 20 Oct 2024 19:28:59 +0200 Subject: [PATCH 6/8] Add `allow_unconnected_components` config option When `true`, the graph will be considered valid even if it has unconnected components that are not reachable from the root. Signed-off-by: Sahas Subramanian --- src/config.rs | 4 +++ src/graph/creation.rs | 1 + src/graph/formulas/generators/battery.rs | 1 + src/graph/validation.rs | 43 ++++++++++++++++++------ src/graph/validation/validate_graph.rs | 11 ++++++ 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index 25c2c53..7044d8a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,10 @@ /// Configuration options for the `ComponentGraph`. #[derive(Clone, Default, Debug)] pub struct ComponentGraphConfig { + /// 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. diff --git a/src/graph/creation.rs b/src/graph/creation.rs index 4f81e10..679b1c5 100644 --- a/src/graph/creation.rs +++ b/src/graph/creation.rs @@ -175,6 +175,7 @@ mod tests { // 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()); diff --git a/src/graph/formulas/generators/battery.rs b/src/graph/formulas/generators/battery.rs index 739028c..045566c 100644 --- a/src/graph/formulas/generators/battery.rs +++ b/src/graph/formulas/generators/battery.rs @@ -206,6 +206,7 @@ mod tests { let graph = builder.build(Some(ComponentGraphConfig { allow_unspecified_inverters: true, + ..Default::default() }))?; let formula = graph.battery_formula(None)?; assert_eq!( diff --git a/src/graph/validation.rs b/src/graph/validation.rs index 53ae20d..8ceb9e5 100644 --- a/src/graph/validation.rs +++ b/src/graph/validation.rs @@ -38,8 +38,14 @@ where validator.validate_acyclicity(root, vec![])?; let mut errors = vec![]; + let mut validation_failed = false; + + if let Err(err) = validator.validate_connected_graph(root) { + errors.push(err); + validation_failed = !self.config.allow_unconnected_components; + } + for result in [ - validator.validate_connected_graph(root), validator.validate_root(), validator.validate_meters(), validator.validate_inverters(), @@ -49,18 +55,33 @@ where ] { if let Err(e) = result { errors.push(e); + validation_failed = true; } } - if errors.len() == 1 { - return Err(errors[0].clone()); - } else if !errors.is_empty() { - let error_messages = "Multiple validation failures:\n ".to_string() - + &errors - .into_iter() - .map(|e| e.to_string()) - .collect::>() - .join("\n "); - return Err(Error::invalid_graph(error_messages)); + match errors.len() { + 0 => {} + 1 => { + if validation_failed { + return Err(errors[0].clone()); + } else { + tracing::warn!("{}", errors[0]); + } + } + _ => { + let err = Error::invalid_graph(format!( + "Multiple validation failures:\n {}", + errors + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n ") + )); + if validation_failed { + return Err(err); + } else { + tracing::warn!("{}", err); + } + } } Ok(()) } diff --git a/src/graph/validation/validate_graph.rs b/src/graph/validation/validate_graph.rs index 86ab1a6..d8a10ca 100644 --- a/src/graph/validation/validate_graph.rs +++ b/src/graph/validation/validate_graph.rs @@ -152,12 +152,23 @@ mod tests { connections.push(TestConnection::new(11, 12)); + // With the default config, this fails validation. assert!( ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and( |e| e == Error::invalid_graph("Nodes [11, 12] are not connected to the root.") ) ); + // With `allow_unconnected_components=true`, this passes validation. + assert!(ComponentGraph::try_new( + components.clone(), + connections.clone(), + ComponentGraphConfig { + allow_unconnected_components: true, + ..config.clone() + } + ) + .is_ok()); connections.pop(); components.pop(); From 79e1e14f40a05306401def7569d5a13805dac8a0 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 20 Oct 2024 19:38:34 +0200 Subject: [PATCH 7/8] Add `allow_component_validation_failures` config option When `true`, the graph will be considered valid even if validation failed for some of the components. Signed-off-by: Sahas Subramanian --- src/config.rs | 5 +++++ src/graph/validation.rs | 2 +- src/graph/validation/validate_neighbors.rs | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 7044d8a..2bb7f37 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,11 @@ /// 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, diff --git a/src/graph/validation.rs b/src/graph/validation.rs index 8ceb9e5..6c0271b 100644 --- a/src/graph/validation.rs +++ b/src/graph/validation.rs @@ -55,7 +55,7 @@ where ] { if let Err(e) = result { errors.push(e); - validation_failed = true; + validation_failed = !self.config.allow_component_validation_failures; } } match errors.len() { diff --git a/src/graph/validation/validate_neighbors.rs b/src/graph/validation/validate_neighbors.rs index 334398f..1dc5300 100644 --- a/src/graph/validation/validate_neighbors.rs +++ b/src/graph/validation/validate_neighbors.rs @@ -275,6 +275,7 @@ r#"InvalidGraph: Multiple validation failures: TestConnection::new(2, 3), TestConnection::new(3, 4), ]; + // With default config, this validation fails assert!( ComponentGraph::try_new(components.clone(), connections.clone(), config.clone()) .is_err_and(|e| { @@ -283,6 +284,16 @@ r#"InvalidGraph: Multiple validation failures: ) }), ); + // With `allow_component_validation_failures=true`, this would pass. + assert!(ComponentGraph::try_new( + components.clone(), + connections.clone(), + ComponentGraphConfig { + allow_component_validation_failures: true, + ..config.clone() + } + ) + .is_ok()); components.pop(); connections.pop(); From 9bc80f1e24f97d75abf32d8029c1768e0990a245 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Thu, 9 Jan 2025 10:35:59 +0100 Subject: [PATCH 8/8] Add `disable_fallback_components` config option When this is `true`, formulas will be generated without considering fallback components. Signed-off-by: Sahas Subramanian --- src/config.rs | 4 +++ src/graph/formulas/fallback.rs | 37 ++++++++++++++++++++++- src/graph/formulas/generators/consumer.rs | 19 +++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 2bb7f37..d04c763 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,4 +19,8 @@ pub struct ComponentGraphConfig { /// 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, } diff --git a/src/graph/formulas/fallback.rs b/src/graph/formulas/fallback.rs index 8da2bf5..f7c1c11 100644 --- a/src/graph/formulas/fallback.rs +++ b/src/graph/formulas/fallback.rs @@ -45,6 +45,12 @@ where { fn generate(&self, mut component_ids: BTreeSet) -> Result { let mut formula = None::; + if self.graph.config.disable_fallback_components { + while let Some(component_id) = component_ids.pop_first() { + formula = Self::add_to_option(formula, Expr::component(component_id)); + } + return formula.ok_or(Error::internal("No components to generate formula.")); + } while let Some(component_id) = component_ids.pop_first() { if let Some(expr) = self.meter_fallback(component_id)? { formula = Self::add_to_option(formula, expr); @@ -169,7 +175,7 @@ where #[cfg(test)] mod tests { - use crate::{graph::test_utils::ComponentGraphBuilder, Error}; + use crate::{graph::test_utils::ComponentGraphBuilder, ComponentGraphConfig, Error}; #[test] fn test_meter_fallback() -> Result<(), Error> { @@ -197,6 +203,19 @@ mod tests { let expr = graph.fallback_expr(vec![3], true)?; assert_eq!(expr.to_string(), "COALESCE(#2, #3, 0.0)"); + let graph = builder.build(Some(ComponentGraphConfig { + disable_fallback_components: true, + ..Default::default() + }))?; + let expr = graph.fallback_expr(vec![1, 2], false)?; + assert_eq!(expr.to_string(), "#1 + #2"); + + let expr = graph.fallback_expr(vec![1, 2], true)?; + assert_eq!(expr.to_string(), "#1 + #2"); + + let expr = graph.fallback_expr(vec![3], true)?; + assert_eq!(expr.to_string(), "#3"); + // Add a battery meter with three inverter and three batteries let meter_bat_chain = builder.meter_bat_chain(3, 3); builder.connect(grid_meter, meter_bat_chain); @@ -241,6 +260,22 @@ mod tests { "COALESCE(#2, #3, 0.0) + COALESCE(#7, 0.0) + COALESCE(#8, 0.0)" ); + let graph = builder.build(Some(ComponentGraphConfig { + disable_fallback_components: true, + ..Default::default() + }))?; + let expr = graph.fallback_expr(vec![3, 5], false)?; + assert_eq!(expr.to_string(), "#3 + #5"); + + let expr = graph.fallback_expr(vec![2, 5], true)?; + assert_eq!(expr.to_string(), "#2 + #5"); + + let expr = graph.fallback_expr(vec![2, 6, 7, 8], true)?; + assert_eq!(expr.to_string(), "#2 + #6 + #7 + #8"); + + let expr = graph.fallback_expr(vec![2, 7, 8], true)?; + assert_eq!(expr.to_string(), "#2 + #7 + #8"); + let meter = builder.meter(); let chp = builder.chp(); let pv_inverter = builder.solar_inverter(); diff --git a/src/graph/formulas/generators/consumer.rs b/src/graph/formulas/generators/consumer.rs index 7e22109..0e17a05 100644 --- a/src/graph/formulas/generators/consumer.rs +++ b/src/graph/formulas/generators/consumer.rs @@ -126,7 +126,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::graph::test_utils::ComponentGraphBuilder; + use crate::{graph::test_utils::ComponentGraphBuilder, ComponentGraphConfig}; #[test] fn test_zero_consumers() -> Result<(), Error> { @@ -230,6 +230,23 @@ mod tests { "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0)" ) ); + let graph = builder.build(Some(ComponentGraphConfig { + disable_fallback_components: true, + ..Default::default() + }))?; + let formula = graph.consumer_formula()?; + assert_eq!( + formula, + concat!( + // difference of grid meter from all its suceessors (without fallbacks) + "MAX(0.0, #1 - #2 - #5 - #11) + ", + // difference of battery meter from battery inverter and pv + // meter from the two pv inverters. + "COALESCE(MAX(0.0, #2 - #3), 0.0) + COALESCE(MAX(0.0, #5 - #6 - #7), 0.0) + ", + // difference of "mixed" meter from its successors. + "COALESCE(MAX(0.0, #11 - #8 - #9 - #10), 0.0)" + ) + ); // add a battery chain to the grid meter and a dangling meter to the grid. let meter_bat_chain = builder.meter_bat_chain(1, 1);