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_units: Option<u32>,
169}
170
171#[derive(Serialize, Deserialize, Debug, PartialEq)]
173struct CommodityFlowRow {
174 milestone_year: u32,
175 asset_id: AssetID,
176 commodity_id: CommodityID,
177 time_slice: TimeSliceID,
178 flow: Flow,
179}
180
181#[derive(Serialize, Deserialize, Debug, PartialEq)]
183struct CommodityPriceRow {
184 milestone_year: u32,
185 commodity_id: CommodityID,
186 region_id: RegionID,
187 time_slice: TimeSliceID,
188 price: MoneyPerFlow,
189}
190
191#[derive(Serialize, Deserialize, Debug, PartialEq)]
193struct DispatchRow {
194 milestone_year: u32,
195 run_description: String,
196 asset_id: Option<AssetID>,
197 process_id: ProcessID,
198 region_id: RegionID,
199 time_slice: TimeSliceID,
200 activity: Option<Activity>,
201 activity_dual: Option<MoneyPerActivity>,
202 column_dual: Option<MoneyPerActivity>,
203}
204
205#[derive(Serialize, Deserialize, Debug, PartialEq)]
207struct CommodityBalanceDualsRow {
208 milestone_year: u32,
209 run_description: String,
210 commodity_id: CommodityID,
211 region_id: RegionID,
212 time_slice: TimeSliceID,
213 value: MoneyPerFlow,
214}
215
216#[derive(Serialize, Deserialize, Debug, PartialEq)]
218struct UnmetDemandRow {
219 milestone_year: u32,
220 run_description: String,
221 commodity_id: CommodityID,
222 region_id: RegionID,
223 time_slice: TimeSliceID,
224 value: Flow,
225}
226
227#[derive(Serialize, Deserialize, Debug, PartialEq)]
229struct SolverValuesRow {
230 milestone_year: u32,
231 run_description: String,
232 objective_value: Money,
233}
234
235#[derive(Serialize, Deserialize, Debug, PartialEq)]
237struct AppraisalResultsRow {
238 milestone_year: u32,
239 run_description: String,
240 asset_id: Option<AssetID>,
241 process_id: ProcessID,
242 region_id: RegionID,
243 capacity: Capacity,
244 metric: Option<f64>,
245}
246
247#[derive(Serialize, Deserialize, Debug, PartialEq)]
249struct AppraisalResultsTimeSliceRow {
250 milestone_year: u32,
251 run_description: String,
252 asset_id: Option<AssetID>,
253 process_id: ProcessID,
254 region_id: RegionID,
255 time_slice: TimeSliceID,
256 activity: Activity,
257 activity_coefficient: MoneyPerActivity,
258 demand: Flow,
259 unmet_demand: Flow,
260}
261
262struct DebugDataWriter {
264 context: Option<String>,
265 unmet_demand_file_path: PathBuf,
266 commodity_balance_duals_writer: csv::Writer<File>,
267 unmet_demand_writer: Option<csv::Writer<File>>,
268 solver_values_writer: csv::Writer<File>,
269 appraisal_results_writer: csv::Writer<File>,
270 appraisal_results_time_slice_writer: csv::Writer<File>,
271 dispatch_asset_writer: csv::Writer<File>,
272}
273
274impl DebugDataWriter {
275 fn create(output_path: &Path) -> Result<Self> {
281 let new_writer = |file_name| {
282 let file_path = output_path.join(file_name);
283 csv::Writer::from_path(file_path)
284 };
285
286 Ok(Self {
287 context: None,
288 unmet_demand_file_path: output_path.join(UNMET_DEMAND_FILE_NAME),
289 commodity_balance_duals_writer: new_writer(COMMODITY_BALANCE_DUALS_FILE_NAME)?,
290 unmet_demand_writer: None,
291 solver_values_writer: new_writer(SOLVER_VALUES_FILE_NAME)?,
292 appraisal_results_writer: new_writer(APPRAISAL_RESULTS_FILE_NAME)?,
293 appraisal_results_time_slice_writer: new_writer(
294 APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME,
295 )?,
296 dispatch_asset_writer: new_writer(ACTIVITY_ASSET_DISPATCH)?,
297 })
298 }
299
300 fn with_context(&self, run_description: &str) -> String {
302 if let Some(context) = &self.context {
303 format!("{context}; {run_description}")
304 } else {
305 run_description.to_string()
306 }
307 }
308
309 fn write_dispatch_debug_info(
311 &mut self,
312 milestone_year: u32,
313 run_description: &str,
314 solution: &Solution,
315 ) -> Result<()> {
316 self.write_dispatch(
317 milestone_year,
318 run_description,
319 solution.iter_activity(),
320 solution.iter_activity_duals(),
321 solution.iter_column_duals(),
322 )?;
323 self.write_commodity_balance_duals(
324 milestone_year,
325 run_description,
326 solution.iter_commodity_balance_duals(),
327 )?;
328 self.write_unmet_demand(
329 milestone_year,
330 run_description,
331 solution.iter_unmet_demand(),
332 )?;
333 self.write_solver_values(milestone_year, run_description, solution.objective_value)?;
334 Ok(())
335 }
336
337 fn write_dispatch<'a, I, J, K>(
339 &mut self,
340 milestone_year: u32,
341 run_description: &str,
342 iter_activity: I,
343 iter_activity_duals: J,
344 iter_column_duals: K,
345 ) -> Result<()>
346 where
347 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
348 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
349 K: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
350 {
351 type CompiledActivityData = (
353 Option<Activity>,
354 Option<MoneyPerActivity>,
355 Option<MoneyPerActivity>,
356 );
357 let mut map: IndexMap<(&AssetRef, &TimeSliceID), CompiledActivityData> = IndexMap::new();
358
359 for (asset, time_slice, activity) in iter_activity {
361 map.entry((asset, time_slice)).or_default().0 = Some(activity);
362 }
363 for (asset, time_slice, activity_dual) in iter_activity_duals {
365 map.entry((asset, time_slice)).or_default().1 = Some(activity_dual);
366 }
367 for (asset, time_slice, column_dual) in iter_column_duals {
369 map.entry((asset, time_slice)).or_default().2 = Some(column_dual);
370 }
371
372 for ((asset, time_slice), (activity, activity_dual, column_dual)) in map {
373 let row = DispatchRow {
374 milestone_year,
375 run_description: self.with_context(run_description),
376 asset_id: asset.id(),
377 process_id: asset.process_id().clone(),
378 region_id: asset.region_id().clone(),
379 time_slice: time_slice.clone(),
380 activity,
381 activity_dual,
382 column_dual,
383 };
384 self.dispatch_asset_writer.serialize(row)?;
385 }
386
387 Ok(())
388 }
389
390 fn write_commodity_balance_duals<'a, I>(
392 &mut self,
393 milestone_year: u32,
394 run_description: &str,
395 iter: I,
396 ) -> Result<()>
397 where
398 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
399 {
400 for (commodity_id, region_id, time_slice, value) in iter {
401 let row = CommodityBalanceDualsRow {
402 milestone_year,
403 run_description: self.with_context(run_description),
404 commodity_id: commodity_id.clone(),
405 region_id: region_id.clone(),
406 time_slice: time_slice.clone(),
407 value,
408 };
409 self.commodity_balance_duals_writer.serialize(row)?;
410 }
411
412 Ok(())
413 }
414
415 fn write_unmet_demand<'a, I>(
417 &mut self,
418 milestone_year: u32,
419 run_description: &str,
420 iter: I,
421 ) -> Result<()>
422 where
423 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, Flow)>,
424 {
425 let mut rows = iter.peekable();
426
427 if rows.peek().is_none() {
428 return Ok(());
429 }
430
431 assert!(
433 self.unmet_demand_writer.is_none(),
434 "Unmet demand file already exists!"
435 );
436
437 let run_description = self.with_context(run_description);
438 let writer = self
439 .unmet_demand_writer
440 .insert(csv::Writer::from_path(&self.unmet_demand_file_path)?);
441 for (commodity_id, region_id, time_slice, value) in rows {
442 let row = UnmetDemandRow {
443 milestone_year,
444 run_description: run_description.clone(),
445 commodity_id: commodity_id.clone(),
446 region_id: region_id.clone(),
447 time_slice: time_slice.clone(),
448 value,
449 };
450 writer.serialize(row)?;
451 }
452
453 Ok(())
454 }
455
456 fn write_solver_values(
458 &mut self,
459 milestone_year: u32,
460 run_description: &str,
461 objective_value: Money,
462 ) -> Result<()> {
463 let row = SolverValuesRow {
464 milestone_year,
465 run_description: self.with_context(run_description),
466 objective_value,
467 };
468 self.solver_values_writer.serialize(row)?;
469 self.solver_values_writer.flush()?;
470
471 Ok(())
472 }
473
474 fn write_appraisal_results(
476 &mut self,
477 milestone_year: u32,
478 run_description: &str,
479 appraisal_results: &[AppraisalOutput],
480 ) -> Result<()> {
481 for result in appraisal_results {
482 let row = AppraisalResultsRow {
483 milestone_year,
484 run_description: self.with_context(run_description),
485 asset_id: result.asset.id(),
486 process_id: result.asset.process_id().clone(),
487 region_id: result.asset.region_id().clone(),
488 capacity: result.asset.capacity().total_capacity(),
489 metric: result.metric.as_ref().map(|m| m.value()),
490 };
491 self.appraisal_results_writer.serialize(row)?;
492 }
493
494 Ok(())
495 }
496
497 fn write_appraisal_time_slice_results(
499 &mut self,
500 milestone_year: u32,
501 run_description: &str,
502 appraisal_results: &[AppraisalOutput],
503 demand: &IndexMap<TimeSliceID, Flow>,
504 ) -> Result<()> {
505 for result in appraisal_results {
506 for (time_slice, activity) in &result.activity {
507 let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
508 let demand = demand[time_slice];
509 let unmet_demand = result.unmet_demand[time_slice];
510 let row = AppraisalResultsTimeSliceRow {
511 milestone_year,
512 run_description: self.with_context(run_description),
513 asset_id: result.asset.id(),
514 process_id: result.asset.process_id().clone(),
515 region_id: result.asset.region_id().clone(),
516 time_slice: time_slice.clone(),
517 activity: *activity,
518 activity_coefficient,
519 demand,
520 unmet_demand,
521 };
522 self.appraisal_results_time_slice_writer.serialize(row)?;
523 }
524 }
525
526 Ok(())
527 }
528
529 fn flush(&mut self) -> Result<()> {
531 if let Some(wrt) = &mut self.unmet_demand_writer {
532 wrt.flush()?;
533 }
534 self.commodity_balance_duals_writer.flush()?;
535 self.solver_values_writer.flush()?;
536 self.appraisal_results_writer.flush()?;
537 self.appraisal_results_time_slice_writer.flush()?;
538 self.dispatch_asset_writer.flush()?;
539
540 Ok(())
541 }
542}
543
544pub struct DataWriter {
546 assets: csv::Writer<File>,
547 asset_capacities: csv::Writer<File>,
548 flows: csv::Writer<File>,
549 prices: csv::Writer<File>,
550 debug: Option<DebugDataWriter>,
551}
552
553impl DataWriter {
554 pub fn create(output_path: &Path, model_path: &Path, save_debug_info: bool) -> Result<Self> {
562 write_metadata(output_path, model_path).context("Failed to save metadata")?;
563
564 let new_writer = |file_name| {
565 let file_path = output_path.join(file_name);
566 csv::Writer::from_path(file_path)
567 };
568
569 let debug_writer = if save_debug_info {
570 Some(DebugDataWriter::create(output_path)?)
572 } else {
573 None
574 };
575
576 Ok(Self {
577 assets: new_writer(ASSETS_FILE_NAME)?,
578 asset_capacities: new_writer(ASSET_CAPACITIES_FILE_NAME)?,
579 flows: new_writer(COMMODITY_FLOWS_FILE_NAME)?,
580 prices: new_writer(COMMODITY_PRICES_FILE_NAME)?,
581 debug: debug_writer,
582 })
583 }
584
585 pub fn write_dispatch_debug_info(
587 &mut self,
588 milestone_year: u32,
589 run_description: &str,
590 solution: &Solution,
591 ) -> Result<()> {
592 if let Some(wtr) = &mut self.debug {
593 wtr.write_dispatch_debug_info(milestone_year, run_description, solution)?;
594 }
595
596 Ok(())
597 }
598
599 pub fn write_appraisal_debug_info(
601 &mut self,
602 milestone_year: u32,
603 run_description: &str,
604 appraisal_results: &[AppraisalOutput],
605 demand: &IndexMap<TimeSliceID, Flow>,
606 ) -> Result<()> {
607 if let Some(wtr) = &mut self.debug {
608 wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
609 wtr.write_appraisal_time_slice_results(
610 milestone_year,
611 run_description,
612 appraisal_results,
613 demand,
614 )?;
615 }
616
617 Ok(())
618 }
619
620 pub fn write_assets(&mut self, assets: &[AssetRef]) -> Result<()> {
622 for asset in assets {
623 self.assets.serialize(AssetRow::new(asset))?;
624 }
625
626 Ok(())
627 }
628
629 pub fn write_asset_capacities(
631 &mut self,
632 milestone_year: u32,
633 assets: &[AssetRef],
634 ) -> Result<()> {
635 for asset in assets {
636 let row = AssetCapacityRow {
637 milestone_year,
638 asset_id: asset.id().unwrap(),
639 capacity: asset.total_capacity(),
640 num_units: asset.capacity().n_units(),
641 };
642 self.asset_capacities.serialize(row)?;
643 }
644
645 Ok(())
646 }
647
648 pub fn write_flows(&mut self, milestone_year: u32, flow_map: &FlowMap) -> Result<()> {
650 for ((asset, commodity_id, time_slice), flow) in flow_map {
651 let row = CommodityFlowRow {
652 milestone_year,
653 asset_id: asset.id().unwrap(),
654 commodity_id: commodity_id.clone(),
655 time_slice: time_slice.clone(),
656 flow: *flow,
657 };
658 self.flows.serialize(row)?;
659 }
660
661 Ok(())
662 }
663
664 pub fn write_prices(&mut self, milestone_year: u32, prices: &PriceMap) -> Result<()> {
666 for (commodity_id, region_id, time_slice, price) in prices.iter() {
667 let row = CommodityPriceRow {
668 milestone_year,
669 commodity_id: commodity_id.clone(),
670 region_id: region_id.clone(),
671 time_slice: time_slice.clone(),
672 price,
673 };
674 self.prices.serialize(row)?;
675 }
676
677 Ok(())
678 }
679
680 pub fn flush(&mut self) -> Result<()> {
682 self.assets.flush()?;
683 self.asset_capacities.flush()?;
684 self.flows.flush()?;
685 self.prices.flush()?;
686 if let Some(wtr) = &mut self.debug {
687 wtr.flush()?;
688 }
689
690 Ok(())
691 }
692
693 pub fn set_debug_context(&mut self, context: String) {
695 if let Some(wtr) = &mut self.debug {
696 wtr.context = Some(context);
697 }
698 }
699
700 pub fn clear_debug_context(&mut self) {
702 if let Some(wtr) = &mut self.debug {
703 wtr.context = None;
704 }
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use crate::asset::AssetPool;
712 use crate::fixture::{appraisal_output, asset, assets, commodity_id, region_id, time_slice};
713 use crate::simulation::investment::appraisal::AppraisalOutput;
714 use crate::time_slice::TimeSliceID;
715 use indexmap::indexmap;
716 use itertools::{Itertools, assert_equal};
717 use rstest::rstest;
718 use std::iter;
719 use tempfile::tempdir;
720
721 #[rstest]
722 fn write_assets(assets: AssetPool) {
723 let dir = tempdir().unwrap();
724
725 {
727 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
728 writer.write_assets(&assets).unwrap();
729 writer.flush().unwrap();
730 }
731
732 let asset = assets.iter().next().unwrap();
734 let expected = AssetRow::new(asset);
735 let records: Vec<AssetRow> = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME))
736 .unwrap()
737 .into_deserialize()
738 .try_collect()
739 .unwrap();
740 assert_equal(records, iter::once(expected));
741 }
742
743 #[rstest]
744 fn write_asset_capacities(assets: AssetPool) {
745 let milestone_year = 2020;
746 let dir = tempdir().unwrap();
747
748 {
750 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
751 writer
752 .write_asset_capacities(milestone_year, &assets)
753 .unwrap();
754 writer.flush().unwrap();
755 }
756
757 let asset = assets.iter().next().unwrap();
759 let expected = AssetCapacityRow {
760 milestone_year,
761 asset_id: asset.id().unwrap(),
762 capacity: asset.total_capacity(),
763 num_units: None,
764 };
765 let records: Vec<AssetCapacityRow> =
766 csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
767 .unwrap()
768 .into_deserialize()
769 .try_collect()
770 .unwrap();
771 assert_equal(records, iter::once(expected));
772 }
773
774 #[rstest]
775 fn write_flows(assets: AssetPool, commodity_id: CommodityID, time_slice: TimeSliceID) {
776 let milestone_year = 2020;
777 let asset = assets.iter().next().unwrap();
778 let flow_map = indexmap! {
779 (asset.clone(), commodity_id.clone(), time_slice.clone()) => Flow(42.0)
780 };
781
782 let dir = tempdir().unwrap();
784 {
785 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
786 writer.write_flows(milestone_year, &flow_map).unwrap();
787 writer.flush().unwrap();
788 }
789
790 let expected = CommodityFlowRow {
792 milestone_year,
793 asset_id: asset.id().unwrap(),
794 commodity_id,
795 time_slice,
796 flow: Flow(42.0),
797 };
798 let records: Vec<CommodityFlowRow> =
799 csv::Reader::from_path(dir.path().join(COMMODITY_FLOWS_FILE_NAME))
800 .unwrap()
801 .into_deserialize()
802 .try_collect()
803 .unwrap();
804 assert_equal(records, iter::once(expected));
805 }
806
807 #[rstest]
808 fn write_prices(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
809 let milestone_year = 2020;
810 let price = MoneyPerFlow(42.0);
811 let mut prices = PriceMap::default();
812 prices.insert(&commodity_id, ®ion_id, &time_slice, price);
813
814 let dir = tempdir().unwrap();
815
816 {
818 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
819 writer.write_prices(milestone_year, &prices).unwrap();
820 writer.flush().unwrap();
821 }
822
823 let expected = CommodityPriceRow {
825 milestone_year,
826 commodity_id,
827 region_id,
828 time_slice,
829 price,
830 };
831 let records: Vec<CommodityPriceRow> =
832 csv::Reader::from_path(dir.path().join(COMMODITY_PRICES_FILE_NAME))
833 .unwrap()
834 .into_deserialize()
835 .try_collect()
836 .unwrap();
837 assert_equal(records, iter::once(expected));
838 }
839
840 #[rstest]
841 fn write_commodity_balance_duals(
842 commodity_id: CommodityID,
843 region_id: RegionID,
844 time_slice: TimeSliceID,
845 ) {
846 let milestone_year = 2020;
847 let run_description = "test_run".to_string();
848 let value = MoneyPerFlow(0.5);
849 let dir = tempdir().unwrap();
850
851 {
853 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
854 writer
855 .write_commodity_balance_duals(
856 milestone_year,
857 &run_description,
858 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
859 )
860 .unwrap();
861 writer.flush().unwrap();
862 }
863
864 let expected = CommodityBalanceDualsRow {
866 milestone_year,
867 run_description,
868 commodity_id,
869 region_id,
870 time_slice,
871 value,
872 };
873 let records: Vec<CommodityBalanceDualsRow> =
874 csv::Reader::from_path(dir.path().join(COMMODITY_BALANCE_DUALS_FILE_NAME))
875 .unwrap()
876 .into_deserialize()
877 .try_collect()
878 .unwrap();
879 assert_equal(records, iter::once(expected));
880 }
881
882 #[rstest]
883 fn write_unmet_demand(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
884 let milestone_year = 2020;
885 let run_description = "test_run".to_string();
886 let value = Flow(0.5);
887 let dir = tempdir().unwrap();
888
889 {
891 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
892 writer
893 .write_unmet_demand(
894 milestone_year,
895 &run_description,
896 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
897 )
898 .unwrap();
899 writer.flush().unwrap();
900 }
901
902 let expected = UnmetDemandRow {
904 milestone_year,
905 run_description,
906 commodity_id,
907 region_id,
908 time_slice,
909 value,
910 };
911 let records: Vec<UnmetDemandRow> =
912 csv::Reader::from_path(dir.path().join(UNMET_DEMAND_FILE_NAME))
913 .unwrap()
914 .into_deserialize()
915 .try_collect()
916 .unwrap();
917 assert_equal(records, iter::once(expected));
918 }
919
920 #[rstest]
921 fn write_dispatch(assets: AssetPool, time_slice: TimeSliceID) {
922 let milestone_year = 2020;
923 let run_description = "test_run".to_string();
924 let activity = Activity(100.5);
925 let activity_dual = MoneyPerActivity(-1.5);
926 let column_dual = MoneyPerActivity(5.0);
927 let dir = tempdir().unwrap();
928 let asset = assets.iter().next().unwrap();
929
930 {
932 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
933 writer
934 .write_dispatch(
935 milestone_year,
936 &run_description,
937 iter::once((asset, &time_slice, activity)),
938 iter::once((asset, &time_slice, activity_dual)),
939 iter::once((asset, &time_slice, column_dual)),
940 )
941 .unwrap();
942 writer.flush().unwrap();
943 }
944
945 let expected = DispatchRow {
947 milestone_year,
948 run_description,
949 asset_id: asset.id(),
950 process_id: asset.process_id().clone(),
951 region_id: asset.region_id().clone(),
952 time_slice,
953 activity: Some(activity),
954 activity_dual: Some(activity_dual),
955 column_dual: Some(column_dual),
956 };
957 let records: Vec<DispatchRow> =
958 csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
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_with_missing_keys(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 dir = tempdir().unwrap();
972 let asset = assets.iter().next().unwrap();
973
974 {
976 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
977 writer
978 .write_dispatch(
979 milestone_year,
980 &run_description,
981 iter::once((asset, &time_slice, activity)),
982 iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
983 iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
984 )
985 .unwrap();
986 writer.flush().unwrap();
987 }
988
989 let expected = DispatchRow {
991 milestone_year,
992 run_description,
993 asset_id: asset.id(),
994 process_id: asset.process_id().clone(),
995 region_id: asset.region_id().clone(),
996 time_slice,
997 activity: Some(activity),
998 activity_dual: None,
999 column_dual: None,
1000 };
1001 let records: Vec<DispatchRow> =
1002 csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1003 .unwrap()
1004 .into_deserialize()
1005 .try_collect()
1006 .unwrap();
1007 assert_equal(records, iter::once(expected));
1008 }
1009
1010 #[rstest]
1011 fn write_solver_values() {
1012 let milestone_year = 2020;
1013 let run_description = "test_run".to_string();
1014 let objective_value = Money(1234.56);
1015 let dir = tempdir().unwrap();
1016
1017 {
1019 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1020 writer
1021 .write_solver_values(milestone_year, &run_description, objective_value)
1022 .unwrap();
1023 writer.flush().unwrap();
1024 }
1025
1026 let expected = SolverValuesRow {
1028 milestone_year,
1029 run_description,
1030 objective_value,
1031 };
1032 let records: Vec<SolverValuesRow> =
1033 csv::Reader::from_path(dir.path().join(SOLVER_VALUES_FILE_NAME))
1034 .unwrap()
1035 .into_deserialize()
1036 .try_collect()
1037 .unwrap();
1038 assert_equal(records, iter::once(expected));
1039 }
1040
1041 #[rstest]
1042 fn write_appraisal_results(asset: Asset, appraisal_output: AppraisalOutput) {
1043 let milestone_year = 2020;
1044 let run_description = "test_run".to_string();
1045 let dir = tempdir().unwrap();
1046
1047 {
1049 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1050 writer
1051 .write_appraisal_results(milestone_year, &run_description, &[appraisal_output])
1052 .unwrap();
1053 writer.flush().unwrap();
1054 }
1055
1056 let expected = AppraisalResultsRow {
1058 milestone_year,
1059 run_description,
1060 asset_id: None,
1061 process_id: asset.process_id().clone(),
1062 region_id: asset.region_id().clone(),
1063 capacity: Capacity(2.0),
1064 metric: Some(4.14),
1065 };
1066 let records: Vec<AppraisalResultsRow> =
1067 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
1068 .unwrap()
1069 .into_deserialize()
1070 .try_collect()
1071 .unwrap();
1072 assert_equal(records, iter::once(expected));
1073 }
1074
1075 #[rstest]
1076 fn write_appraisal_time_slice_results(
1077 asset: Asset,
1078 appraisal_output: AppraisalOutput,
1079 time_slice: TimeSliceID,
1080 ) {
1081 let milestone_year = 2020;
1082 let run_description = "test_run".to_string();
1083 let dir = tempdir().unwrap();
1084 let demand = indexmap! {time_slice.clone() => Flow(100.0) };
1085
1086 {
1088 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1089 writer
1090 .write_appraisal_time_slice_results(
1091 milestone_year,
1092 &run_description,
1093 &[appraisal_output],
1094 &demand,
1095 )
1096 .unwrap();
1097 writer.flush().unwrap();
1098 }
1099
1100 let expected = AppraisalResultsTimeSliceRow {
1102 milestone_year,
1103 run_description,
1104 asset_id: None,
1105 process_id: asset.process_id().clone(),
1106 region_id: asset.region_id().clone(),
1107 time_slice: time_slice.clone(),
1108 activity: Activity(10.0),
1109 activity_coefficient: MoneyPerActivity(0.5),
1110 demand: Flow(100.0),
1111 unmet_demand: Flow(5.0),
1112 };
1113 let records: Vec<AppraisalResultsTimeSliceRow> =
1114 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME))
1115 .unwrap()
1116 .into_deserialize()
1117 .try_collect()
1118 .unwrap();
1119 assert_equal(records, iter::once(expected));
1120 }
1121
1122 #[test]
1123 fn create_output_directory_new_directory() {
1124 let temp_dir = tempdir().unwrap();
1125 let output_dir = temp_dir.path().join("new_output");
1126
1127 let result = create_output_directory(&output_dir, false).unwrap();
1129 assert!(!result);
1130 assert!(output_dir.exists());
1131 assert!(output_dir.is_dir());
1132 }
1133
1134 #[test]
1135 fn create_output_directory_existing_empty_directory() {
1136 let temp_dir = tempdir().unwrap();
1137 let output_dir = temp_dir.path().join("empty_output");
1138
1139 fs::create_dir(&output_dir).unwrap();
1141
1142 let result = create_output_directory(&output_dir, false).unwrap();
1144 assert!(!result);
1145 assert!(output_dir.exists());
1146 assert!(output_dir.is_dir());
1147 }
1148
1149 #[test]
1150 fn create_output_directory_existing_with_files_no_overwrite() {
1151 let temp_dir = tempdir().unwrap();
1152 let output_dir = temp_dir.path().join("output_with_files");
1153
1154 fs::create_dir(&output_dir).unwrap();
1156 fs::write(output_dir.join("existing_file.txt"), "some content").unwrap();
1157
1158 let result = create_output_directory(&output_dir, false);
1160 assert!(result.is_err());
1161 assert!(
1162 result
1163 .unwrap_err()
1164 .to_string()
1165 .contains("Output folder already exists")
1166 );
1167 }
1168
1169 #[test]
1170 fn create_output_directory_existing_with_files_allow_overwrite() {
1171 let temp_dir = tempdir().unwrap();
1172 let output_dir = temp_dir.path().join("output_with_files");
1173
1174 fs::create_dir(&output_dir).unwrap();
1176 let file_path = output_dir.join("existing_file.txt");
1177 fs::write(&file_path, "some content").unwrap();
1178
1179 let result = create_output_directory(&output_dir, true).unwrap();
1181 assert!(result);
1182 assert!(output_dir.exists());
1183 assert!(output_dir.is_dir());
1184 assert!(!file_path.exists()); }
1186
1187 #[test]
1188 fn create_output_directory_nested_path() {
1189 let temp_dir = tempdir().unwrap();
1190 let output_dir = temp_dir.path().join("nested").join("path").join("output");
1191
1192 let result = create_output_directory(&output_dir, false).unwrap();
1194 assert!(!result);
1195 assert!(output_dir.exists());
1196 assert!(output_dir.is_dir());
1197 }
1198
1199 #[test]
1200 fn create_output_directory_existing_subdirs_with_files_allow_overwrite() {
1201 let temp_dir = tempdir().unwrap();
1202 let output_dir = temp_dir.path().join("output_with_subdirs");
1203
1204 fs::create_dir_all(output_dir.join("subdir")).unwrap();
1206 fs::write(output_dir.join("file1.txt"), "content1").unwrap();
1207 fs::write(output_dir.join("subdir").join("file2.txt"), "content2").unwrap();
1208
1209 let result = create_output_directory(&output_dir, true).unwrap();
1211 assert!(result);
1212 assert!(output_dir.exists());
1213 assert!(output_dir.is_dir());
1214 assert!(!output_dir.join("file1.txt").exists());
1216 assert!(!output_dir.join("subdir").exists());
1217 }
1218}