Skip to content
75 changes: 58 additions & 17 deletions src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ use crate::commodity::CommodityID;
use crate::process::{FlowDirection, Process, ProcessFlow, ProcessID, ProcessParameter};
use crate::region::RegionID;
use crate::simulation::CommodityPrices;
use crate::time_slice::TimeSliceID;
use crate::units::{Activity, ActivityPerCapacity, Capacity, Dimensionless, MoneyPerActivity};
use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
use crate::units::{
Activity, ActivityPerCapacityPerYear, ActivityPerYear, Capacity, MoneyPerActivity, PerYear,
};
use anyhow::{Context, Result, ensure};
use indexmap::IndexMap;
use itertools::{Itertools, chain};
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::ops::{Deref, RangeInclusive};
use std::rc::Rc;
Expand Down Expand Up @@ -84,7 +85,7 @@ pub struct Asset {
/// The [`Process`] that this asset corresponds to
process: Rc<Process>,
/// Activity limits for this asset
activity_limits: Rc<HashMap<TimeSliceID, RangeInclusive<Dimensionless>>>,
activity_limits: Rc<IndexMap<TimeSliceSelection, RangeInclusive<PerYear>>>,
/// The commodity flows for this asset
flows: Rc<IndexMap<CommodityID, ProcessFlow>>,
/// The [`ProcessParameter`] corresponding to the asset's region and commission year
Expand Down Expand Up @@ -275,8 +276,11 @@ impl Asset {
}

/// Get the activity limits for this asset in a particular time slice
pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive<Activity> {
let limits = &self.activity_limits[time_slice];
pub fn get_activity_limits(
&self,
ts_selection: &TimeSliceSelection,
) -> RangeInclusive<ActivityPerYear> {
let limits = &self.activity_limits[ts_selection];
let max_act = self.max_activity();

// limits in real units (which are user defined)
Expand All @@ -287,12 +291,50 @@ impl Asset {
pub fn get_activity_per_capacity_limits(
&self,
time_slice: &TimeSliceID,
) -> RangeInclusive<ActivityPerCapacity> {
let limits = &self.activity_limits[time_slice];
) -> RangeInclusive<ActivityPerCapacityPerYear> {
// Get limits for this time slice specifically
let mut limits = self
.activity_limits
.get(&TimeSliceSelection::Single(time_slice.clone()))
.cloned()
.unwrap_or(PerYear(0.0)..=PerYear(1.0));
let mut combine_limits = |ts_selection| {
if let Some(other_limits) = self.activity_limits.get(&ts_selection) {
let lower = limits.start().max(*other_limits.start());
let upper = limits.end().min(*other_limits.end());
limits = lower..=upper;
}
};

// Take into account possibly more restrictive limits at seasonal and annual level
combine_limits(TimeSliceSelection::Season(time_slice.season.clone()));
combine_limits(TimeSliceSelection::Annual);

let cap2act = self.process.capacity_to_activity;
(cap2act * *limits.start())..=(cap2act * *limits.end())
}

/// Iterate over all activity limits for this asset
pub fn iter_activity_limits(
&self,
time_slice_info: &TimeSliceInfo,
) -> impl Iterator<Item = (&TimeSliceSelection, RangeInclusive<Activity>)> {
let max_act = self.max_activity();
self.activity_limits
.iter()
.map(move |(ts_selection, limits)| {
// NB: Unit types are correct here, but we're circumventing checks so we don't have
// to define a bunch of extra `Mul` impls
let mul = max_act.value() * time_slice_info.get_duration(ts_selection).value();
let to_act = |limit: PerYear| Activity(mul * limit.value());

(
ts_selection,
to_act(*limits.start())..=to_act(*limits.end()),
)
})
}

/// Get the operating cost for this asset in a given year and time slice
pub fn get_operating_cost(&self, year: u32, time_slice: &TimeSliceID) -> MoneyPerActivity {
// The cost for all commodity flows (including levies/incentives)
Expand Down Expand Up @@ -916,7 +958,6 @@ mod tests {
use itertools::{Itertools, assert_equal};
use map_macro::hash_map;
use rstest::{fixture, rstest};
use std::collections::HashMap;
use std::iter;
use std::rc::Rc;

Expand Down Expand Up @@ -963,7 +1004,7 @@ mod tests {
let flows = hash_map! {(region_id.clone(), 2020) => flow_map.into()};

// Create empty activity limits map
let activity_limits = hash_map! {(region_id.clone(), 2020) => Rc::new(HashMap::new())};
let activity_limits = hash_map! {(region_id.clone(), 2020) => Rc::new(IndexMap::new())};

let process = Rc::new(Process {
id: ProcessID::from("PROC1"),
Expand Down Expand Up @@ -1054,7 +1095,7 @@ mod tests {
.collect();
let activity_limits = years
.iter()
.map(|&year| ((region_id.clone(), year), Rc::new(HashMap::new())))
.map(|&year| ((region_id.clone(), year), Rc::new(IndexMap::new())))
.collect();
let flows = years
.iter()
Expand Down Expand Up @@ -1106,10 +1147,10 @@ mod tests {
season: "winter".into(),
time_of_day: "day".into(),
};
let fraction_limits = Dimensionless(1.0)..=Dimensionless(2.0);
let fraction_limits = PerYear(0.5)..=PerYear(1.0);
let mut flows = ProcessFlowsMap::new();
let mut activity_limits = ProcessActivityLimitsMap::new();
let limit_map = Rc::new(hash_map! {time_slice => fraction_limits});
let limit_map = Rc::new(indexmap! {time_slice.into() => fraction_limits});
for year in [2010, 2020] {
// empty flows map, but this is fine for our purposes
flows.insert((region_id.clone(), year), Rc::new(IndexMap::new()));
Expand Down Expand Up @@ -1143,8 +1184,8 @@ mod tests {
#[rstest]
fn test_asset_get_activity_limits(asset_with_activity_limits: Asset, time_slice: TimeSliceID) {
assert_eq!(
asset_with_activity_limits.get_activity_limits(&time_slice),
Activity(6.0)..=Activity(12.0)
asset_with_activity_limits.get_activity_limits(&time_slice.into()),
ActivityPerYear(3.0)..=ActivityPerYear(6.0)
);
}

Expand All @@ -1154,8 +1195,8 @@ mod tests {
time_slice: TimeSliceID,
) {
assert_eq!(
asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice),
ActivityPerCapacity(3.0)..=ActivityPerCapacity(6.0)
asset_with_activity_limits.get_activity_per_capacity_limits(&time_slice.into()),
ActivityPerCapacityPerYear(1.5)..=ActivityPerCapacityPerYear(3.0)
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ pub fn process(

// Create maps with (empty) entries for every region/year combo
let activity_limits = iproduct!(region_ids.iter(), years.iter())
.map(|(region_id, year)| ((region_id.clone(), *year), Rc::new(HashMap::new())))
.map(|(region_id, year)| ((region_id.clone(), *year), Rc::new(IndexMap::new())))
.collect();
let flows = iproduct!(region_ids.iter(), years.iter())
.map(|(region_id, year)| ((region_id.clone(), *year), Rc::new(IndexMap::new())))
Expand Down
60 changes: 48 additions & 12 deletions src/graph/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,54 @@ use crate::commodity::{CommodityMap, CommodityType};
use crate::process::ProcessMap;
use crate::region::RegionID;
use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
use crate::units::{Dimensionless, Flow};
use crate::units::{Flow, PerYear};
use anyhow::{Context, Result, ensure};
use indexmap::IndexMap;
use std::ops::RangeInclusive;
use strum::IntoEnumIterator;

/// Check whether any availability is permissible for the [`TimeSliceSelection`] and its parents
fn has_availability_with_parents(
limits: &IndexMap<TimeSliceSelection, RangeInclusive<PerYear>>,
ts_selection: &TimeSliceSelection,
) -> bool {
limits
.get(ts_selection)
.is_none_or(|limits| *limits.end() > PerYear(0.0))
&& ts_selection
.get_parent()
.is_none_or(|ts_selection| has_availability_with_parents(limits, &ts_selection))
}

/// Check whether any availability is permissible for any children of the specified
/// [`TimeSliceSelection`].
///
/// Returns true if there are no children.
fn children_have_availability(
limits: &IndexMap<TimeSliceSelection, RangeInclusive<PerYear>>,
ts_selection: &TimeSliceSelection,
time_slice_info: &TimeSliceInfo,
) -> bool {
ts_selection.iter_children(time_slice_info).all(|child| {
!(limits
.get(ts_selection)
.is_none_or(|limits| *limits.end() > PerYear(0.0))
&& children_have_availability(limits, &child, time_slice_info))
})
}

/// Check whether there is any availability for this [`TimeSliceSelection`].
///
/// Searches at both coarser and finer [`TimeSliceLevel`]s to check for limits.
fn has_any_availability(
limits: &IndexMap<TimeSliceSelection, RangeInclusive<PerYear>>,
ts_selection: &TimeSliceSelection,
time_slice_info: &TimeSliceInfo,
) -> bool {
has_availability_with_parents(limits, ts_selection)
&& children_have_availability(limits, ts_selection, time_slice_info)
}

/// Prepares a graph for validation with [`validate_commodities_graph`].
///
/// It takes a base graph produced by `create_commodities_graph_for_region_year`, and modifies it to
Expand Down Expand Up @@ -41,17 +84,10 @@ fn prepare_commodities_graph_for_validation(
};
let process = &processes[process_id];

// Check if the process has availability > 0 in any time slice in the selection
time_slice_selection
.iter(time_slice_info)
.any(|(time_slice, _)| {
let Some(limits_map) = process.activity_limits.get(&key) else {
return false;
};
limits_map
.get(time_slice)
.is_some_and(|avail| *avail.end() > Dimensionless(0.0))
})
// Check whether there is any availability for this time slice selection
process.activity_limits.get(&key).is_some_and(|limits| {
has_any_availability(limits, time_slice_selection, time_slice_info)
})
});

// Add demand edges
Expand Down
23 changes: 21 additions & 2 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ use region::read_regions;
mod time_slice;
use time_slice::read_time_slice_info;

/// A trait which provides a method to insert a key and value into a map
pub trait Insert<K, V> {
/// Insert a key and value into the map
fn insert(&mut self, key: K, value: V) -> Option<V>;
}

impl<K: Eq + Hash, V> Insert<K, V> for HashMap<K, V> {
fn insert(&mut self, key: K, value: V) -> Option<V> {
HashMap::insert(self, key, value)
}
}

impl<K: Eq + Hash, V> Insert<K, V> for IndexMap<K, V> {
fn insert(&mut self, key: K, value: V) -> Option<V> {
IndexMap::insert(self, key, value)
}
}

/// Read a series of type `T`s from a CSV file.
///
/// Will raise an error if the file is empty.
Expand Down Expand Up @@ -163,9 +181,10 @@ where
/// Inserts a key-value pair into a `HashMap` if the key does not already exist.
///
/// If the key already exists, it returns an error with a message indicating the key's existence.
pub fn try_insert<K, V>(map: &mut HashMap<K, V>, key: &K, value: V) -> Result<()>
pub fn try_insert<M, K, V>(map: &mut M, key: &K, value: V) -> Result<()>
where
K: Eq + Hash + Clone + fmt::Debug,
M: Insert<K, V>,
K: Eq + Hash + Clone + std::fmt::Debug,
{
let existing = map.insert(key.clone(), value).is_some();
ensure!(!existing, "Key {key:?} already exists in the map");
Expand Down
3 changes: 1 addition & 2 deletions src/input/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ pub fn read_processes(
) -> Result<ProcessMap> {
let base_year = milestone_years[0];
let mut processes = read_processes_file(model_dir, milestone_years, region_ids, commodities)?;
let mut activity_limits =
read_process_availabilities(model_dir, &processes, time_slice_info, base_year)?;
let mut activity_limits = read_process_availabilities(model_dir, &processes, time_slice_info)?;
let mut flows = read_process_flows(model_dir, &mut processes, commodities)?;
let mut parameters = read_process_parameters(model_dir, &processes, base_year)?;

Expand Down
Loading
Loading