1use crate::agent::AgentID;
3use crate::asset::{Asset, AssetID, AssetRef};
4use crate::commodity::CommodityID;
5use crate::process::ProcessID;
6use crate::region::RegionID;
7use crate::simulation::investment::appraisal::AppraisalOutput;
8use crate::simulation::optimisation::{FlowMap, Solution};
9use crate::simulation::prices::PriceMap;
10use crate::time_slice::TimeSliceID;
11use crate::units::{Activity, Capacity, Flow, Money, MoneyPerActivity, MoneyPerFlow};
12use anyhow::{Context, Result, ensure};
13use csv;
14use indexmap::IndexMap;
15use serde::{Deserialize, Serialize};
16use std::fs;
17use std::fs::File;
18use std::path::{Path, PathBuf};
19
20pub mod metadata;
21use metadata::write_metadata;
22
23const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv";
25
26const COMMODITY_PRICES_FILE_NAME: &str = "commodity_prices.csv";
28
29const ASSETS_FILE_NAME: &str = "assets.csv";
31
32const ASSET_CAPACITIES_FILE_NAME: &str = "asset_capacities.csv";
34
35const ACTIVITY_ASSET_DISPATCH: &str = "debug_dispatch_assets.csv";
37
38const COMMODITY_BALANCE_DUALS_FILE_NAME: &str = "debug_commodity_balance_duals.csv";
40
41const UNMET_DEMAND_FILE_NAME: &str = "debug_unmet_demand.csv";
43
44const SOLVER_VALUES_FILE_NAME: &str = "debug_solver.csv";
46
47const APPRAISAL_RESULTS_FILE_NAME: &str = "debug_appraisal_results.csv";
49
50const APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME: &str = "debug_appraisal_results_time_slices.csv";
52
53pub fn get_output_dir(model_dir: &Path, results_root: PathBuf) -> Result<PathBuf> {
55 let model_dir = model_dir
58 .canonicalize() .context("Could not resolve path to model")?;
60
61 let model_name = model_dir
62 .file_name()
63 .context("Model cannot be in root folder")?
64 .to_str()
65 .context("Invalid chars in model dir name")?;
66
67 Ok([results_root, model_name.into()].iter().collect())
69}
70
71pub fn get_graphs_dir(model_dir: &Path, graph_results_root: PathBuf) -> Result<PathBuf> {
73 let model_dir = model_dir
74 .canonicalize() .context("Could not resolve path to model")?;
76 let model_name = model_dir
77 .file_name()
78 .context("Model cannot be in root folder")?
79 .to_str()
80 .context("Invalid chars in model dir name")?;
81 Ok([graph_results_root, model_name.into()].iter().collect())
82}
83
84pub fn create_output_directory(output_dir: &Path, allow_overwrite: bool) -> Result<bool> {
95 let overwrite = if let Ok(mut it) = fs::read_dir(output_dir) {
97 if it.next().is_none() {
98 return Ok(false);
100 }
101
102 ensure!(
103 allow_overwrite,
104 "Output folder already exists and is not empty. \
105 Please delete the folder or pass the --overwrite command-line option."
106 );
107
108 fs::remove_dir_all(output_dir).context("Could not delete folder")?;
109 true
110 } else {
111 false
112 };
113
114 fs::create_dir_all(output_dir)?;
116
117 Ok(overwrite)
118}
119
120pub fn copy_input_files(model_dir: &Path, output_dir: &Path, model_name: &str) -> Result<()> {
122 let mut input_copy_dir = output_dir.to_path_buf();
124 input_copy_dir.extend(["input", model_name]);
125
126 fs::create_dir_all(&input_copy_dir).context("Could not create input copy directory")?;
127
128 for entry in fs::read_dir(model_dir)? {
129 let entry = entry?;
130 let path = entry.path();
131 if path.is_file() {
132 let file_name = path.file_name().unwrap();
133 fs::copy(&path, input_copy_dir.join(file_name))?;
134 }
135 }
136 Ok(())
137}
138
139#[derive(Serialize, Deserialize, Debug, PartialEq)]
141struct AssetRow {
142 asset_id: AssetID,
143 process_id: ProcessID,
144 region_id: RegionID,
145 agent_id: AgentID,
146 commission_year: u32,
147}
148
149impl AssetRow {
150 fn new(asset: &Asset) -> Self {
152 Self {
153 asset_id: asset.id().unwrap(),
154 process_id: asset.process_id().clone(),
155 region_id: asset.region_id().clone(),
156 agent_id: asset.agent_id().unwrap().clone(),
157 commission_year: asset.commission_year(),
158 }
159 }
160}
161
162#[derive(Serialize, Deserialize, Debug, PartialEq)]
164struct AssetCapacityRow {
165 milestone_year: u32,
166 asset_id: AssetID,
167 capacity: Capacity,
168 num_tranches: u32,
169 mothballed_capacity: Capacity,
170 mothballed_tranches: u32,
171}
172
173#[derive(Serialize, Deserialize, Debug, PartialEq)]
175struct CommodityFlowRow {
176 milestone_year: u32,
177 asset_id: AssetID,
178 commodity_id: CommodityID,
179 time_slice: TimeSliceID,
180 flow: Flow,
181}
182
183#[derive(Serialize, Deserialize, Debug, PartialEq)]
185struct CommodityPriceRow {
186 milestone_year: u32,
187 commodity_id: CommodityID,
188 region_id: RegionID,
189 time_slice: TimeSliceID,
190 price: MoneyPerFlow,
191}
192
193#[derive(Serialize, Deserialize, Debug, PartialEq)]
195struct DispatchRow {
196 milestone_year: u32,
197 run_description: String,
198 asset_id: Option<AssetID>,
199 process_id: ProcessID,
200 region_id: RegionID,
201 time_slice: TimeSliceID,
202 activity: Option<Activity>,
203 activity_dual: Option<MoneyPerActivity>,
204 column_dual: Option<MoneyPerActivity>,
205}
206
207#[derive(Serialize, Deserialize, Debug, PartialEq)]
209struct CommodityBalanceDualsRow {
210 milestone_year: u32,
211 run_description: String,
212 commodity_id: CommodityID,
213 region_id: RegionID,
214 time_slice: TimeSliceID,
215 value: MoneyPerFlow,
216}
217
218#[derive(Serialize, Deserialize, Debug, PartialEq)]
220struct UnmetDemandRow {
221 milestone_year: u32,
222 run_description: String,
223 commodity_id: CommodityID,
224 region_id: RegionID,
225 time_slice: TimeSliceID,
226 value: Flow,
227}
228
229#[derive(Serialize, Deserialize, Debug, PartialEq)]
231struct SolverValuesRow {
232 milestone_year: u32,
233 run_description: String,
234 objective_value: Money,
235}
236
237#[derive(Serialize, Deserialize, Debug, PartialEq)]
239struct AppraisalResultsRow {
240 milestone_year: u32,
241 run_description: String,
242 asset_id: Option<AssetID>,
243 process_id: ProcessID,
244 region_id: RegionID,
245 capacity: Capacity,
246 metric: Option<f64>,
247}
248
249#[derive(Serialize, Deserialize, Debug, PartialEq)]
251struct AppraisalResultsTimeSliceRow {
252 milestone_year: u32,
253 run_description: String,
254 asset_id: Option<AssetID>,
255 process_id: ProcessID,
256 region_id: RegionID,
257 time_slice: TimeSliceID,
258 activity: Activity,
259 activity_coefficient: MoneyPerActivity,
260 demand: Flow,
261 unmet_demand: Flow,
262}
263
264struct DebugDataWriter {
266 context: Option<String>,
267 unmet_demand_file_path: PathBuf,
268 commodity_balance_duals_writer: csv::Writer<File>,
269 unmet_demand_writer: Option<csv::Writer<File>>,
270 solver_values_writer: csv::Writer<File>,
271 appraisal_results_writer: csv::Writer<File>,
272 appraisal_results_time_slice_writer: csv::Writer<File>,
273 dispatch_asset_writer: csv::Writer<File>,
274}
275
276impl DebugDataWriter {
277 fn create(output_path: &Path) -> Result<Self> {
283 let new_writer = |file_name| {
284 let file_path = output_path.join(file_name);
285 csv::Writer::from_path(file_path)
286 };
287
288 Ok(Self {
289 context: None,
290 unmet_demand_file_path: output_path.join(UNMET_DEMAND_FILE_NAME),
291 commodity_balance_duals_writer: new_writer(COMMODITY_BALANCE_DUALS_FILE_NAME)?,
292 unmet_demand_writer: None,
293 solver_values_writer: new_writer(SOLVER_VALUES_FILE_NAME)?,
294 appraisal_results_writer: new_writer(APPRAISAL_RESULTS_FILE_NAME)?,
295 appraisal_results_time_slice_writer: new_writer(
296 APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME,
297 )?,
298 dispatch_asset_writer: new_writer(ACTIVITY_ASSET_DISPATCH)?,
299 })
300 }
301
302 fn with_context(&self, run_description: &str) -> String {
304 if let Some(context) = &self.context {
305 format!("{context}; {run_description}")
306 } else {
307 run_description.to_string()
308 }
309 }
310
311 fn write_dispatch_debug_info(
313 &mut self,
314 milestone_year: u32,
315 run_description: &str,
316 solution: &Solution,
317 ) -> Result<()> {
318 self.write_dispatch(
319 milestone_year,
320 run_description,
321 solution.iter_activity(),
322 solution.iter_activity_duals(),
323 solution.iter_column_duals(),
324 )?;
325 self.write_commodity_balance_duals(
326 milestone_year,
327 run_description,
328 solution.iter_commodity_balance_duals(),
329 )?;
330 self.write_unmet_demand(
331 milestone_year,
332 run_description,
333 solution.iter_unmet_demand(),
334 )?;
335 self.write_solver_values(milestone_year, run_description, solution.objective_value)?;
336 Ok(())
337 }
338
339 fn write_dispatch<'a, I, J, K>(
341 &mut self,
342 milestone_year: u32,
343 run_description: &str,
344 iter_activity: I,
345 iter_activity_duals: J,
346 iter_column_duals: K,
347 ) -> Result<()>
348 where
349 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
350 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
351 K: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
352 {
353 type CompiledActivityData = (
355 Option<Activity>,
356 Option<MoneyPerActivity>,
357 Option<MoneyPerActivity>,
358 );
359 let mut map: IndexMap<(&AssetRef, &TimeSliceID), CompiledActivityData> = IndexMap::new();
360
361 for (asset, time_slice, activity) in iter_activity {
363 map.entry((asset, time_slice)).or_default().0 = Some(activity);
364 }
365 for (asset, time_slice, activity_dual) in iter_activity_duals {
367 map.entry((asset, time_slice)).or_default().1 = Some(activity_dual);
368 }
369 for (asset, time_slice, column_dual) in iter_column_duals {
371 map.entry((asset, time_slice)).or_default().2 = Some(column_dual);
372 }
373
374 for ((asset, time_slice), (activity, activity_dual, column_dual)) in map {
375 let row = DispatchRow {
376 milestone_year,
377 run_description: self.with_context(run_description),
378 asset_id: asset.id(),
379 process_id: asset.process_id().clone(),
380 region_id: asset.region_id().clone(),
381 time_slice: time_slice.clone(),
382 activity,
383 activity_dual,
384 column_dual,
385 };
386 self.dispatch_asset_writer.serialize(row)?;
387 }
388
389 Ok(())
390 }
391
392 fn write_commodity_balance_duals<'a, I>(
394 &mut self,
395 milestone_year: u32,
396 run_description: &str,
397 iter: I,
398 ) -> Result<()>
399 where
400 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
401 {
402 for (commodity_id, region_id, time_slice, value) in iter {
403 let row = CommodityBalanceDualsRow {
404 milestone_year,
405 run_description: self.with_context(run_description),
406 commodity_id: commodity_id.clone(),
407 region_id: region_id.clone(),
408 time_slice: time_slice.clone(),
409 value,
410 };
411 self.commodity_balance_duals_writer.serialize(row)?;
412 }
413
414 Ok(())
415 }
416
417 fn write_unmet_demand<'a, I>(
419 &mut self,
420 milestone_year: u32,
421 run_description: &str,
422 iter: I,
423 ) -> Result<()>
424 where
425 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, Flow)>,
426 {
427 let mut rows = iter.peekable();
428
429 if rows.peek().is_none() {
430 return Ok(());
431 }
432
433 assert!(
435 self.unmet_demand_writer.is_none(),
436 "Unmet demand file already exists!"
437 );
438
439 let run_description = self.with_context(run_description);
440 let writer = self
441 .unmet_demand_writer
442 .insert(csv::Writer::from_path(&self.unmet_demand_file_path)?);
443 for (commodity_id, region_id, time_slice, value) in rows {
444 let row = UnmetDemandRow {
445 milestone_year,
446 run_description: run_description.clone(),
447 commodity_id: commodity_id.clone(),
448 region_id: region_id.clone(),
449 time_slice: time_slice.clone(),
450 value,
451 };
452 writer.serialize(row)?;
453 }
454
455 Ok(())
456 }
457
458 fn write_solver_values(
460 &mut self,
461 milestone_year: u32,
462 run_description: &str,
463 objective_value: Money,
464 ) -> Result<()> {
465 let row = SolverValuesRow {
466 milestone_year,
467 run_description: self.with_context(run_description),
468 objective_value,
469 };
470 self.solver_values_writer.serialize(row)?;
471 self.solver_values_writer.flush()?;
472
473 Ok(())
474 }
475
476 fn write_appraisal_results(
478 &mut self,
479 milestone_year: u32,
480 run_description: &str,
481 appraisal_results: &[AppraisalOutput],
482 ) -> Result<()> {
483 for result in appraisal_results {
484 let row = AppraisalResultsRow {
485 milestone_year,
486 run_description: self.with_context(run_description),
487 asset_id: result.asset.id(),
488 process_id: result.asset.process_id().clone(),
489 region_id: result.asset.region_id().clone(),
490 capacity: result.asset.total_capacity(),
491 metric: result.metric.as_ref().map(|m| m.value()),
492 };
493 self.appraisal_results_writer.serialize(row)?;
494 }
495
496 Ok(())
497 }
498
499 fn write_appraisal_time_slice_results(
501 &mut self,
502 milestone_year: u32,
503 run_description: &str,
504 appraisal_results: &[AppraisalOutput],
505 demand: &IndexMap<TimeSliceID, Flow>,
506 ) -> Result<()> {
507 for result in appraisal_results {
508 for (time_slice, activity) in &result.activity {
509 let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
510 let demand = demand[time_slice];
511 let unmet_demand = result.unmet_demand[time_slice];
512 let row = AppraisalResultsTimeSliceRow {
513 milestone_year,
514 run_description: self.with_context(run_description),
515 asset_id: result.asset.id(),
516 process_id: result.asset.process_id().clone(),
517 region_id: result.asset.region_id().clone(),
518 time_slice: time_slice.clone(),
519 activity: *activity,
520 activity_coefficient,
521 demand,
522 unmet_demand,
523 };
524 self.appraisal_results_time_slice_writer.serialize(row)?;
525 }
526 }
527
528 Ok(())
529 }
530
531 fn flush(&mut self) -> Result<()> {
533 if let Some(wrt) = &mut self.unmet_demand_writer {
534 wrt.flush()?;
535 }
536 self.commodity_balance_duals_writer.flush()?;
537 self.solver_values_writer.flush()?;
538 self.appraisal_results_writer.flush()?;
539 self.appraisal_results_time_slice_writer.flush()?;
540 self.dispatch_asset_writer.flush()?;
541
542 Ok(())
543 }
544}
545
546pub struct DataWriter {
548 assets: csv::Writer<File>,
549 asset_capacities: csv::Writer<File>,
550 flows: csv::Writer<File>,
551 prices: csv::Writer<File>,
552 debug: Option<DebugDataWriter>,
553}
554
555impl DataWriter {
556 pub fn create(output_path: &Path, model_path: &Path, save_debug_info: bool) -> Result<Self> {
564 write_metadata(output_path, model_path).context("Failed to save metadata")?;
565
566 let new_writer = |file_name| {
567 let file_path = output_path.join(file_name);
568 csv::Writer::from_path(file_path)
569 };
570
571 let debug_writer = if save_debug_info {
572 Some(DebugDataWriter::create(output_path)?)
574 } else {
575 None
576 };
577
578 Ok(Self {
579 assets: new_writer(ASSETS_FILE_NAME)?,
580 asset_capacities: new_writer(ASSET_CAPACITIES_FILE_NAME)?,
581 flows: new_writer(COMMODITY_FLOWS_FILE_NAME)?,
582 prices: new_writer(COMMODITY_PRICES_FILE_NAME)?,
583 debug: debug_writer,
584 })
585 }
586
587 pub fn write_dispatch_debug_info(
589 &mut self,
590 milestone_year: u32,
591 run_description: &str,
592 solution: &Solution,
593 ) -> Result<()> {
594 if let Some(wtr) = &mut self.debug {
595 wtr.write_dispatch_debug_info(milestone_year, run_description, solution)?;
596 }
597
598 Ok(())
599 }
600
601 pub fn write_appraisal_debug_info(
603 &mut self,
604 milestone_year: u32,
605 run_description: &str,
606 appraisal_results: &[AppraisalOutput],
607 demand: &IndexMap<TimeSliceID, Flow>,
608 ) -> Result<()> {
609 if let Some(wtr) = &mut self.debug {
610 wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
611 wtr.write_appraisal_time_slice_results(
612 milestone_year,
613 run_description,
614 appraisal_results,
615 demand,
616 )?;
617 }
618
619 Ok(())
620 }
621
622 pub fn write_assets(&mut self, assets: &[AssetRef]) -> Result<()> {
624 for asset in assets {
625 self.assets.serialize(AssetRow::new(asset))?;
626 }
627
628 Ok(())
629 }
630
631 pub fn write_asset_capacities(
633 &mut self,
634 milestone_year: u32,
635 assets: &[AssetRef],
636 ) -> Result<()> {
637 for asset in assets {
638 let row = AssetCapacityRow {
639 milestone_year,
640 asset_id: asset.id().unwrap(),
641 capacity: asset.total_capacity(),
642 num_tranches: asset.capacity().num_tranches(),
643 mothballed_capacity: asset.mothballed_capacity(),
644 mothballed_tranches: asset.get_num_mothballed_tranches(),
645 };
646 self.asset_capacities.serialize(row)?;
647 }
648
649 Ok(())
650 }
651
652 pub fn write_flows(&mut self, milestone_year: u32, flow_map: &FlowMap) -> Result<()> {
654 for ((asset, commodity_id, time_slice), flow) in flow_map {
655 let row = CommodityFlowRow {
656 milestone_year,
657 asset_id: asset.id().unwrap(),
658 commodity_id: commodity_id.clone(),
659 time_slice: time_slice.clone(),
660 flow: *flow,
661 };
662 self.flows.serialize(row)?;
663 }
664
665 Ok(())
666 }
667
668 pub fn write_prices(&mut self, milestone_year: u32, prices: &PriceMap) -> Result<()> {
670 for (commodity_id, region_id, time_slice, price) in prices.iter() {
671 let row = CommodityPriceRow {
672 milestone_year,
673 commodity_id: commodity_id.clone(),
674 region_id: region_id.clone(),
675 time_slice: time_slice.clone(),
676 price,
677 };
678 self.prices.serialize(row)?;
679 }
680
681 Ok(())
682 }
683
684 pub fn flush(&mut self) -> Result<()> {
686 self.assets.flush()?;
687 self.asset_capacities.flush()?;
688 self.flows.flush()?;
689 self.prices.flush()?;
690 if let Some(wtr) = &mut self.debug {
691 wtr.flush()?;
692 }
693
694 Ok(())
695 }
696
697 pub fn set_debug_context(&mut self, context: String) {
699 if let Some(wtr) = &mut self.debug {
700 wtr.context = Some(context);
701 }
702 }
703
704 pub fn clear_debug_context(&mut self) {
706 if let Some(wtr) = &mut self.debug {
707 wtr.context = None;
708 }
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::asset::AssetPool;
716 use crate::fixture::{
717 appraisal_output, asset, assets, commodity_id, multi_tranche_asset, region_id, time_slice,
718 };
719 use crate::simulation::investment::appraisal::AppraisalOutput;
720 use crate::time_slice::TimeSliceID;
721 use indexmap::indexmap;
722 use itertools::{Itertools, assert_equal};
723 use rstest::rstest;
724 use std::iter;
725 use tempfile::tempdir;
726
727 #[rstest]
728 fn write_assets(assets: AssetPool) {
729 let dir = tempdir().unwrap();
730
731 {
733 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
734 writer.write_assets(&assets).unwrap();
735 writer.flush().unwrap();
736 }
737
738 let asset = assets.iter().next().unwrap();
740 let expected = AssetRow::new(asset);
741 let records: Vec<AssetRow> = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME))
742 .unwrap()
743 .into_deserialize()
744 .try_collect()
745 .unwrap();
746 assert_equal(records, iter::once(expected));
747 }
748
749 #[rstest]
750 fn write_asset_capacities(assets: AssetPool) {
751 let milestone_year = 2020;
752 let dir = tempdir().unwrap();
753
754 {
756 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
757 writer
758 .write_asset_capacities(milestone_year, &assets)
759 .unwrap();
760 writer.flush().unwrap();
761 }
762
763 let asset = assets.iter().next().unwrap();
765 let expected = AssetCapacityRow {
766 milestone_year,
767 asset_id: asset.id().unwrap(),
768 capacity: asset.total_capacity(),
769 num_tranches: 1,
770 mothballed_capacity: Capacity(0.0),
771 mothballed_tranches: 0,
772 };
773 let records: Vec<AssetCapacityRow> =
774 csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
775 .unwrap()
776 .into_deserialize()
777 .try_collect()
778 .unwrap();
779 assert_equal(records, iter::once(expected));
780 }
781
782 #[rstest]
783 fn write_asset_capacities_with_mothballed_tranches(multi_tranche_asset: Asset) {
784 let milestone_year = 2020;
785 let dir = tempdir().unwrap();
786 let mut assets = AssetPool::new();
787 assets.commission_new(2010, &mut vec![multi_tranche_asset.into()]);
788 let asset = assets
789 .iter()
790 .next()
791 .unwrap()
792 .clone()
793 .with_mothballed_tranches(1, Some(milestone_year));
794
795 {
796 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
797 writer
798 .write_asset_capacities(milestone_year, std::slice::from_ref(&asset))
799 .unwrap();
800 writer.flush().unwrap();
801 }
802
803 let expected = AssetCapacityRow {
804 milestone_year,
805 asset_id: asset.id().unwrap(),
806 capacity: Capacity(12.0),
807 num_tranches: 3,
808 mothballed_capacity: Capacity(4.0),
809 mothballed_tranches: 1,
810 };
811 let records: Vec<AssetCapacityRow> =
812 csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
813 .unwrap()
814 .into_deserialize()
815 .try_collect()
816 .unwrap();
817 assert_equal(records, iter::once(expected));
818 }
819
820 #[rstest]
821 fn write_flows(assets: AssetPool, commodity_id: CommodityID, time_slice: TimeSliceID) {
822 let milestone_year = 2020;
823 let asset = assets.iter().next().unwrap();
824 let flow_map = indexmap! {
825 (asset.clone(), commodity_id.clone(), time_slice.clone()) => Flow(42.0)
826 };
827
828 let dir = tempdir().unwrap();
830 {
831 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
832 writer.write_flows(milestone_year, &flow_map).unwrap();
833 writer.flush().unwrap();
834 }
835
836 let expected = CommodityFlowRow {
838 milestone_year,
839 asset_id: asset.id().unwrap(),
840 commodity_id,
841 time_slice,
842 flow: Flow(42.0),
843 };
844 let records: Vec<CommodityFlowRow> =
845 csv::Reader::from_path(dir.path().join(COMMODITY_FLOWS_FILE_NAME))
846 .unwrap()
847 .into_deserialize()
848 .try_collect()
849 .unwrap();
850 assert_equal(records, iter::once(expected));
851 }
852
853 #[rstest]
854 fn write_prices(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
855 let milestone_year = 2020;
856 let price = MoneyPerFlow(42.0);
857 let mut prices = PriceMap::default();
858 prices.insert(&commodity_id, ®ion_id, &time_slice, price);
859
860 let dir = tempdir().unwrap();
861
862 {
864 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
865 writer.write_prices(milestone_year, &prices).unwrap();
866 writer.flush().unwrap();
867 }
868
869 let expected = CommodityPriceRow {
871 milestone_year,
872 commodity_id,
873 region_id,
874 time_slice,
875 price,
876 };
877 let records: Vec<CommodityPriceRow> =
878 csv::Reader::from_path(dir.path().join(COMMODITY_PRICES_FILE_NAME))
879 .unwrap()
880 .into_deserialize()
881 .try_collect()
882 .unwrap();
883 assert_equal(records, iter::once(expected));
884 }
885
886 #[rstest]
887 fn write_commodity_balance_duals(
888 commodity_id: CommodityID,
889 region_id: RegionID,
890 time_slice: TimeSliceID,
891 ) {
892 let milestone_year = 2020;
893 let run_description = "test_run".to_string();
894 let value = MoneyPerFlow(0.5);
895 let dir = tempdir().unwrap();
896
897 {
899 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
900 writer
901 .write_commodity_balance_duals(
902 milestone_year,
903 &run_description,
904 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
905 )
906 .unwrap();
907 writer.flush().unwrap();
908 }
909
910 let expected = CommodityBalanceDualsRow {
912 milestone_year,
913 run_description,
914 commodity_id,
915 region_id,
916 time_slice,
917 value,
918 };
919 let records: Vec<CommodityBalanceDualsRow> =
920 csv::Reader::from_path(dir.path().join(COMMODITY_BALANCE_DUALS_FILE_NAME))
921 .unwrap()
922 .into_deserialize()
923 .try_collect()
924 .unwrap();
925 assert_equal(records, iter::once(expected));
926 }
927
928 #[rstest]
929 fn write_unmet_demand(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
930 let milestone_year = 2020;
931 let run_description = "test_run".to_string();
932 let value = Flow(0.5);
933 let dir = tempdir().unwrap();
934
935 {
937 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
938 writer
939 .write_unmet_demand(
940 milestone_year,
941 &run_description,
942 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
943 )
944 .unwrap();
945 writer.flush().unwrap();
946 }
947
948 let expected = UnmetDemandRow {
950 milestone_year,
951 run_description,
952 commodity_id,
953 region_id,
954 time_slice,
955 value,
956 };
957 let records: Vec<UnmetDemandRow> =
958 csv::Reader::from_path(dir.path().join(UNMET_DEMAND_FILE_NAME))
959 .unwrap()
960 .into_deserialize()
961 .try_collect()
962 .unwrap();
963 assert_equal(records, iter::once(expected));
964 }
965
966 #[rstest]
967 fn write_dispatch(assets: AssetPool, time_slice: TimeSliceID) {
968 let milestone_year = 2020;
969 let run_description = "test_run".to_string();
970 let activity = Activity(100.5);
971 let activity_dual = MoneyPerActivity(-1.5);
972 let column_dual = MoneyPerActivity(5.0);
973 let dir = tempdir().unwrap();
974 let asset = assets.iter().next().unwrap();
975
976 {
978 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
979 writer
980 .write_dispatch(
981 milestone_year,
982 &run_description,
983 iter::once((asset, &time_slice, activity)),
984 iter::once((asset, &time_slice, activity_dual)),
985 iter::once((asset, &time_slice, column_dual)),
986 )
987 .unwrap();
988 writer.flush().unwrap();
989 }
990
991 let expected = DispatchRow {
993 milestone_year,
994 run_description,
995 asset_id: asset.id(),
996 process_id: asset.process_id().clone(),
997 region_id: asset.region_id().clone(),
998 time_slice,
999 activity: Some(activity),
1000 activity_dual: Some(activity_dual),
1001 column_dual: Some(column_dual),
1002 };
1003 let records: Vec<DispatchRow> =
1004 csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1005 .unwrap()
1006 .into_deserialize()
1007 .try_collect()
1008 .unwrap();
1009 assert_equal(records, iter::once(expected));
1010 }
1011
1012 #[rstest]
1013 fn write_dispatch_with_missing_keys(assets: AssetPool, time_slice: TimeSliceID) {
1014 let milestone_year = 2020;
1015 let run_description = "test_run".to_string();
1016 let activity = Activity(100.5);
1017 let dir = tempdir().unwrap();
1018 let asset = assets.iter().next().unwrap();
1019
1020 {
1022 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1023 writer
1024 .write_dispatch(
1025 milestone_year,
1026 &run_description,
1027 iter::once((asset, &time_slice, activity)),
1028 iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
1029 iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
1030 )
1031 .unwrap();
1032 writer.flush().unwrap();
1033 }
1034
1035 let expected = DispatchRow {
1037 milestone_year,
1038 run_description,
1039 asset_id: asset.id(),
1040 process_id: asset.process_id().clone(),
1041 region_id: asset.region_id().clone(),
1042 time_slice,
1043 activity: Some(activity),
1044 activity_dual: None,
1045 column_dual: None,
1046 };
1047 let records: Vec<DispatchRow> =
1048 csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1049 .unwrap()
1050 .into_deserialize()
1051 .try_collect()
1052 .unwrap();
1053 assert_equal(records, iter::once(expected));
1054 }
1055
1056 #[rstest]
1057 fn write_solver_values() {
1058 let milestone_year = 2020;
1059 let run_description = "test_run".to_string();
1060 let objective_value = Money(1234.56);
1061 let dir = tempdir().unwrap();
1062
1063 {
1065 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1066 writer
1067 .write_solver_values(milestone_year, &run_description, objective_value)
1068 .unwrap();
1069 writer.flush().unwrap();
1070 }
1071
1072 let expected = SolverValuesRow {
1074 milestone_year,
1075 run_description,
1076 objective_value,
1077 };
1078 let records: Vec<SolverValuesRow> =
1079 csv::Reader::from_path(dir.path().join(SOLVER_VALUES_FILE_NAME))
1080 .unwrap()
1081 .into_deserialize()
1082 .try_collect()
1083 .unwrap();
1084 assert_equal(records, iter::once(expected));
1085 }
1086
1087 #[rstest]
1088 fn write_appraisal_results(asset: Asset, appraisal_output: AppraisalOutput) {
1089 let milestone_year = 2020;
1090 let run_description = "test_run".to_string();
1091 let dir = tempdir().unwrap();
1092
1093 {
1095 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1096 writer
1097 .write_appraisal_results(milestone_year, &run_description, &[appraisal_output])
1098 .unwrap();
1099 writer.flush().unwrap();
1100 }
1101
1102 let expected = AppraisalResultsRow {
1104 milestone_year,
1105 run_description,
1106 asset_id: None,
1107 process_id: asset.process_id().clone(),
1108 region_id: asset.region_id().clone(),
1109 capacity: Capacity(2.0),
1110 metric: Some(4.14),
1111 };
1112 let records: Vec<AppraisalResultsRow> =
1113 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
1114 .unwrap()
1115 .into_deserialize()
1116 .try_collect()
1117 .unwrap();
1118 assert_equal(records, iter::once(expected));
1119 }
1120
1121 #[rstest]
1122 fn write_appraisal_time_slice_results(
1123 asset: Asset,
1124 appraisal_output: AppraisalOutput,
1125 time_slice: TimeSliceID,
1126 ) {
1127 let milestone_year = 2020;
1128 let run_description = "test_run".to_string();
1129 let dir = tempdir().unwrap();
1130 let demand = indexmap! {time_slice.clone() => Flow(100.0) };
1131
1132 {
1134 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1135 writer
1136 .write_appraisal_time_slice_results(
1137 milestone_year,
1138 &run_description,
1139 &[appraisal_output],
1140 &demand,
1141 )
1142 .unwrap();
1143 writer.flush().unwrap();
1144 }
1145
1146 let expected = AppraisalResultsTimeSliceRow {
1148 milestone_year,
1149 run_description,
1150 asset_id: None,
1151 process_id: asset.process_id().clone(),
1152 region_id: asset.region_id().clone(),
1153 time_slice: time_slice.clone(),
1154 activity: Activity(10.0),
1155 activity_coefficient: MoneyPerActivity(0.5),
1156 demand: Flow(100.0),
1157 unmet_demand: Flow(5.0),
1158 };
1159 let records: Vec<AppraisalResultsTimeSliceRow> =
1160 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME))
1161 .unwrap()
1162 .into_deserialize()
1163 .try_collect()
1164 .unwrap();
1165 assert_equal(records, iter::once(expected));
1166 }
1167
1168 #[test]
1169 fn create_output_directory_new_directory() {
1170 let temp_dir = tempdir().unwrap();
1171 let output_dir = temp_dir.path().join("new_output");
1172
1173 let result = create_output_directory(&output_dir, false).unwrap();
1175 assert!(!result);
1176 assert!(output_dir.exists());
1177 assert!(output_dir.is_dir());
1178 }
1179
1180 #[test]
1181 fn create_output_directory_existing_empty_directory() {
1182 let temp_dir = tempdir().unwrap();
1183 let output_dir = temp_dir.path().join("empty_output");
1184
1185 fs::create_dir(&output_dir).unwrap();
1187
1188 let result = create_output_directory(&output_dir, false).unwrap();
1190 assert!(!result);
1191 assert!(output_dir.exists());
1192 assert!(output_dir.is_dir());
1193 }
1194
1195 #[test]
1196 fn create_output_directory_existing_with_files_no_overwrite() {
1197 let temp_dir = tempdir().unwrap();
1198 let output_dir = temp_dir.path().join("output_with_files");
1199
1200 fs::create_dir(&output_dir).unwrap();
1202 fs::write(output_dir.join("existing_file.txt"), "some content").unwrap();
1203
1204 let result = create_output_directory(&output_dir, false);
1206 assert!(result.is_err());
1207 assert!(
1208 result
1209 .unwrap_err()
1210 .to_string()
1211 .contains("Output folder already exists")
1212 );
1213 }
1214
1215 #[test]
1216 fn create_output_directory_existing_with_files_allow_overwrite() {
1217 let temp_dir = tempdir().unwrap();
1218 let output_dir = temp_dir.path().join("output_with_files");
1219
1220 fs::create_dir(&output_dir).unwrap();
1222 let file_path = output_dir.join("existing_file.txt");
1223 fs::write(&file_path, "some content").unwrap();
1224
1225 let result = create_output_directory(&output_dir, true).unwrap();
1227 assert!(result);
1228 assert!(output_dir.exists());
1229 assert!(output_dir.is_dir());
1230 assert!(!file_path.exists()); }
1232
1233 #[test]
1234 fn create_output_directory_nested_path() {
1235 let temp_dir = tempdir().unwrap();
1236 let output_dir = temp_dir.path().join("nested").join("path").join("output");
1237
1238 let result = create_output_directory(&output_dir, false).unwrap();
1240 assert!(!result);
1241 assert!(output_dir.exists());
1242 assert!(output_dir.is_dir());
1243 }
1244
1245 #[test]
1246 fn create_output_directory_existing_subdirs_with_files_allow_overwrite() {
1247 let temp_dir = tempdir().unwrap();
1248 let output_dir = temp_dir.path().join("output_with_subdirs");
1249
1250 fs::create_dir_all(output_dir.join("subdir")).unwrap();
1252 fs::write(output_dir.join("file1.txt"), "content1").unwrap();
1253 fs::write(output_dir.join("subdir").join("file2.txt"), "content2").unwrap();
1254
1255 let result = create_output_directory(&output_dir, true).unwrap();
1257 assert!(result);
1258 assert!(output_dir.exists());
1259 assert!(output_dir.is_dir());
1260 assert!(!output_dir.join("file1.txt").exists());
1262 assert!(!output_dir.join("subdir").exists());
1263 }
1264}