From 5c99247ee8f212a9a75761121917f643e15f6639 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Wed, 18 Dec 2024 17:00:49 +0000 Subject: [PATCH] Move time slice CSV code to input submodule Closes #288. --- src/input.rs | 2 + src/input/time_slice.rs | 190 ++++++++++++++++++++++++++++++++++++++++ src/model.rs | 2 +- src/time_slice.rs | 169 +---------------------------------- 4 files changed, 195 insertions(+), 168 deletions(-) create mode 100644 src/input/time_slice.rs diff --git a/src/input.rs b/src/input.rs index 49ab9d2f8..2a8d403eb 100644 --- a/src/input.rs +++ b/src/input.rs @@ -16,6 +16,8 @@ pub mod process; pub use process::read_processes; pub mod region; pub use region::read_regions; +mod time_slice; +pub use time_slice::read_time_slice_info; /// Read a series of type `T`s from a CSV file. /// diff --git a/src/input/time_slice.rs b/src/input/time_slice.rs new file mode 100644 index 000000000..b0868eb43 --- /dev/null +++ b/src/input/time_slice.rs @@ -0,0 +1,190 @@ +//! Code for reading in time slice info from a CSV file. +#![allow(missing_docs)] +use crate::input::*; +use anyhow::{ensure, Context, Result}; +use serde::Deserialize; +use serde_string_enum::DeserializeLabeledStringEnum; +use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::rc::Rc; + +use crate::time_slice::{TimeSliceID, TimeSliceInfo}; + +const TIME_SLICES_FILE_NAME: &str = "time_slices.csv"; + +/// A time slice record retrieved from a CSV file +#[derive(PartialEq, Debug, Deserialize)] +struct TimeSliceRaw { + season: String, + time_of_day: String, + #[serde(deserialize_with = "deserialise_proportion_nonzero")] + fraction: f64, +} + +/// Get the specified `String` from `set` or insert if it doesn't exist +fn get_or_insert(value: String, set: &mut HashSet>) -> Rc { + // Sadly there's no entry API for HashSets: https://github.com/rust-lang/rfcs/issues/1490 + match set.get(value.as_str()) { + Some(value) => Rc::clone(value), + None => { + let value = Rc::from(value); + set.insert(Rc::clone(&value)); + value + } + } +} + +/// Read time slice information from an iterator of raw time slice records +fn read_time_slice_info_from_iter(iter: I) -> Result +where + I: Iterator, +{ + let mut seasons: HashSet> = HashSet::new(); + let mut times_of_day = HashSet::new(); + let mut fractions = HashMap::new(); + for time_slice in iter { + let season = get_or_insert(time_slice.season, &mut seasons); + let time_of_day = get_or_insert(time_slice.time_of_day, &mut times_of_day); + let id = TimeSliceID { + season, + time_of_day, + }; + + ensure!( + fractions.insert(id.clone(), time_slice.fraction).is_none(), + "Duplicate time slice entry for {id}", + ); + } + + // Validate data + check_fractions_sum_to_one(fractions.values().cloned()) + .context("Invalid time slice fractions")?; + + Ok(TimeSliceInfo { + seasons, + times_of_day, + fractions, + }) +} + +/// Refers to a particular aspect of a time slice +#[derive(PartialEq, Debug, DeserializeLabeledStringEnum)] +pub enum TimeSliceLevel { + #[string = "annual"] + Annual, + #[string = "season"] + Season, + #[string = "daynight"] + DayNight, +} + +/// Read time slices from a CSV file. +/// +/// # Arguments +/// +/// * `model_dir` - Folder containing model configuration files +/// +/// # Returns +/// +/// This function returns a `TimeSliceInfo` struct or, if the file doesn't exist, a single time +/// slice covering the whole year (see `TimeSliceInfo::default()`). +pub fn read_time_slice_info(model_dir: &Path) -> Result { + let file_path = model_dir.join(TIME_SLICES_FILE_NAME); + if !file_path.exists() { + return Ok(TimeSliceInfo::default()); + } + + let time_slices_csv = read_csv(&file_path)?; + read_time_slice_info_from_iter(time_slices_csv).with_context(|| input_err_msg(file_path)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use std::path::Path; + use tempfile::tempdir; + + /// Create an example time slices file in dir_path + fn create_time_slices_file(dir_path: &Path) { + let file_path = dir_path.join(TIME_SLICES_FILE_NAME); + let mut file = File::create(file_path).unwrap(); + writeln!( + file, + "season,time_of_day,fraction +winter,day,0.25 +peak,night,0.25 +summer,peak,0.25 +autumn,evening,0.25" + ) + .unwrap(); + } + + #[test] + fn test_read_time_slice_info() { + let dir = tempdir().unwrap(); + create_time_slices_file(dir.path()); + + let info = read_time_slice_info(dir.path()).unwrap(); + assert_eq!( + info, + TimeSliceInfo { + seasons: [ + "winter".into(), + "peak".into(), + "summer".into(), + "autumn".into() + ] + .into_iter() + .collect(), + times_of_day: [ + "day".into(), + "night".into(), + "peak".into(), + "evening".into() + ] + .into_iter() + .collect(), + fractions: [ + ( + TimeSliceID { + season: "winter".into(), + time_of_day: "day".into(), + }, + 0.25, + ), + ( + TimeSliceID { + season: "peak".into(), + time_of_day: "night".into(), + }, + 0.25, + ), + ( + TimeSliceID { + season: "summer".into(), + time_of_day: "peak".into(), + }, + 0.25, + ), + ( + TimeSliceID { + season: "autumn".into(), + time_of_day: "evening".into(), + }, + 0.25, + ), + ] + .into_iter() + .collect() + } + ); + } + + #[test] + fn test_read_time_slice_info_non_existent() { + let actual = read_time_slice_info(tempdir().unwrap().path()); + assert_eq!(actual.unwrap(), TimeSliceInfo::default()); + } +} diff --git a/src/model.rs b/src/model.rs index 24b36bf86..c042e8bf1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -5,7 +5,7 @@ use crate::commodity::Commodity; use crate::input::*; use crate::process::Process; use crate::region::Region; -use crate::time_slice::{read_time_slice_info, TimeSliceInfo}; +use crate::time_slice::TimeSliceInfo; use anyhow::{ensure, Context, Result}; use serde::Deserialize; use std::collections::HashMap; diff --git a/src/time_slice.rs b/src/time_slice.rs index e0155a854..c55288566 100644 --- a/src/time_slice.rs +++ b/src/time_slice.rs @@ -1,21 +1,17 @@ -//! Code for reading and working with time slices. +//! Code for working with time slices. //! //! Time slices provide a mechanism for users to indicate production etc. varies with the time of //! day and time of year. #![allow(missing_docs)] use crate::input::*; -use anyhow::{ensure, Context, Result}; +use anyhow::{Context, Result}; use itertools::Itertools; -use serde::Deserialize; use serde_string_enum::DeserializeLabeledStringEnum; use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::iter; -use std::path::Path; use std::rc::Rc; -const TIME_SLICES_FILE_NAME: &str = "time_slices.csv"; - /// An ID describing season and time of day #[derive(Hash, Eq, PartialEq, Clone, Debug)] pub struct TimeSliceID { @@ -197,61 +193,6 @@ impl TimeSliceInfo { } } -/// A time slice record retrieved from a CSV file -#[derive(PartialEq, Debug, Deserialize)] -struct TimeSliceRaw { - season: String, - time_of_day: String, - #[serde(deserialize_with = "deserialise_proportion_nonzero")] - fraction: f64, -} - -/// Get the specified `String` from `set` or insert if it doesn't exist -fn get_or_insert(value: String, set: &mut HashSet>) -> Rc { - // Sadly there's no entry API for HashSets: https://github.com/rust-lang/rfcs/issues/1490 - match set.get(value.as_str()) { - Some(value) => Rc::clone(value), - None => { - let value = Rc::from(value); - set.insert(Rc::clone(&value)); - value - } - } -} - -/// Read time slice information from an iterator of raw time slice records -fn read_time_slice_info_from_iter(iter: I) -> Result -where - I: Iterator, -{ - let mut seasons: HashSet> = HashSet::new(); - let mut times_of_day = HashSet::new(); - let mut fractions = HashMap::new(); - for time_slice in iter { - let season = get_or_insert(time_slice.season, &mut seasons); - let time_of_day = get_or_insert(time_slice.time_of_day, &mut times_of_day); - let id = TimeSliceID { - season, - time_of_day, - }; - - ensure!( - fractions.insert(id.clone(), time_slice.fraction).is_none(), - "Duplicate time slice entry for {id}", - ); - } - - // Validate data - check_fractions_sum_to_one(fractions.values().cloned()) - .context("Invalid time slice fractions")?; - - Ok(TimeSliceInfo { - seasons, - times_of_day, - fractions, - }) -} - /// Refers to a particular aspect of a time slice #[derive(PartialEq, Debug, DeserializeLabeledStringEnum)] pub enum TimeSliceLevel { @@ -263,116 +204,10 @@ pub enum TimeSliceLevel { DayNight, } -/// Read time slices from a CSV file. -/// -/// # Arguments -/// -/// * `model_dir` - Folder containing model configuration files -/// -/// # Returns -/// -/// This function returns a `TimeSliceInfo` struct or, if the file doesn't exist, a single time -/// slice covering the whole year (see `TimeSliceInfo::default()`). -pub fn read_time_slice_info(model_dir: &Path) -> Result { - let file_path = model_dir.join(TIME_SLICES_FILE_NAME); - if !file_path.exists() { - return Ok(TimeSliceInfo::default()); - } - - let time_slices_csv = read_csv(&file_path)?; - read_time_slice_info_from_iter(time_slices_csv).with_context(|| input_err_msg(file_path)) -} - #[cfg(test)] mod tests { use super::*; use float_cmp::assert_approx_eq; - use std::fs::File; - use std::io::Write; - use std::path::Path; - use tempfile::tempdir; - - /// Create an example time slices file in dir_path - fn create_time_slices_file(dir_path: &Path) { - let file_path = dir_path.join(TIME_SLICES_FILE_NAME); - let mut file = File::create(file_path).unwrap(); - writeln!( - file, - "season,time_of_day,fraction -winter,day,0.25 -peak,night,0.25 -summer,peak,0.25 -autumn,evening,0.25" - ) - .unwrap(); - } - - #[test] - fn test_read_time_slice_info() { - let dir = tempdir().unwrap(); - create_time_slices_file(dir.path()); - - let info = read_time_slice_info(dir.path()).unwrap(); - assert_eq!( - info, - TimeSliceInfo { - seasons: [ - "winter".into(), - "peak".into(), - "summer".into(), - "autumn".into() - ] - .into_iter() - .collect(), - times_of_day: [ - "day".into(), - "night".into(), - "peak".into(), - "evening".into() - ] - .into_iter() - .collect(), - fractions: [ - ( - TimeSliceID { - season: "winter".into(), - time_of_day: "day".into(), - }, - 0.25, - ), - ( - TimeSliceID { - season: "peak".into(), - time_of_day: "night".into(), - }, - 0.25, - ), - ( - TimeSliceID { - season: "summer".into(), - time_of_day: "peak".into(), - }, - 0.25, - ), - ( - TimeSliceID { - season: "autumn".into(), - time_of_day: "evening".into(), - }, - 0.25, - ), - ] - .into_iter() - .collect() - } - ); - } - - #[test] - fn test_read_time_slice_info_non_existent() { - let actual = read_time_slice_info(tempdir().unwrap().path()); - assert_eq!(actual.unwrap(), TimeSliceInfo::default()); - } #[test] fn test_iter_selection() {