muse2/
model.rs

1//! The model represents the static input data provided by the user.
2use crate::agent::AgentMap;
3use crate::commodity::CommodityMap;
4use crate::input::{input_err_msg, is_sorted_and_unique, read_toml};
5use crate::process::ProcessMap;
6use crate::region::{RegionID, RegionMap};
7use crate::time_slice::TimeSliceInfo;
8use anyhow::{ensure, Context, Result};
9use serde::Deserialize;
10use std::path::{Path, PathBuf};
11
12const MODEL_FILE_NAME: &str = "model.toml";
13
14/// Model definition
15pub struct Model {
16    /// Path to model folder
17    pub model_path: PathBuf,
18    /// Milestone years for the simulation. Sorted.
19    pub milestone_years: Vec<u32>,
20    /// Agents for the simulation
21    pub agents: AgentMap,
22    /// Commodities for the simulation
23    pub commodities: CommodityMap,
24    /// Processes for the simulation
25    pub processes: ProcessMap,
26    /// Information about seasons and time slices
27    pub time_slice_info: TimeSliceInfo,
28    /// Regions for the simulation
29    pub regions: RegionMap,
30}
31
32/// Represents the contents of the entire model file.
33#[derive(Debug, Deserialize, PartialEq)]
34pub struct ModelFile {
35    /// Milestone years section of model file
36    pub milestone_years: MilestoneYears,
37}
38
39/// Represents the "milestone_years" section of the model file.
40#[derive(Debug, Deserialize, PartialEq)]
41pub struct MilestoneYears {
42    /// Milestone years
43    pub years: Vec<u32>,
44}
45
46/// Check that the milestone years parameter is valid
47///
48/// # Arguments
49///
50/// * `years` - Integer list of milestone years
51///
52/// # Returns
53///
54/// An error if the milestone years are invalid
55fn check_milestone_years(years: &[u32]) -> Result<()> {
56    ensure!(!years.is_empty(), "`milestone_years` is empty");
57
58    ensure!(
59        is_sorted_and_unique(years),
60        "`milestone_years` must be composed of unique values in order"
61    );
62
63    Ok(())
64}
65
66impl ModelFile {
67    /// Read a model file from the specified directory.
68    ///
69    /// # Arguments
70    ///
71    /// * `model_dir` - Folder containing model configuration files
72    ///
73    /// # Returns
74    ///
75    /// The model file contents as a `ModelFile` struct or an error if the file is invalid
76    pub fn from_path<P: AsRef<Path>>(model_dir: P) -> Result<ModelFile> {
77        let file_path = model_dir.as_ref().join(MODEL_FILE_NAME);
78        let model_file: ModelFile = read_toml(&file_path)?;
79        check_milestone_years(&model_file.milestone_years.years)
80            .with_context(|| input_err_msg(file_path))?;
81
82        Ok(model_file)
83    }
84}
85
86impl Model {
87    /// Iterate over the model's milestone years.
88    pub fn iter_years(&self) -> impl Iterator<Item = u32> + '_ {
89        self.milestone_years.iter().copied()
90    }
91
92    /// Iterate over the model's regions (region IDs).
93    pub fn iter_regions(&self) -> impl Iterator<Item = &RegionID> + '_ {
94        self.regions.keys()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::fs::File;
102    use std::io::Write;
103    use tempfile::tempdir;
104
105    #[test]
106    fn test_check_milestone_years() {
107        // Valid
108        assert!(check_milestone_years(&[1]).is_ok());
109        assert!(check_milestone_years(&[1, 2]).is_ok());
110
111        // Invalid
112        assert!(check_milestone_years(&[]).is_err());
113        assert!(check_milestone_years(&[1, 1]).is_err());
114        assert!(check_milestone_years(&[2, 1]).is_err());
115    }
116
117    #[test]
118    fn test_model_file_from_path() {
119        let dir = tempdir().unwrap();
120        {
121            let mut file = File::create(dir.path().join(MODEL_FILE_NAME)).unwrap();
122            writeln!(file, "[milestone_years]\nyears = [2020, 2100]").unwrap();
123        }
124
125        let model_file = ModelFile::from_path(dir.path()).unwrap();
126        assert_eq!(model_file.milestone_years.years, [2020, 2100]);
127    }
128}