1use crate::graph::investment::solve_investment_order_for_model;
3use crate::graph::validate::validate_commodity_graphs_for_model;
4use crate::graph::{CommoditiesGraph, build_commodity_graphs_for_model};
5use crate::id::{HasID, ID};
6use crate::model::{Model, ModelParameters};
7use crate::units::UnitType;
8use anyhow::{Context, Result, bail, ensure};
9use float_cmp::approx_eq;
10use indexmap::IndexMap;
11use itertools::Itertools;
12use serde::de::{Deserialize, DeserializeOwned, Deserializer};
13use std::collections::HashMap;
14use std::fmt::{self, Write};
15use std::fs;
16use std::hash::Hash;
17use std::path::Path;
18
19mod agent;
20use agent::read_agents;
21mod asset;
22use asset::read_user_assets;
23mod commodity;
24use commodity::read_commodities;
25mod process;
26use process::read_processes;
27mod region;
28use region::read_regions;
29mod time_slice;
30use time_slice::read_time_slice_info;
31mod range;
32use range::{parse_range, parse_range_parts};
33mod year;
34use year::parse_year_str;
35
36pub trait Insert<K, V> {
38 fn insert(&mut self, key: K, value: V) -> Option<V>;
40}
41
42impl<K: Eq + Hash, V> Insert<K, V> for HashMap<K, V> {
43 fn insert(&mut self, key: K, value: V) -> Option<V> {
44 HashMap::insert(self, key, value)
45 }
46}
47
48impl<K: Eq + Hash, V> Insert<K, V> for IndexMap<K, V> {
49 fn insert(&mut self, key: K, value: V) -> Option<V> {
50 IndexMap::insert(self, key, value)
51 }
52}
53
54pub fn read_csv<'a, T: DeserializeOwned + 'a>(
62 file_path: &'a Path,
63) -> Result<impl Iterator<Item = T> + 'a> {
64 let vec = read_csv_internal(file_path)?;
65 if vec.is_empty() {
66 bail!("CSV file {} cannot be empty", file_path.display());
67 }
68 Ok(vec.into_iter())
69}
70
71pub fn read_csv_optional<'a, T: DeserializeOwned + 'a>(
77 file_path: &'a Path,
78) -> Result<impl Iterator<Item = T> + 'a> {
79 if !file_path.exists() {
80 return Ok(Vec::new().into_iter());
81 }
82
83 let vec = read_csv_internal(file_path)?;
84 Ok(vec.into_iter())
85}
86
87fn read_csv_internal<'a, T: DeserializeOwned + 'a>(file_path: &'a Path) -> Result<Vec<T>> {
88 let vec = csv::ReaderBuilder::new()
89 .trim(csv::Trim::All)
90 .from_path(file_path)
91 .with_context(|| input_err_msg(file_path))?
92 .into_deserialize()
93 .process_results(|iter| iter.collect_vec())
94 .with_context(|| input_err_msg(file_path))?;
95
96 Ok(vec)
97}
98
99pub fn read_toml<T: DeserializeOwned>(file_path: &Path) -> Result<T> {
109 let toml_str = fs::read_to_string(file_path).with_context(|| input_err_msg(file_path))?;
110 let toml_data = toml::from_str(&toml_str).with_context(|| input_err_msg(file_path))?;
111 Ok(toml_data)
112}
113
114pub fn deserialise_proportion_nonzero<'de, D, T>(deserialiser: D) -> Result<T, D::Error>
116where
117 T: UnitType,
118 D: Deserializer<'de>,
119{
120 let value = f64::deserialize(deserialiser)?;
121 if !(value > 0.0 && value <= 1.0) {
122 Err(serde::de::Error::custom("Value must be > 0 and <= 1"))?;
123 }
124
125 Ok(T::new(value))
126}
127
128pub fn deserialise_finite_non_negative<'de, D, T>(deserialiser: D) -> Result<T, D::Error>
130where
131 T: UnitType,
132 D: Deserializer<'de>,
133{
134 let value = f64::deserialize(deserialiser)?;
135 if !(value.is_finite() && value >= 0.0) {
136 Err(serde::de::Error::custom(
137 "Value must be a finite number greater than or equal to zero",
138 ))?;
139 }
140
141 Ok(T::new(value))
142}
143
144pub fn input_err_msg<P: AsRef<Path>>(file_path: P) -> String {
146 format!("Error reading {}", file_path.as_ref().display())
147}
148
149fn read_csv_id_file<T, I: ID>(file_path: &Path) -> Result<IndexMap<I, T>>
154where
155 T: HasID<I> + DeserializeOwned,
156{
157 fn fill_and_validate_map<T, I: ID>(file_path: &Path) -> Result<IndexMap<I, T>>
158 where
159 T: HasID<I> + DeserializeOwned,
160 {
161 let mut map = IndexMap::new();
162 for record in read_csv::<T>(file_path)? {
163 let id = record.get_id().clone();
164 let existing = map.insert(id.clone(), record).is_some();
165 ensure!(!existing, "Duplicate ID found: {id}");
166 }
167 ensure!(!map.is_empty(), "CSV file is empty");
168
169 Ok(map)
170 }
171
172 fill_and_validate_map(file_path).with_context(|| input_err_msg(file_path))
173}
174
175fn check_values_sum_to_one_approx<I, T>(fractions: I) -> Result<()>
177where
178 T: UnitType,
179 I: Iterator<Item = T>,
180{
181 let sum = fractions.sum();
182 ensure!(
183 approx_eq!(T, sum, T::new(1.0), epsilon = 1e-5),
184 "Sum of fractions does not equal one (actual: {sum})"
185 );
186
187 Ok(())
188}
189
190pub fn is_sorted_and_unique<T, I>(iter: I) -> bool
192where
193 T: PartialOrd + Clone,
194 I: IntoIterator<Item = T>,
195{
196 is_sorted_and_unique_with(iter, |a, b| a < b)
197}
198
199pub fn is_sorted_and_unique_with<T, I, F>(iter: I, mut less_than: F) -> bool
202where
203 T: Clone,
204 I: IntoIterator<Item = T>,
205 F: FnMut(&T, &T) -> bool,
206{
207 iter.into_iter()
208 .tuple_windows()
209 .all(|(a, b)| less_than(&a, &b))
210}
211
212pub fn try_insert<M, K, V>(map: &mut M, key: &K, value: V) -> Result<()>
218where
219 M: Insert<K, V>,
220 K: Eq + Hash + Clone + std::fmt::Debug,
221{
222 let existing = map.insert(key.clone(), value).is_some();
223 ensure!(!existing, "Key {key:?} already exists in the map");
224 Ok(())
225}
226
227pub fn format_items_with_cap<I, J, T>(items: I) -> String
229where
230 I: IntoIterator<Item = T, IntoIter = J>,
231 J: ExactSizeIterator<Item = T>,
232 T: fmt::Debug,
233{
234 const MAX_DISPLAY: usize = 10;
235
236 let items = items.into_iter();
237 let total_count = items.len();
238
239 let formatted_items = items
241 .take(MAX_DISPLAY)
242 .format_with(", ", |items, f| f(&format_args!("{items:?}")));
243 let mut out = format!("[{formatted_items}]");
244
245 if total_count > MAX_DISPLAY {
247 write!(&mut out, " and {} more", total_count - MAX_DISPLAY).unwrap();
248 }
249
250 out
251}
252
253pub fn load_model<P: AsRef<Path>>(model_dir: P) -> Result<Model> {
263 let model_params = ModelParameters::from_path(&model_dir)?;
264
265 let time_slice_info = read_time_slice_info(model_dir.as_ref())?;
266 let regions = read_regions(model_dir.as_ref())?;
267 let region_ids = regions.keys().cloned().collect();
268 let years = &model_params.milestone_years;
269
270 let commodities = read_commodities(model_dir.as_ref(), ®ion_ids, &time_slice_info, years)?;
271 let processes = read_processes(
272 model_dir.as_ref(),
273 &commodities,
274 ®ion_ids,
275 &time_slice_info,
276 years,
277 )?;
278 let agents = read_agents(
279 model_dir.as_ref(),
280 &commodities,
281 &processes,
282 ®ion_ids,
283 years,
284 )?;
285 let agent_ids = agents.keys().cloned().collect();
286 let user_assets = read_user_assets(model_dir.as_ref(), &agent_ids, &processes, ®ion_ids)?;
287
288 let commodity_graphs = build_commodity_graphs_for_model(&processes, ®ion_ids, years);
290 validate_commodity_graphs_for_model(
291 &commodity_graphs,
292 &processes,
293 &commodities,
294 &time_slice_info,
295 )?;
296
297 let investment_order = solve_investment_order_for_model(&commodity_graphs, &commodities, years);
299
300 let model_path = model_dir
301 .as_ref()
302 .canonicalize()
303 .context("Could not parse path to model")?;
304 let model = Model {
305 model_path,
306 parameters: model_params,
307 agents,
308 commodities,
309 processes,
310 time_slice_info,
311 regions,
312 user_assets,
313 investment_order,
314 };
315 Ok(model)
316}
317
318pub fn load_commodity_graphs<P: AsRef<Path>>(
326 model_dir: P,
327) -> Result<IndexMap<u32, CommoditiesGraph>> {
328 let model_params = ModelParameters::from_path(&model_dir)?;
329
330 let time_slice_info = read_time_slice_info(model_dir.as_ref())?;
331 let regions = read_regions(model_dir.as_ref())?;
332 let region_ids = regions.keys().cloned().collect();
333 let years = &model_params.milestone_years;
334
335 let commodities = read_commodities(model_dir.as_ref(), ®ion_ids, &time_slice_info, years)?;
336 let processes = read_processes(
337 model_dir.as_ref(),
338 &commodities,
339 ®ion_ids,
340 &time_slice_info,
341 years,
342 )?;
343
344 let commodity_graphs = build_commodity_graphs_for_model(&processes, ®ion_ids, years);
345 Ok(commodity_graphs)
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::id::GenericID;
352 use crate::units::Dimensionless;
353 use rstest::rstest;
354 use serde::Deserialize;
355 use serde::de::IntoDeserializer;
356 use serde::de::value::{Error as ValueError, F64Deserializer};
357 use std::fs::File;
358 use std::io::Write;
359 use std::path::PathBuf;
360 use tempfile::tempdir;
361
362 #[derive(Debug, PartialEq, Deserialize)]
363 struct Record {
364 id: GenericID,
365 value: u32,
366 }
367
368 impl HasID<GenericID> for Record {
369 fn get_id(&self) -> &GenericID {
370 &self.id
371 }
372 }
373
374 fn create_csv_file(dir_path: &Path, contents: &str) -> PathBuf {
376 let file_path = dir_path.join("test.csv");
377 let mut file = File::create(&file_path).unwrap();
378 writeln!(file, "{contents}").unwrap();
379 file_path
380 }
381
382 #[test]
384 fn read_csv_works() {
385 let dir = tempdir().unwrap();
386 let file_path = create_csv_file(dir.path(), "id,value\nhello,1\nworld,2\n");
387 let records: Vec<Record> = read_csv(&file_path).unwrap().collect();
388 assert_eq!(
389 records,
390 &[
391 Record {
392 id: "hello".into(),
393 value: 1,
394 },
395 Record {
396 id: "world".into(),
397 value: 2,
398 }
399 ]
400 );
401
402 let dir = tempdir().unwrap();
404 let file_path = create_csv_file(dir.path(), "id , value\t\n hello\t ,1\n world ,2\n");
405 let records: Vec<Record> = read_csv(&file_path).unwrap().collect();
406 assert_eq!(
407 records,
408 &[
409 Record {
410 id: "hello".into(),
411 value: 1,
412 },
413 Record {
414 id: "world".into(),
415 value: 2,
416 }
417 ]
418 );
419
420 let file_path = create_csv_file(dir.path(), "id,value\n");
422 assert!(read_csv::<Record>(&file_path).is_err());
423 assert!(
424 read_csv_optional::<Record>(&file_path)
425 .unwrap()
426 .next()
427 .is_none()
428 );
429
430 let dir = tempdir().unwrap();
432 let file_path = dir.path().join("a_missing_file.csv");
433 assert!(!file_path.exists());
434 assert!(read_csv::<Record>(&file_path).is_err());
435 assert!(
437 read_csv_optional::<Record>(&file_path)
438 .unwrap()
439 .next()
440 .is_none()
441 );
442 }
443
444 #[test]
445 fn read_toml_works() {
446 let dir = tempdir().unwrap();
447 let file_path = dir.path().join("test.toml");
448 {
449 let mut file = File::create(&file_path).unwrap();
450 writeln!(file, "id = \"hello\"\nvalue = 1").unwrap();
451 }
452
453 assert_eq!(
454 read_toml::<Record>(&file_path).unwrap(),
455 Record {
456 id: "hello".into(),
457 value: 1,
458 }
459 );
460
461 {
462 let mut file = File::create(&file_path).unwrap();
463 writeln!(file, "bad toml syntax").unwrap();
464 }
465
466 read_toml::<Record>(&file_path).unwrap_err();
467 }
468
469 fn deserialise_f64(value: f64) -> Result<Dimensionless, ValueError> {
471 let deserialiser: F64Deserializer<ValueError> = value.into_deserializer();
472 deserialise_proportion_nonzero(deserialiser)
473 }
474
475 #[test]
476 fn deserialise_proportion_nonzero_works() {
477 assert_eq!(deserialise_f64(0.01), Ok(Dimensionless(0.01)));
479 assert_eq!(deserialise_f64(0.5), Ok(Dimensionless(0.5)));
480 assert_eq!(deserialise_f64(1.0), Ok(Dimensionless(1.0)));
481
482 deserialise_f64(0.0).unwrap_err();
484 deserialise_f64(-1.0).unwrap_err();
485 deserialise_f64(2.0).unwrap_err();
486 deserialise_f64(f64::NAN).unwrap_err();
487 deserialise_f64(f64::INFINITY).unwrap_err();
488 }
489
490 #[rstest]
491 #[case(0.0, true)] #[case(1e-10, true)] #[case(f64::MAX, true)] #[case(-1e-10, false)] #[case(-1.0, false)] #[case(f64::NAN, false)] #[case(f64::INFINITY, false)] #[case(f64::NEG_INFINITY, false)] fn deserialise_finite_non_negative_works(#[case] value: f64, #[case] expected_valid: bool) {
500 let deserialiser: F64Deserializer<ValueError> = value.into_deserializer();
501 let result = deserialise_finite_non_negative(deserialiser);
502 if expected_valid {
503 assert_eq!(result, Ok(Dimensionless(value)));
504 } else {
505 result.unwrap_err();
506 }
507 }
508
509 #[test]
510 fn check_values_sum_to_one_approx_works() {
511 check_values_sum_to_one_approx([Dimensionless(1.0)].into_iter()).unwrap();
513
514 check_values_sum_to_one_approx([Dimensionless(0.4), Dimensionless(0.6)].into_iter())
516 .unwrap();
517
518 assert!(check_values_sum_to_one_approx([Dimensionless(0.5)].into_iter()).is_err());
520
521 assert!(
523 check_values_sum_to_one_approx([Dimensionless(0.4), Dimensionless(0.3)].into_iter())
524 .is_err()
525 );
526
527 assert!(
529 check_values_sum_to_one_approx([Dimensionless(f64::INFINITY)].into_iter()).is_err()
530 );
531 assert!(check_values_sum_to_one_approx([Dimensionless(f64::NAN)].into_iter()).is_err());
532 }
533
534 #[rstest]
535 #[case(&[], true)]
536 #[case(&[1], true)]
537 #[case(&[1,2], true)]
538 #[case(&[1,2,3,4], true)]
539 #[case(&[2,1],false)]
540 #[case(&[1,1],false)]
541 #[case(&[1,3,2,4], false)]
542 fn is_sorted_and_unique_works(#[case] values: &[u32], #[case] expected: bool) {
543 assert_eq!(is_sorted_and_unique(values), expected);
544 }
545
546 #[test]
547 fn format_items_with_cap_works() {
548 let items = vec!["a", "b", "c"];
549 assert_eq!(format_items_with_cap(&items), r#"["a", "b", "c"]"#);
550
551 let many_items = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"];
553 assert_eq!(
554 format_items_with_cap(&many_items),
555 r#"["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] and 2 more"#
556 );
557 }
558}