Skip to content

Commit fddbbc6

Browse files
committed
Fix unit tests
1 parent a56bb7a commit fddbbc6

4 files changed

Lines changed: 73 additions & 60 deletions

File tree

src/commodity.rs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ use crate::region::RegionID;
44
use crate::time_slice::{TimeSliceID, TimeSliceLevel, TimeSliceSelection};
55
use crate::units::{Flow, MoneyPerFlow};
66
use indexmap::IndexMap;
7-
use itertools::Itertools;
87
use serde::Deserialize;
98
use serde_string_enum::DeserializeLabeledStringEnum;
109
use std::collections::HashMap;
11-
use std::fmt::Display;
1210
use std::rc::Rc;
1311

1412
define_id_type! {CommodityID}
@@ -54,33 +52,6 @@ pub struct Commodity {
5452
}
5553
define_id_getter! {Commodity, CommodityID}
5654

57-
/// A set of commodities that can be invested in together
58-
pub enum InvestmentSet {
59-
/// Single commodity
60-
Single(CommodityID),
61-
/// A set of commodities that form a cycle
62-
Cycle(Vec<CommodityID>),
63-
}
64-
65-
impl InvestmentSet {
66-
/// Returns an iterator over the commodity IDs in this investment set
67-
pub fn iter(&self) -> impl Iterator<Item = &CommodityID> {
68-
match self {
69-
InvestmentSet::Single(id) => std::slice::from_ref(id).iter(),
70-
InvestmentSet::Cycle(ids) => ids.iter(),
71-
}
72-
}
73-
}
74-
75-
impl Display for InvestmentSet {
76-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77-
match self {
78-
InvestmentSet::Single(id) => write!(f, "{id}"),
79-
InvestmentSet::Cycle(ids) => write!(f, "[{}]", ids.iter().join(", ")),
80-
}
81-
}
82-
}
83-
8455
/// Type of balance for application of cost
8556
#[derive(PartialEq, Clone, Debug, DeserializeLabeledStringEnum)]
8657
pub enum BalanceType {

src/graph.rs

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
//! Module for creating and analysing commodity graphs
2-
use crate::commodity::{CommodityID, CommodityMap, CommodityType, InvestmentSet};
2+
use crate::commodity::{CommodityID, CommodityMap, CommodityType};
33
use crate::process::{ProcessID, ProcessMap};
44
use crate::region::RegionID;
5+
use crate::simulation::investment::InvestmentSet;
56
use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
67
use crate::units::{Dimensionless, Flow};
78
use anyhow::{Context, Result, ensure};
@@ -338,7 +339,7 @@ fn solve_investment_order(
338339
.iter()
339340
.rev()
340341
.filter_map(|node_idx| {
341-
// Get set of commodity IDs for the node
342+
// Get set of commodity ID(s) for the node, referring back to `condensed_graph`
342343
let commodities: Vec<CommodityID> = condensed_graph
343344
.node_weight(*node_idx)
344345
.unwrap()
@@ -495,7 +496,10 @@ mod tests {
495496
use std::rc::Rc;
496497

497498
#[rstest]
498-
fn test_topo_sort_linear_graph(sed_commodity: Commodity, svd_commodity: Commodity) {
499+
fn test_solve_investment_order_linear_graph(
500+
sed_commodity: Commodity,
501+
svd_commodity: Commodity,
502+
) {
499503
// Create a simple linear graph: A -> B -> C
500504
let mut graph = Graph::new();
501505

@@ -513,17 +517,18 @@ mod tests {
513517
commodities.insert("B".into(), Rc::new(sed_commodity));
514518
commodities.insert("C".into(), Rc::new(svd_commodity));
515519

516-
let result = solve_investment_order(&graph, &commodities).unwrap();
520+
let result = solve_investment_order(&graph, &commodities);
517521

518522
// Expected order: C, B, A (leaf nodes first)
523+
// No cycles, so all investment sets should be `Single`
519524
assert_eq!(result.len(), 3);
520-
assert_eq!(result[0], "C".into());
521-
assert_eq!(result[1], "B".into());
522-
assert_eq!(result[2], "A".into());
525+
assert_eq!(result[0], InvestmentSet::Single("C".into()));
526+
assert_eq!(result[1], InvestmentSet::Single("B".into()));
527+
assert_eq!(result[2], InvestmentSet::Single("A".into()));
523528
}
524529

525530
#[rstest]
526-
fn test_topo_sort_cyclic_graph(sed_commodity: Commodity) {
531+
fn test_solve_investment_order_cyclic_graph(sed_commodity: Commodity) {
527532
// Create a simple cyclic graph: A -> B -> A
528533
let mut graph = Graph::new();
529534

@@ -539,11 +544,14 @@ mod tests {
539544
commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
540545
commodities.insert("B".into(), Rc::new(sed_commodity));
541546

542-
// This should return an error due to the cycle
543-
// The error message should flag commodity B
544-
// Note: A is also involved in the cycle, but B is flagged as it is encountered first
545547
let result = solve_investment_order(&graph, &commodities);
546-
assert_error!(result, "Cycle detected in commodity graph for commodity B");
548+
549+
// Should be a single `Cycle` investment set containing both commodities
550+
assert_eq!(result.len(), 1);
551+
assert_eq!(
552+
result[0],
553+
InvestmentSet::Cycle(vec!["A".into(), "B".into()])
554+
);
547555
}
548556

549557
#[rstest]
@@ -570,8 +578,7 @@ mod tests {
570578
graph.add_edge(node_c, node_d, GraphEdge::Demand);
571579

572580
// Validate the graph at DayNight level
573-
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual);
574-
assert!(result.is_ok());
581+
assert!(validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual).is_ok());
575582
}
576583

577584
#[rstest]
@@ -596,8 +603,10 @@ mod tests {
596603
graph.add_edge(node_a, node_b, GraphEdge::Primary("process2".into()));
597604

598605
// Validate the graph at DayNight level
599-
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
600-
assert_error!(result, "SVD commodity A cannot be an input to a process");
606+
assert_error!(
607+
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
608+
"SVD commodity A cannot be an input to a process"
609+
);
601610
}
602611

603612
#[rstest]
@@ -614,8 +623,10 @@ mod tests {
614623
graph.add_edge(node_a, node_b, GraphEdge::Demand);
615624

616625
// Validate the graph at DayNight level
617-
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
618-
assert_error!(result, "SVD commodity A is demanded but has no producers");
626+
assert_error!(
627+
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
628+
"SVD commodity A is demanded but has no producers"
629+
);
619630
}
620631

621632
#[rstest]
@@ -633,9 +644,8 @@ mod tests {
633644
graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
634645

635646
// Validate the graph at DayNight level
636-
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
637647
assert_error!(
638-
result,
648+
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
639649
"SED commodity B may be consumed but has no producers"
640650
);
641651
}
@@ -661,9 +671,8 @@ mod tests {
661671
graph.add_edge(node_a, node_c, GraphEdge::Primary("process2".into()));
662672

663673
// Validate the graph at DayNight level
664-
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
665674
assert_error!(
666-
result,
675+
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
667676
"OTH commodity A cannot have both producers and consumers"
668677
);
669678
}

src/model.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! The model represents the static input data provided by the user.
22
use crate::agent::AgentMap;
3-
use crate::commodity::{CommodityMap, InvestmentSet};
3+
use crate::commodity::CommodityMap;
44
use crate::process::ProcessMap;
55
use crate::region::{Region, RegionID, RegionMap};
6+
use crate::simulation::investment::InvestmentSet;
67
use crate::time_slice::TimeSliceInfo;
78
use std::collections::HashMap;
89
use std::path::PathBuf;

src/simulation/investment.rs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use super::optimisation::{DispatchRun, FlowMap};
33
use crate::agent::Agent;
44
use crate::asset::{Asset, AssetIterator, AssetRef, AssetState};
5-
use crate::commodity::{Commodity, CommodityID, CommodityMap, InvestmentSet};
5+
use crate::commodity::{Commodity, CommodityID, CommodityMap};
66
use crate::model::Model;
77
use crate::output::DataWriter;
88
use crate::region::RegionID;
@@ -11,9 +11,10 @@ use crate::time_slice::{TimeSliceID, TimeSliceInfo};
1111
use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity};
1212
use anyhow::{Result, bail, ensure};
1313
use indexmap::IndexMap;
14-
use itertools::{chain, iproduct};
14+
use itertools::{Itertools, chain, iproduct};
1515
use log::debug;
1616
use std::collections::HashMap;
17+
use std::fmt::Display;
1718

1819
pub mod appraisal;
1920
use appraisal::coefficients::calculate_coefficients_for_assets;
@@ -25,6 +26,34 @@ type DemandMap = IndexMap<TimeSliceID, Flow>;
2526
/// Demand for a given combination of commodity, region and time slice
2627
type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceID), Flow>;
2728

29+
/// Represents a set of commodities which are invested in together.
30+
#[derive(PartialEq, Debug)]
31+
pub enum InvestmentSet {
32+
/// Assets are selected for a single commodity using `select_assets_for_commodity`
33+
Single(CommodityID),
34+
/// Assets are selected for a group of commodities which forms a cycle. NOT YET IMPLEMENTED.
35+
Cycle(Vec<CommodityID>),
36+
}
37+
38+
impl InvestmentSet {
39+
/// Returns an iterator over the commodity IDs in this investment set
40+
pub fn iter(&self) -> impl Iterator<Item = &CommodityID> {
41+
match self {
42+
InvestmentSet::Single(id) => std::slice::from_ref(id).iter(),
43+
InvestmentSet::Cycle(ids) => ids.iter(),
44+
}
45+
}
46+
}
47+
48+
impl Display for InvestmentSet {
49+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50+
match self {
51+
InvestmentSet::Single(id) => write!(f, "{id}"),
52+
InvestmentSet::Cycle(ids) => write!(f, "[{}]", ids.iter().join(", ")),
53+
}
54+
}
55+
}
56+
2857
/// Perform agent investment to determine capacity investment of new assets for next milestone year.
2958
///
3059
/// # Arguments
@@ -50,15 +79,18 @@ pub fn perform_agent_investment(
5079
let mut all_selected_assets = Vec::new();
5180

5281
// External prices to be used in dispatch optimisation
53-
// As investments are performed for each commodity/region combination, the system will become
54-
// able to produce its own prices endogenously, so we'll gradually remove these external prices.
82+
// Once investments are performed for a commodity, the dispatch system will be able to produce
83+
// endogenous prices for that commodity, so we'll gradually remove these external prices.
5584
let mut external_prices = prices.clone();
5685

5786
for region_id in model.iter_regions() {
58-
let investment_order = &model.investment_order[&(region_id.clone(), year)];
59-
60-
// Iterate over investment sets in the investment order
87+
// Keep track of the commodities that have been seen so far. This will be used to apply
88+
// balance constraints in the dispatch optimisation - e only apply balance constraints for
89+
// commodities that have been seen so far.
6190
let mut seen_commodities = Vec::new();
91+
92+
// Iterate over investment sets in the investment order for this region/year
93+
let investment_order = &model.investment_order[&(region_id.clone(), year)];
6294
for investment_set in investment_order {
6395
// Select assets for the commodity(/ies) of interest
6496
let selected_assets = match investment_set {
@@ -98,7 +130,7 @@ pub fn perform_agent_investment(
98130
external_prices.remove(commodity_id, region_id, time_slice);
99131
}
100132

101-
// If no assets have been selected for this region/commodity, skip dispatch optimisation
133+
// If no assets have been selected, skip dispatch optimisation
102134
// **TODO**: this probably means there's no demand for the commodity, which we could
103135
// presumably preempt
104136
if selected_assets.is_empty() {

0 commit comments

Comments
 (0)