Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading