1use crate::agent::AgentMap;
3use crate::asset::check_capacity_valid_for_asset;
4use crate::commodity::{CommodityID, CommodityMap};
5use crate::input::{
6 deserialise_proportion_nonzero, input_err_msg, is_sorted_and_unique, read_toml,
7};
8use crate::process::ProcessMap;
9use crate::region::{RegionID, RegionMap};
10use crate::time_slice::TimeSliceInfo;
11use crate::units::{Capacity, Dimensionless, MoneyPerFlow};
12use anyhow::{Context, Result, ensure};
13use log::warn;
14use serde::Deserialize;
15use serde_string_enum::DeserializeLabeledStringEnum;
16use std::collections::HashMap;
17use std::path::{Path, PathBuf};
18
19const MODEL_FILE_NAME: &str = "model.toml";
20
21macro_rules! define_unit_param_default {
22 ($name:ident, $type: ty, $value: expr) => {
23 fn $name() -> $type {
24 <$type>::new($value)
25 }
26 };
27}
28
29macro_rules! define_param_default {
30 ($name:ident, $type: ty, $value: expr) => {
31 fn $name() -> $type {
32 $value
33 }
34 };
35}
36
37define_unit_param_default!(default_candidate_asset_capacity, Capacity, 0.0001);
38define_unit_param_default!(default_capacity_limit_factor, Dimensionless, 0.1);
39define_unit_param_default!(default_value_of_lost_load, MoneyPerFlow, 1e9);
40define_unit_param_default!(default_price_tolerance, Dimensionless, 1e-6);
41define_param_default!(default_max_ironing_out_iterations, u32, 10);
42
43pub struct Model {
45 pub model_path: PathBuf,
47 pub parameters: ModelFile,
49 pub agents: AgentMap,
51 pub commodities: CommodityMap,
53 pub processes: ProcessMap,
55 pub time_slice_info: TimeSliceInfo,
57 pub regions: RegionMap,
59 pub commodity_order: HashMap<(RegionID, u32), Vec<CommodityID>>,
61}
62
63#[derive(Debug, Deserialize, PartialEq)]
65pub struct ModelFile {
66 pub milestone_years: Vec<u32>,
68 #[serde(default = "default_candidate_asset_capacity")]
72 pub candidate_asset_capacity: Capacity,
73 #[serde(default)]
75 pub pricing_strategy: PricingStrategy,
76 #[serde(default = "default_capacity_limit_factor")]
80 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
81 pub capacity_limit_factor: Dimensionless,
82 #[serde(default = "default_value_of_lost_load")]
86 pub value_of_lost_load: MoneyPerFlow,
87 #[serde(default = "default_max_ironing_out_iterations")]
89 pub max_ironing_out_iterations: u32,
90 #[serde(default = "default_price_tolerance")]
92 pub price_tolerance: Dimensionless,
93}
94
95#[derive(DeserializeLabeledStringEnum, Debug, PartialEq, Default)]
97pub enum PricingStrategy {
98 #[default]
100 #[string = "shadow_prices"]
101 ShadowPrices,
102 #[string = "scarcity_adjusted"]
104 ScarcityAdjusted,
105}
106
107fn check_milestone_years(years: &[u32]) -> Result<()> {
117 ensure!(!years.is_empty(), "`milestone_years` is empty");
118
119 ensure!(
120 is_sorted_and_unique(years),
121 "`milestone_years` must be composed of unique values in order"
122 );
123
124 Ok(())
125}
126
127impl ModelFile {
128 pub fn from_path<P: AsRef<Path>>(model_dir: P) -> Result<ModelFile> {
138 let file_path = model_dir.as_ref().join(MODEL_FILE_NAME);
139 let model_file: ModelFile = read_toml(&file_path)?;
140
141 if model_file.pricing_strategy == PricingStrategy::ScarcityAdjusted {
142 warn!(
143 "The pricing strategy is set to 'scarcity_adjusted'. Commodity prices may be \
144 incorrect if assets have more than one output commodity. See: {}/issues/677",
145 env!("CARGO_PKG_REPOSITORY")
146 );
147 }
148
149 let validate = || -> Result<()> {
150 check_milestone_years(&model_file.milestone_years)?;
151 check_capacity_valid_for_asset(model_file.candidate_asset_capacity)
152 .context("Invalid value for candidate_asset_capacity")?;
153
154 Ok(())
155 };
156 validate().with_context(|| input_err_msg(file_path))?;
157
158 Ok(model_file)
159 }
160}
161
162impl Model {
163 pub fn iter_years(&self) -> impl Iterator<Item = u32> + '_ {
165 self.parameters.milestone_years.iter().copied()
166 }
167
168 pub fn iter_regions(&self) -> impl Iterator<Item = &RegionID> + '_ {
170 self.regions.keys()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use std::fs::File;
178 use std::io::Write;
179 use tempfile::tempdir;
180
181 #[test]
182 fn test_check_milestone_years() {
183 assert!(check_milestone_years(&[1]).is_ok());
185 assert!(check_milestone_years(&[1, 2]).is_ok());
186
187 assert!(check_milestone_years(&[]).is_err());
189 assert!(check_milestone_years(&[1, 1]).is_err());
190 assert!(check_milestone_years(&[2, 1]).is_err());
191 }
192
193 #[test]
194 fn test_model_file_from_path() {
195 let dir = tempdir().unwrap();
196 {
197 let mut file = File::create(dir.path().join(MODEL_FILE_NAME)).unwrap();
198 writeln!(file, "milestone_years = [2020, 2100]").unwrap();
199 }
200
201 let model_file = ModelFile::from_path(dir.path()).unwrap();
202 assert_eq!(model_file.milestone_years, [2020, 2100]);
203 }
204}