use crate::commodity::{Commodity, CommodityID};
use crate::id::define_id_type;
use crate::region::RegionID;
use crate::time_slice::{Season, TimeSliceID, TimeSliceInfo, TimeSliceSelection};
use crate::units::{
ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
};
use anyhow::{Result, ensure};
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use serde_string_enum::DeserializeLabeledStringEnum;
use std::collections::HashMap;
use std::ops::RangeInclusive;
use std::rc::Rc;
define_id_type! {ProcessID}
pub type ProcessMap = IndexMap<ProcessID, Rc<Process>>;
pub type ProcessActivityLimitsMap = HashMap<(RegionID, u32), Rc<ActivityLimits>>;
pub type ProcessParameterMap = HashMap<(RegionID, u32), Rc<ProcessParameter>>;
pub type ProcessFlowsMap = HashMap<(RegionID, u32), Rc<IndexMap<CommodityID, ProcessFlow>>>;
pub type ProcessInvestmentConstraintsMap =
HashMap<(RegionID, u32), Rc<ProcessInvestmentConstraint>>;
#[derive(PartialEq, Debug)]
pub struct Process {
pub id: ProcessID,
pub description: String,
pub years: RangeInclusive<u32>,
pub activity_limits: ProcessActivityLimitsMap,
pub flows: ProcessFlowsMap,
pub parameters: ProcessParameterMap,
pub regions: IndexSet<RegionID>,
pub primary_output: Option<CommodityID>,
pub capacity_to_activity: ActivityPerCapacity,
pub investment_constraints: ProcessInvestmentConstraintsMap,
pub unit_size: Option<Capacity>,
}
impl Process {
pub fn active_for_year(&self, year: u32) -> bool {
self.years.contains(&year)
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct ActivityLimits {
annual_limit: Option<RangeInclusive<Dimensionless>>,
seasonal_limits: IndexMap<Season, RangeInclusive<Dimensionless>>,
time_slice_limits: IndexMap<TimeSliceID, RangeInclusive<Dimensionless>>,
}
impl ActivityLimits {
pub fn new_with_full_availability(time_slice_info: &TimeSliceInfo) -> Self {
let mut ts_limits = IndexMap::new();
for (ts_id, ts_length) in time_slice_info.iter() {
ts_limits.insert(
ts_id.clone(),
Dimensionless(0.0)..=Dimensionless(ts_length.value()),
);
}
ActivityLimits {
annual_limit: None,
seasonal_limits: IndexMap::new(),
time_slice_limits: ts_limits,
}
}
pub fn new_from_limits(
limits: &HashMap<TimeSliceSelection, RangeInclusive<Dimensionless>>,
time_slice_info: &TimeSliceInfo,
) -> Result<Self> {
let mut result = ActivityLimits::new_with_full_availability(time_slice_info);
let mut time_slices_added = IndexSet::new();
for (ts_selection, limit) in limits {
if let TimeSliceSelection::Single(ts_id) = ts_selection {
result.add_time_slice_limit(ts_id.clone(), limit.clone());
time_slices_added.insert(ts_id.clone());
}
}
if !time_slices_added.is_empty() {
let missing = time_slice_info
.iter_ids()
.filter(|ts_id| !time_slices_added.contains(*ts_id))
.collect::<Vec<_>>();
ensure!(
missing.is_empty(),
"Missing availability limits for time slices: [{}]. Please provide",
missing.iter().join(", ")
);
}
let mut seasons_added = IndexSet::new();
for (ts_selection, limit) in limits {
if let TimeSliceSelection::Season(season) = ts_selection {
result.add_seasonal_limit(season.clone(), limit.clone())?;
seasons_added.insert(season.clone());
}
}
if !seasons_added.is_empty() {
let missing = time_slice_info
.iter_seasons()
.filter(|season| !seasons_added.contains(*season))
.collect::<Vec<_>>();
ensure!(
missing.is_empty(),
"Missing availability limits for seasons: [{}]. Please provide",
missing.iter().join(", "),
);
}
if let Some(limit) = limits.get(&TimeSliceSelection::Annual) {
result.add_annual_limit(limit.clone())?;
}
Ok(result)
}
pub fn add_time_slice_limit(
&mut self,
ts_id: TimeSliceID,
limit: RangeInclusive<Dimensionless>,
) {
self.time_slice_limits.insert(ts_id, limit);
}
fn add_seasonal_limit(
&mut self,
season: Season,
limit: RangeInclusive<Dimensionless>,
) -> Result<()> {
let current_limit = self.get_limit_for_season(&season);
ensure!(
*limit.start() <= *current_limit.end() && *limit.end() >= *current_limit.start(),
"Availability limit for season {season} clashes with time slice limits",
);
if *limit.start() > *current_limit.start() || *limit.end() < *current_limit.end() {
self.seasonal_limits.insert(season, limit);
}
Ok(())
}
fn add_annual_limit(&mut self, limit: RangeInclusive<Dimensionless>) -> Result<()> {
let current_limit = self.get_limit_for_year();
ensure!(
*limit.start() <= *current_limit.end() && *limit.end() >= *current_limit.start(),
"Annual availability limit clashes with time slice/seasonal limits",
);
if *limit.start() > *current_limit.start() || *limit.end() < *current_limit.end() {
self.annual_limit = Some(limit);
}
Ok(())
}
pub fn get_limit(
&self,
time_slice_selection: &TimeSliceSelection,
) -> RangeInclusive<Dimensionless> {
match time_slice_selection {
TimeSliceSelection::Single(ts_id) => self.get_limit_for_time_slice(ts_id),
TimeSliceSelection::Season(season) => self.get_limit_for_season(season),
TimeSliceSelection::Annual => self.get_limit_for_year(),
}
}
pub fn get_limit_for_time_slice(
&self,
time_slice: &TimeSliceID,
) -> RangeInclusive<Dimensionless> {
let ts_limit = self.time_slice_limits[time_slice].clone();
let lower = *ts_limit.start();
let mut upper = *ts_limit.end();
if let Some(seasonal_limit) = self.seasonal_limits.get(&time_slice.season) {
upper = upper.min(*seasonal_limit.end());
}
if let Some(annual_limit) = &self.annual_limit {
upper = upper.min(*annual_limit.end());
}
lower..=upper
}
fn get_limit_for_season(&self, season: &Season) -> RangeInclusive<Dimensionless> {
let mut lower = Dimensionless(0.0);
let mut upper = Dimensionless(0.0);
for (ts, limit) in &self.time_slice_limits {
if &ts.season == season {
lower += *limit.start();
upper += *limit.end();
}
}
if let Some(seasonal_limit) = self.seasonal_limits.get(season) {
lower = lower.max(*seasonal_limit.start());
upper = upper.min(*seasonal_limit.end());
}
if let Some(annual_limit) = &self.annual_limit {
upper = upper.min(*annual_limit.end());
}
lower..=upper
}
fn get_limit_for_year(&self) -> RangeInclusive<Dimensionless> {
let mut total_lower = Dimensionless(0.0);
let mut total_upper = Dimensionless(0.0);
let seasons = self
.time_slice_limits
.keys()
.map(|ts_id| ts_id.season.clone())
.unique();
for season in seasons {
let season_limit = self.get_limit_for_season(&season);
total_lower += *season_limit.start();
total_upper += *season_limit.end();
}
if let Some(annual_limit) = &self.annual_limit {
total_lower = total_lower.max(*annual_limit.start());
total_upper = total_upper.min(*annual_limit.end());
}
total_lower..=total_upper
}
pub fn iter_limits(
&self,
) -> impl Iterator<Item = (TimeSliceSelection, &RangeInclusive<Dimensionless>)> {
let time_slice_limits = self
.time_slice_limits
.iter()
.map(|(ts_id, limit)| (TimeSliceSelection::Single(ts_id.clone()), limit));
let seasonal_limits = self
.seasonal_limits
.iter()
.map(|(season, limit)| (TimeSliceSelection::Season(season.clone()), limit));
let annual_limits = self
.annual_limit
.as_ref()
.map(|limit| (TimeSliceSelection::Annual, limit));
time_slice_limits
.chain(seasonal_limits)
.chain(annual_limits)
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct ProcessFlow {
pub commodity: Rc<Commodity>,
pub coeff: FlowPerActivity,
pub kind: FlowType,
pub cost: MoneyPerFlow,
}
impl ProcessFlow {
pub fn get_total_cost_per_flow(
&self,
region_id: &RegionID,
year: u32,
time_slice: &TimeSliceID,
) -> MoneyPerFlow {
self.cost + self.get_levy(region_id, year, time_slice)
}
pub fn get_total_cost_per_activity(
&self,
region_id: &RegionID,
year: u32,
time_slice: &TimeSliceID,
) -> MoneyPerActivity {
let cost_per_unit = self.get_total_cost_per_flow(region_id, year, time_slice);
self.coeff.abs() * cost_per_unit
}
fn get_levy(&self, region_id: &RegionID, year: u32, time_slice: &TimeSliceID) -> MoneyPerFlow {
match self.direction() {
FlowDirection::Input => *self
.commodity
.levies_cons
.get(&(region_id.clone(), year, time_slice.clone()))
.unwrap_or(&MoneyPerFlow(0.0)),
FlowDirection::Output => *self
.commodity
.levies_prod
.get(&(region_id.clone(), year, time_slice.clone()))
.unwrap_or(&MoneyPerFlow(0.0)),
FlowDirection::Zero => MoneyPerFlow(0.0),
}
}
pub fn direction(&self) -> FlowDirection {
match self.coeff {
x if x < FlowPerActivity(0.0) => FlowDirection::Input,
x if x > FlowPerActivity(0.0) => FlowDirection::Output,
_ => FlowDirection::Zero,
}
}
}
#[derive(PartialEq, Default, Debug, Clone, DeserializeLabeledStringEnum)]
pub enum FlowType {
#[default]
#[string = "fixed"]
Fixed,
#[string = "flexible"]
Flexible,
}
#[derive(PartialEq, Debug)]
pub enum FlowDirection {
Input,
Output,
Zero,
}
#[derive(PartialEq, Clone, Debug)]
pub struct ProcessParameter {
pub capital_cost: MoneyPerCapacity,
pub fixed_operating_cost: MoneyPerCapacityPerYear,
pub variable_operating_cost: MoneyPerActivity,
pub lifetime: u32,
pub discount_rate: Dimensionless,
}
#[derive(PartialEq, Debug, Clone)]
pub struct ProcessInvestmentConstraint {
pub addition_limit: Option<Capacity>,
}
impl ProcessInvestmentConstraint {
pub fn get_addition_limit(&self) -> Option<Capacity> {
self.addition_limit
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commodity::{CommodityLevyMap, CommodityType, DemandMap, PricingStrategy};
use crate::fixture::{assert_error, region_id, time_slice, time_slice_info2};
use crate::time_slice::TimeSliceLevel;
use crate::time_slice::TimeSliceSelection;
use float_cmp::assert_approx_eq;
use rstest::{fixture, rstest};
use std::collections::HashMap;
use std::rc::Rc;
#[fixture]
fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
let mut levies_prod = CommodityLevyMap::new();
let mut levies_cons = CommodityLevyMap::new();
levies_prod.insert(
(region_id.clone(), 2020, time_slice.clone()),
MoneyPerFlow(10.0),
);
levies_cons.insert(
(region_id.clone(), 2020, time_slice.clone()),
MoneyPerFlow(-10.0),
);
levies_prod.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(5.0));
levies_cons.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(-5.0));
levies_prod.insert(
(region_id.clone(), 2030, time_slice.clone()),
MoneyPerFlow(7.0),
);
levies_cons.insert(
(region_id.clone(), 2030, time_slice.clone()),
MoneyPerFlow(-7.0),
);
levies_prod.insert(
(
region_id.clone(),
2020,
TimeSliceID {
season: "summer".into(),
time_of_day: "day".into(),
},
),
MoneyPerFlow(3.0),
);
levies_cons.insert(
(
region_id.clone(),
2020,
TimeSliceID {
season: "summer".into(),
time_of_day: "day".into(),
},
),
MoneyPerFlow(-3.0),
);
Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod,
levies_cons,
demand: DemandMap::new(),
units: "PJ".into(),
})
}
#[fixture]
fn commodity_with_consumption_levy(
region_id: RegionID,
time_slice: TimeSliceID,
) -> Rc<Commodity> {
let mut levies = CommodityLevyMap::new();
levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: CommodityLevyMap::new(),
levies_cons: levies,
demand: DemandMap::new(),
units: "PJ".into(),
})
}
#[fixture]
fn commodity_with_production_levy(
region_id: RegionID,
time_slice: TimeSliceID,
) -> Rc<Commodity> {
let mut levies = CommodityLevyMap::new();
levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: levies,
levies_cons: CommodityLevyMap::new(),
demand: DemandMap::new(),
units: "PJ".into(),
})
}
#[fixture]
fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
let mut levies_prod = CommodityLevyMap::new();
levies_prod.insert(
(region_id.clone(), 2020, time_slice.clone()),
MoneyPerFlow(-5.0),
);
let mut levies_cons = CommodityLevyMap::new();
levies_cons.insert((region_id, 2020, time_slice), MoneyPerFlow(5.0));
Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod,
levies_cons,
demand: DemandMap::new(),
units: "PJ".into(),
})
}
#[fixture]
fn commodity_no_levies() -> Rc<Commodity> {
Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: CommodityLevyMap::new(),
levies_cons: CommodityLevyMap::new(),
demand: DemandMap::new(),
units: "PJ".into(),
})
}
#[fixture]
fn flow_with_cost() -> ProcessFlow {
ProcessFlow {
commodity: Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: CommodityLevyMap::new(),
levies_cons: CommodityLevyMap::new(),
demand: DemandMap::new(),
units: "PJ".into(),
}),
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(5.0),
}
}
#[fixture]
fn flow_with_cost_and_levy(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
let mut levies = CommodityLevyMap::new();
levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
ProcessFlow {
commodity: Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: levies,
levies_cons: CommodityLevyMap::new(),
demand: DemandMap::new(),
units: "PJ".into(),
}),
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(5.0),
}
}
#[fixture]
fn flow_with_cost_and_incentive(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
let mut levies = CommodityLevyMap::new();
levies.insert((region_id, 2020, time_slice), MoneyPerFlow(-3.0));
ProcessFlow {
commodity: Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: levies,
levies_cons: CommodityLevyMap::new(),
demand: DemandMap::new(),
units: "PJ".into(),
}),
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(5.0),
}
}
#[rstest]
fn get_levy_no_levies(
commodity_no_levies: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_no_levies,
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(0.0)
);
}
#[rstest]
fn get_levy_with_levy(
commodity_with_levy: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_levy,
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(10.0)
);
}
#[rstest]
fn get_levy_with_incentive(
commodity_with_incentive: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_incentive,
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(-5.0)
);
}
#[rstest]
fn get_levy_different_region(commodity_with_levy: Rc<Commodity>, time_slice: TimeSliceID) {
let flow = ProcessFlow {
commodity: commodity_with_levy,
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(&"USA".into(), 2020, &time_slice),
MoneyPerFlow(5.0)
);
}
#[rstest]
fn get_levy_different_year(
commodity_with_levy: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_levy,
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2030, &time_slice),
MoneyPerFlow(7.0)
);
}
#[rstest]
fn get_levy_different_time_slice(commodity_with_levy: Rc<Commodity>, region_id: RegionID) {
let flow = ProcessFlow {
commodity: commodity_with_levy,
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
let different_time_slice = TimeSliceID {
season: "summer".into(),
time_of_day: "day".into(),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &different_time_slice),
MoneyPerFlow(3.0)
);
}
#[rstest]
fn get_levy_consumption_positive_coeff(
commodity_with_consumption_levy: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_consumption_levy,
coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(0.0)
);
}
#[rstest]
fn get_levy_consumption_negative_coeff(
commodity_with_consumption_levy: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_consumption_levy,
coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(10.0)
);
}
#[rstest]
fn get_levy_production_positive_coeff(
commodity_with_production_levy: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_production_levy,
coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(10.0)
);
}
#[rstest]
fn get_levy_production_negative_coeff(
commodity_with_production_levy: Rc<Commodity>,
region_id: RegionID,
time_slice: TimeSliceID,
) {
let flow = ProcessFlow {
commodity: commodity_with_production_levy,
coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert_eq!(
flow.get_levy(®ion_id, 2020, &time_slice),
MoneyPerFlow(0.0)
);
}
#[rstest]
fn get_total_cost_base_cost(
flow_with_cost: ProcessFlow,
region_id: RegionID,
time_slice: TimeSliceID,
) {
assert_eq!(
flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
MoneyPerActivity(5.0)
);
}
#[rstest]
fn get_total_cost_with_levy(
flow_with_cost_and_levy: ProcessFlow,
region_id: RegionID,
time_slice: TimeSliceID,
) {
assert_eq!(
flow_with_cost_and_levy.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
MoneyPerActivity(15.0)
);
}
#[rstest]
fn get_total_cost_with_incentive(
flow_with_cost_and_incentive: ProcessFlow,
region_id: RegionID,
time_slice: TimeSliceID,
) {
assert_eq!(
flow_with_cost_and_incentive.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
MoneyPerActivity(2.0)
);
}
#[rstest]
fn get_total_cost_negative_coeff(
mut flow_with_cost: ProcessFlow,
region_id: RegionID,
time_slice: TimeSliceID,
) {
flow_with_cost.coeff = FlowPerActivity(-2.0);
assert_eq!(
flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
MoneyPerActivity(10.0)
);
}
#[rstest]
fn get_total_cost_zero_coeff(
mut flow_with_cost: ProcessFlow,
region_id: RegionID,
time_slice: TimeSliceID,
) {
flow_with_cost.coeff = FlowPerActivity(0.0);
assert_eq!(
flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
MoneyPerActivity(0.0)
);
}
#[test]
fn is_input_and_is_output() {
let commodity = Rc::new(Commodity {
id: "test_commodity".into(),
description: "Test commodity".into(),
kind: CommodityType::ServiceDemand,
time_slice_level: TimeSliceLevel::Annual,
pricing_strategy: PricingStrategy::Shadow,
levies_prod: CommodityLevyMap::new(),
levies_cons: CommodityLevyMap::new(),
demand: DemandMap::new(),
units: "PJ".into(),
});
let flow_in = ProcessFlow {
commodity: Rc::clone(&commodity),
coeff: FlowPerActivity(-1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
let flow_out = ProcessFlow {
commodity: Rc::clone(&commodity),
coeff: FlowPerActivity(1.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
let flow_zero = ProcessFlow {
commodity: Rc::clone(&commodity),
coeff: FlowPerActivity(0.0),
kind: FlowType::Fixed,
cost: MoneyPerFlow(0.0),
};
assert!(flow_in.direction() == FlowDirection::Input);
assert!(flow_out.direction() == FlowDirection::Output);
assert!(flow_zero.direction() == FlowDirection::Zero);
}
#[rstest]
fn new_with_full_availability(time_slice_info2: TimeSliceInfo) {
let limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
for (ts_id, ts_len) in time_slice_info2.iter() {
let l = limits.get_limit_for_time_slice(ts_id);
assert_eq!(*l.start(), Dimensionless(0.0));
assert_eq!(*l.end(), Dimensionless(ts_len.value()));
}
let annual_limit = limits.get_limit(&TimeSliceSelection::Annual);
assert_approx_eq!(Dimensionless, *annual_limit.start(), Dimensionless(0.0));
assert_approx_eq!(Dimensionless, *annual_limit.end(), Dimensionless(1.0));
}
#[rstest]
fn new_from_limits_with_seasonal_limit_applied(time_slice_info2: TimeSliceInfo) {
let mut limits = HashMap::new();
limits.insert(
TimeSliceSelection::Season("winter".into()),
Dimensionless(0.0)..=Dimensionless(0.01),
);
let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
for (ts_id, _ts_len) in time_slice_info2.iter() {
let ts_limit = result.get_limit_for_time_slice(ts_id);
assert_eq!(*ts_limit.end(), Dimensionless(0.01));
}
let season_limit = result.get_limit(&TimeSliceSelection::Season("winter".into()));
assert_eq!(*season_limit.end(), Dimensionless(0.01));
}
#[rstest]
fn new_from_limits_with_annual_limit_applied(time_slice_info2: TimeSliceInfo) {
let mut limits = HashMap::new();
limits.insert(
TimeSliceSelection::Annual,
Dimensionless(0.0)..=Dimensionless(0.01),
);
let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
for (ts_id, _ts_len) in time_slice_info2.iter() {
let ts_limit = result.get_limit_for_time_slice(ts_id);
assert_eq!(*ts_limit.end(), Dimensionless(0.01));
}
let season_limit = result.get_limit(&TimeSliceSelection::Season("winter".into()));
assert_eq!(*season_limit.end(), Dimensionless(0.01));
let annual_limit = result.get_limit(&TimeSliceSelection::Annual);
assert_eq!(*annual_limit.end(), Dimensionless(0.01));
}
#[rstest]
fn new_from_limits_missing_timeslices_error(time_slice_info2: TimeSliceInfo) {
let mut limits = HashMap::new();
let first_ts = time_slice_info2.iter().next().unwrap().0.clone();
limits.insert(
TimeSliceSelection::Single(first_ts),
Dimensionless(0.0)..=Dimensionless(0.1),
);
assert_error!(
ActivityLimits::new_from_limits(&limits, &time_slice_info2),
"Missing availability limits for time slices: [winter.night]. Please provide"
);
}
#[rstest]
fn new_from_limits_incompatible_limits(time_slice_info2: TimeSliceInfo) {
let mut limits = HashMap::new();
for (ts_id, _ts_len) in time_slice_info2.iter() {
limits.insert(
TimeSliceSelection::Single(ts_id.clone()),
Dimensionless(0.0)..=Dimensionless(0.1),
);
}
limits.insert(
TimeSliceSelection::Season("winter".into()),
Dimensionless(0.99)..=Dimensionless(1.0),
);
assert_error!(
ActivityLimits::new_from_limits(&limits, &time_slice_info2),
"Availability limit for season winter clashes with time slice limits"
);
}
}