use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
use crate::process::{ProcessID, ProcessMap, ProcessParameter, ProcessParameterMap};
use crate::region::parse_region_str;
use crate::units::{Dimensionless, MoneyPerActivity, MoneyPerCapacity, MoneyPerCapacityPerYear};
use crate::year::parse_year_str;
use ::log::warn;
use anyhow::{Context, Result, ensure};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;
const PROCESS_PARAMETERS_FILE_NAME: &str = "process_parameters.csv";
#[derive(PartialEq, Debug, Deserialize)]
struct ProcessParameterRaw {
process_id: String,
regions: String,
commission_years: String,
capital_cost: MoneyPerCapacity,
fixed_operating_cost: MoneyPerCapacityPerYear,
variable_operating_cost: MoneyPerActivity,
lifetime: u32,
discount_rate: Option<Dimensionless>,
}
impl ProcessParameterRaw {
fn into_parameter(self) -> Result<ProcessParameter> {
self.validate()?;
Ok(ProcessParameter {
capital_cost: self.capital_cost,
fixed_operating_cost: self.fixed_operating_cost,
variable_operating_cost: self.variable_operating_cost,
lifetime: self.lifetime,
discount_rate: self.discount_rate.unwrap_or(Dimensionless(0.0)),
})
}
}
impl ProcessParameterRaw {
fn validate(&self) -> Result<()> {
ensure!(
self.lifetime > 0,
"Error in parameter for process {}: Lifetime must be greater than 0",
self.process_id
);
if let Some(dr) = self.discount_rate {
ensure!(
dr >= Dimensionless(0.0),
"Error in parameter for process {}: Discount rate must be positive",
self.process_id
);
if dr > Dimensionless(1.0) {
warn!(
"Warning in parameter for process {}: Discount rate is greater than 1",
self.process_id
);
}
}
Ok(())
}
}
pub fn read_process_parameters(
model_dir: &Path,
processes: &ProcessMap,
milestone_years: &[u32],
) -> Result<HashMap<ProcessID, ProcessParameterMap>> {
let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME);
let iter = read_csv::<ProcessParameterRaw>(&file_path)?;
read_process_parameters_from_iter(iter, processes, milestone_years)
.with_context(|| input_err_msg(&file_path))
}
fn read_process_parameters_from_iter<I>(
iter: I,
processes: &ProcessMap,
milestone_years: &[u32],
) -> Result<HashMap<ProcessID, ProcessParameterMap>>
where
I: Iterator<Item = ProcessParameterRaw>,
{
let mut map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
for param_raw in iter {
let (id, process) = processes
.get_key_value(param_raw.process_id.as_str())
.with_context(|| format!("Process {} not found", param_raw.process_id))?;
let process_years: Vec<u32> = process.years.clone().collect();
let parameter_years = parse_year_str(¶m_raw.commission_years, &process_years)
.with_context(|| {
format!("Invalid year for process {id}. Valid years are {process_years:?}")
})?;
let process_regions = &process.regions;
let parameter_regions = parse_region_str(¶m_raw.regions, process_regions)
.with_context(|| {
format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
})?;
let param = Rc::new(param_raw.into_parameter()?);
let entry = map.entry(id.clone()).or_default();
for year in parameter_years {
for region in parameter_regions.clone() {
try_insert(entry, &(region, year), param.clone())?;
}
}
}
check_process_parameters(processes, &map, milestone_years)?;
Ok(map)
}
fn check_process_parameters(
processes: &ProcessMap,
map: &HashMap<ProcessID, ProcessParameterMap>,
milestone_years: &[u32],
) -> Result<()> {
for (process_id, process) in processes {
let parameters = map
.get(process_id)
.with_context(|| format!("Missing parameters for process {process_id}"))?;
let reference_regions = &process.regions;
let mut missing_keys = Vec::new();
for year in process
.years
.clone()
.filter(|y| milestone_years.contains(y))
{
for region in reference_regions {
let key = (region.clone(), year);
if !parameters.contains_key(&key) {
missing_keys.push(key);
}
}
}
ensure!(
missing_keys.is_empty(),
"Process {process_id} is missing parameters for the following regions and years: {}",
format_items_with_cap(&missing_keys)
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixture::{assert_error, process_parameter_map, processes, region_id};
use crate::process::{ProcessID, ProcessMap, ProcessParameterMap};
use crate::region::RegionID;
use rstest::rstest;
use std::collections::HashMap;
fn create_param_raw(
lifetime: u32,
discount_rate: Option<Dimensionless>,
) -> ProcessParameterRaw {
ProcessParameterRaw {
process_id: "id".to_string(),
capital_cost: MoneyPerCapacity(0.0),
fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
variable_operating_cost: MoneyPerActivity(0.0),
lifetime,
discount_rate,
commission_years: "all".to_string(),
regions: "all".to_string(),
}
}
fn create_param(discount_rate: Dimensionless) -> ProcessParameter {
ProcessParameter {
capital_cost: MoneyPerCapacity(0.0),
fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
variable_operating_cost: MoneyPerActivity(0.0),
lifetime: 1,
discount_rate,
}
}
#[test]
fn param_raw_into_param_ok() {
let raw = create_param_raw(1, Some(Dimensionless(1.0)));
assert_eq!(
raw.into_parameter().unwrap(),
create_param(Dimensionless(1.0))
);
let raw = create_param_raw(1, None);
assert_eq!(
raw.into_parameter().unwrap(),
create_param(Dimensionless(0.0))
);
}
#[rstest]
fn check_process_parameters_ok(
processes: ProcessMap,
process_parameter_map: ProcessParameterMap,
) {
let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
let process_id = processes.keys().next().unwrap().clone();
let milestone_years: Vec<u32> = vec![2010, 2020];
param_map.insert(process_id, process_parameter_map.clone());
let result = check_process_parameters(&processes, ¶m_map, &milestone_years);
result.unwrap();
}
#[rstest]
fn check_process_parameters_ok_missing_before_base_year(
processes: ProcessMap,
mut process_parameter_map: ProcessParameterMap,
region_id: RegionID,
) {
let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
let process_id = processes.keys().next().unwrap().clone();
let milestone_years: Vec<u32> = vec![2015, 2020];
process_parameter_map.remove(&(region_id, 2012)).unwrap();
param_map.insert(process_id, process_parameter_map);
let result = check_process_parameters(&processes, ¶m_map, &milestone_years);
result.unwrap();
}
#[rstest]
fn check_process_parameters_missing(
processes: ProcessMap,
mut process_parameter_map: ProcessParameterMap,
region_id: RegionID,
) {
let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
let process_id = processes.keys().next().unwrap().clone();
let milestone_years: Vec<u32> = vec![2010, 2020];
process_parameter_map.remove(&(region_id, 2010)).unwrap();
param_map.insert(process_id, process_parameter_map);
let result = check_process_parameters(&processes, ¶m_map, &milestone_years);
assert_error!(
result,
"Process process1 is missing parameters for the following regions and years: \
[(RegionID(\"GBR\"), 2010)]"
);
}
#[test]
fn param_raw_validate_bad_lifetime() {
assert!(
create_param_raw(0, Some(Dimensionless(1.0)))
.validate()
.is_err()
);
}
#[test]
fn param_raw_validate_bad_discount_rate() {
assert!(
create_param_raw(1, Some(Dimensionless(-1.0)))
.validate()
.is_err()
);
}
}