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, TimeSliceLevel, TimeSliceSelection};
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 time_slice_level: TimeSliceLevel,
261 demand_for_selection: Flow,
262 unmet_demand_for_selection: Flow,
263}
264
265struct DebugDataWriter {
267 context: Option<String>,
268 unmet_demand_file_path: PathBuf,
269 commodity_balance_duals_writer: csv::Writer<File>,
270 unmet_demand_writer: Option<csv::Writer<File>>,
271 solver_values_writer: csv::Writer<File>,
272 appraisal_results_writer: csv::Writer<File>,
273 appraisal_results_time_slice_writer: csv::Writer<File>,
274 dispatch_asset_writer: csv::Writer<File>,
275}
276
277impl DebugDataWriter {
278 fn create(output_path: &Path) -> Result<Self> {
284 let new_writer = |file_name| {
285 let file_path = output_path.join(file_name);
286 csv::Writer::from_path(file_path)
287 };
288
289 Ok(Self {
290 context: None,
291 unmet_demand_file_path: output_path.join(UNMET_DEMAND_FILE_NAME),
292 commodity_balance_duals_writer: new_writer(COMMODITY_BALANCE_DUALS_FILE_NAME)?,
293 unmet_demand_writer: None,
294 solver_values_writer: new_writer(SOLVER_VALUES_FILE_NAME)?,
295 appraisal_results_writer: new_writer(APPRAISAL_RESULTS_FILE_NAME)?,
296 appraisal_results_time_slice_writer: new_writer(
297 APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME,
298 )?,
299 dispatch_asset_writer: new_writer(ACTIVITY_ASSET_DISPATCH)?,
300 })
301 }
302
303 fn with_context(&self, run_description: &str) -> String {
305 if let Some(context) = &self.context {
306 format!("{context}; {run_description}")
307 } else {
308 run_description.to_string()
309 }
310 }
311
312 fn write_dispatch_debug_info(
314 &mut self,
315 milestone_year: u32,
316 run_description: &str,
317 solution: &Solution,
318 ) -> Result<()> {
319 self.write_dispatch(
320 milestone_year,
321 run_description,
322 solution.iter_activity(),
323 solution.iter_activity_duals(),
324 solution.iter_column_duals(),
325 )?;
326 self.write_commodity_balance_duals(
327 milestone_year,
328 run_description,
329 solution.iter_commodity_balance_duals(),
330 )?;
331 self.write_unmet_demand(
332 milestone_year,
333 run_description,
334 solution.iter_unmet_demand(),
335 )?;
336 self.write_solver_values(milestone_year, run_description, solution.objective_value)?;
337 Ok(())
338 }
339
340 fn write_dispatch<'a, I, J, K>(
342 &mut self,
343 milestone_year: u32,
344 run_description: &str,
345 iter_activity: I,
346 iter_activity_duals: J,
347 iter_column_duals: K,
348 ) -> Result<()>
349 where
350 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
351 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
352 K: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
353 {
354 type CompiledActivityData = (
356 Option<Activity>,
357 Option<MoneyPerActivity>,
358 Option<MoneyPerActivity>,
359 );
360 let mut map: IndexMap<(&AssetRef, &TimeSliceID), CompiledActivityData> = IndexMap::new();
361
362 for (asset, time_slice, activity) in iter_activity {
364 map.entry((asset, time_slice)).or_default().0 = Some(activity);
365 }
366 for (asset, time_slice, activity_dual) in iter_activity_duals {
368 map.entry((asset, time_slice)).or_default().1 = Some(activity_dual);
369 }
370 for (asset, time_slice, column_dual) in iter_column_duals {
372 map.entry((asset, time_slice)).or_default().2 = Some(column_dual);
373 }
374
375 for ((asset, time_slice), (activity, activity_dual, column_dual)) in map {
376 let row = DispatchRow {
377 milestone_year,
378 run_description: self.with_context(run_description),
379 asset_id: asset.id(),
380 process_id: asset.process_id().clone(),
381 region_id: asset.region_id().clone(),
382 time_slice: time_slice.clone(),
383 activity,
384 activity_dual,
385 column_dual,
386 };
387 self.dispatch_asset_writer.serialize(row)?;
388 }
389
390 Ok(())
391 }
392
393 fn write_commodity_balance_duals<'a, I>(
395 &mut self,
396 milestone_year: u32,
397 run_description: &str,
398 iter: I,
399 ) -> Result<()>
400 where
401 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
402 {
403 for (commodity_id, region_id, time_slice, value) in iter {
404 let row = CommodityBalanceDualsRow {
405 milestone_year,
406 run_description: self.with_context(run_description),
407 commodity_id: commodity_id.clone(),
408 region_id: region_id.clone(),
409 time_slice: time_slice.clone(),
410 value,
411 };
412 self.commodity_balance_duals_writer.serialize(row)?;
413 }
414
415 Ok(())
416 }
417
418 fn write_unmet_demand<'a, I>(
420 &mut self,
421 milestone_year: u32,
422 run_description: &str,
423 iter: I,
424 ) -> Result<()>
425 where
426 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, Flow)>,
427 {
428 let mut rows = iter.peekable();
429
430 if rows.peek().is_none() {
431 return Ok(());
432 }
433
434 assert!(
436 self.unmet_demand_writer.is_none(),
437 "Unmet demand file already exists!"
438 );
439
440 let run_description = self.with_context(run_description);
441 let writer = self
442 .unmet_demand_writer
443 .insert(csv::Writer::from_path(&self.unmet_demand_file_path)?);
444 for (commodity_id, region_id, time_slice, value) in rows {
445 let row = UnmetDemandRow {
446 milestone_year,
447 run_description: run_description.clone(),
448 commodity_id: commodity_id.clone(),
449 region_id: region_id.clone(),
450 time_slice: time_slice.clone(),
451 value,
452 };
453 writer.serialize(row)?;
454 }
455
456 Ok(())
457 }
458
459 fn write_solver_values(
461 &mut self,
462 milestone_year: u32,
463 run_description: &str,
464 objective_value: Money,
465 ) -> Result<()> {
466 let row = SolverValuesRow {
467 milestone_year,
468 run_description: self.with_context(run_description),
469 objective_value,
470 };
471 self.solver_values_writer.serialize(row)?;
472 self.solver_values_writer.flush()?;
473
474 Ok(())
475 }
476
477 fn write_appraisal_results(
479 &mut self,
480 milestone_year: u32,
481 run_description: &str,
482 appraisal_results: &[AppraisalOutput],
483 ) -> Result<()> {
484 for result in appraisal_results {
485 let row = AppraisalResultsRow {
486 milestone_year,
487 run_description: self.with_context(run_description),
488 asset_id: result.asset.id(),
489 process_id: result.asset.process_id().clone(),
490 region_id: result.asset.region_id().clone(),
491 capacity: result.asset.total_capacity(),
492 metric: result.metric.as_ref().map(|m| m.value()),
493 };
494 self.appraisal_results_writer.serialize(row)?;
495 }
496
497 Ok(())
498 }
499
500 fn write_appraisal_time_slice_results(
502 &mut self,
503 milestone_year: u32,
504 run_description: &str,
505 appraisal_results: &[AppraisalOutput],
506 demand: &IndexMap<TimeSliceSelection, Flow>,
507 time_slice_level: TimeSliceLevel,
508 ) -> Result<()> {
509 for result in appraisal_results {
510 for (time_slice, activity) in &result.activity {
511 let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
512 let selection = match time_slice_level {
515 TimeSliceLevel::Annual => TimeSliceSelection::Annual,
516 TimeSliceLevel::Season => TimeSliceSelection::Season(time_slice.season.clone()),
517 TimeSliceLevel::DayNight => TimeSliceSelection::Single(time_slice.clone()),
518 };
519 let demand = demand[&selection];
520 let unmet_demand = result.unmet_demand[&selection];
521 let row = AppraisalResultsTimeSliceRow {
522 milestone_year,
523 run_description: self.with_context(run_description),
524 asset_id: result.asset.id(),
525 process_id: result.asset.process_id().clone(),
526 region_id: result.asset.region_id().clone(),
527 time_slice: time_slice.clone(),
528 activity: *activity,
529 activity_coefficient,
530 time_slice_level,
531 demand_for_selection: demand,
532 unmet_demand_for_selection: unmet_demand,
533 };
534 self.appraisal_results_time_slice_writer.serialize(row)?;
535 }
536 }
537
538 Ok(())
539 }
540
541 fn flush(&mut self) -> Result<()> {
543 if let Some(wrt) = &mut self.unmet_demand_writer {
544 wrt.flush()?;
545 }
546 self.commodity_balance_duals_writer.flush()?;
547 self.solver_values_writer.flush()?;
548 self.appraisal_results_writer.flush()?;
549 self.appraisal_results_time_slice_writer.flush()?;
550 self.dispatch_asset_writer.flush()?;
551
552 Ok(())
553 }
554}
555
556pub struct DataWriter {
558 assets: csv::Writer<File>,
559 asset_capacities: csv::Writer<File>,
560 flows: csv::Writer<File>,
561 prices: csv::Writer<File>,
562 debug: Option<DebugDataWriter>,
563}
564
565impl DataWriter {
566 pub fn create(output_path: &Path, model_path: &Path, save_debug_info: bool) -> Result<Self> {
574 write_metadata(output_path, model_path).context("Failed to save metadata")?;
575
576 let new_writer = |file_name| {
577 let file_path = output_path.join(file_name);
578 csv::Writer::from_path(file_path)
579 };
580
581 let debug_writer = if save_debug_info {
582 Some(DebugDataWriter::create(output_path)?)
584 } else {
585 None
586 };
587
588 Ok(Self {
589 assets: new_writer(ASSETS_FILE_NAME)?,
590 asset_capacities: new_writer(ASSET_CAPACITIES_FILE_NAME)?,
591 flows: new_writer(COMMODITY_FLOWS_FILE_NAME)?,
592 prices: new_writer(COMMODITY_PRICES_FILE_NAME)?,
593 debug: debug_writer,
594 })
595 }
596
597 pub fn write_dispatch_debug_info(
599 &mut self,
600 milestone_year: u32,
601 run_description: &str,
602 solution: &Solution,
603 ) -> Result<()> {
604 if let Some(wtr) = &mut self.debug {
605 wtr.write_dispatch_debug_info(milestone_year, run_description, solution)?;
606 }
607
608 Ok(())
609 }
610
611 pub fn write_appraisal_debug_info(
613 &mut self,
614 milestone_year: u32,
615 run_description: &str,
616 appraisal_results: &[AppraisalOutput],
617 demand: &IndexMap<TimeSliceSelection, Flow>,
618 time_slice_level: TimeSliceLevel,
619 ) -> Result<()> {
620 if let Some(wtr) = &mut self.debug {
621 wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
622 wtr.write_appraisal_time_slice_results(
623 milestone_year,
624 run_description,
625 appraisal_results,
626 demand,
627 time_slice_level,
628 )?;
629 }
630
631 Ok(())
632 }
633
634 pub fn write_assets(&mut self, assets: &[AssetRef]) -> Result<()> {
636 for asset in assets {
637 self.assets.serialize(AssetRow::new(asset))?;
638 }
639
640 Ok(())
641 }
642
643 pub fn write_asset_capacities(
645 &mut self,
646 milestone_year: u32,
647 assets: &[AssetRef],
648 ) -> Result<()> {
649 for asset in assets {
650 let row = AssetCapacityRow {
651 milestone_year,
652 asset_id: asset.id().unwrap(),
653 capacity: asset.total_capacity(),
654 num_tranches: asset.capacity().num_tranches(),
655 mothballed_capacity: asset.mothballed_capacity(),
656 mothballed_tranches: asset.get_num_mothballed_tranches(),
657 };
658 self.asset_capacities.serialize(row)?;
659 }
660
661 Ok(())
662 }
663
664 pub fn write_flows(&mut self, milestone_year: u32, flow_map: &FlowMap) -> Result<()> {
666 for ((asset, commodity_id, time_slice), flow) in flow_map {
667 let row = CommodityFlowRow {
668 milestone_year,
669 asset_id: asset.id().unwrap(),
670 commodity_id: commodity_id.clone(),
671 time_slice: time_slice.clone(),
672 flow: *flow,
673 };
674 self.flows.serialize(row)?;
675 }
676
677 Ok(())
678 }
679
680 pub fn write_prices(&mut self, milestone_year: u32, prices: &PriceMap) -> Result<()> {
682 for (commodity_id, region_id, time_slice, price) in prices.iter() {
683 let row = CommodityPriceRow {
684 milestone_year,
685 commodity_id: commodity_id.clone(),
686 region_id: region_id.clone(),
687 time_slice: time_slice.clone(),
688 price,
689 };
690 self.prices.serialize(row)?;
691 }
692
693 Ok(())
694 }
695
696 pub fn flush(&mut self) -> Result<()> {
698 self.assets.flush()?;
699 self.asset_capacities.flush()?;
700 self.flows.flush()?;
701 self.prices.flush()?;
702 if let Some(wtr) = &mut self.debug {
703 wtr.flush()?;
704 }
705
706 Ok(())
707 }
708
709 pub fn set_debug_context(&mut self, context: String) {
711 if let Some(wtr) = &mut self.debug {
712 wtr.context = Some(context);
713 }
714 }
715
716 pub fn clear_debug_context(&mut self) {
718 if let Some(wtr) = &mut self.debug {
719 wtr.context = None;
720 }
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use crate::asset::AssetPool;
728 use crate::fixture::{
729 appraisal_output, asset, assets, commodity_id, multi_tranche_asset, region_id, time_slice,
730 };
731 use crate::simulation::investment::appraisal::AppraisalOutput;
732 use crate::time_slice::{TimeSliceID, TimeSliceLevel, TimeSliceSelection};
733 use indexmap::indexmap;
734 use itertools::{Itertools, assert_equal};
735 use rstest::rstest;
736 use std::iter;
737 use tempfile::tempdir;
738
739 #[rstest]
740 fn write_assets(assets: AssetPool) {
741 let dir = tempdir().unwrap();
742
743 {
745 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
746 writer.write_assets(&assets).unwrap();
747 writer.flush().unwrap();
748 }
749
750 let asset = assets.iter().next().unwrap();
752 let expected = AssetRow::new(asset);
753 let records: Vec<AssetRow> = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME))
754 .unwrap()
755 .into_deserialize()
756 .try_collect()
757 .unwrap();
758 assert_equal(records, iter::once(expected));
759 }
760
761 #[rstest]
762 fn write_asset_capacities(assets: AssetPool) {
763 let milestone_year = 2020;
764 let dir = tempdir().unwrap();
765
766 {
768 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
769 writer
770 .write_asset_capacities(milestone_year, &assets)
771 .unwrap();
772 writer.flush().unwrap();
773 }
774
775 let asset = assets.iter().next().unwrap();
777 let expected = AssetCapacityRow {
778 milestone_year,
779 asset_id: asset.id().unwrap(),
780 capacity: asset.total_capacity(),
781 num_tranches: 1,
782 mothballed_capacity: Capacity(0.0),
783 mothballed_tranches: 0,
784 };
785 let records: Vec<AssetCapacityRow> =
786 csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
787 .unwrap()
788 .into_deserialize()
789 .try_collect()
790 .unwrap();
791 assert_equal(records, iter::once(expected));
792 }
793
794 #[rstest]
795 fn write_asset_capacities_with_mothballed_tranches(multi_tranche_asset: Asset) {
796 let milestone_year = 2020;
797 let dir = tempdir().unwrap();
798 let mut assets = AssetPool::new();
799 assets.commission_new(2010, &mut vec![multi_tranche_asset.into()]);
800 let asset = assets
801 .iter()
802 .next()
803 .unwrap()
804 .clone()
805 .with_mothballed_tranches(1, Some(milestone_year));
806
807 {
808 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
809 writer
810 .write_asset_capacities(milestone_year, std::slice::from_ref(&asset))
811 .unwrap();
812 writer.flush().unwrap();
813 }
814
815 let expected = AssetCapacityRow {
816 milestone_year,
817 asset_id: asset.id().unwrap(),
818 capacity: Capacity(12.0),
819 num_tranches: 3,
820 mothballed_capacity: Capacity(4.0),
821 mothballed_tranches: 1,
822 };
823 let records: Vec<AssetCapacityRow> =
824 csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
825 .unwrap()
826 .into_deserialize()
827 .try_collect()
828 .unwrap();
829 assert_equal(records, iter::once(expected));
830 }
831
832 #[rstest]
833 fn write_flows(assets: AssetPool, commodity_id: CommodityID, time_slice: TimeSliceID) {
834 let milestone_year = 2020;
835 let asset = assets.iter().next().unwrap();
836 let flow_map = indexmap! {
837 (asset.clone(), commodity_id.clone(), time_slice.clone()) => Flow(42.0)
838 };
839
840 let dir = tempdir().unwrap();
842 {
843 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
844 writer.write_flows(milestone_year, &flow_map).unwrap();
845 writer.flush().unwrap();
846 }
847
848 let expected = CommodityFlowRow {
850 milestone_year,
851 asset_id: asset.id().unwrap(),
852 commodity_id,
853 time_slice,
854 flow: Flow(42.0),
855 };
856 let records: Vec<CommodityFlowRow> =
857 csv::Reader::from_path(dir.path().join(COMMODITY_FLOWS_FILE_NAME))
858 .unwrap()
859 .into_deserialize()
860 .try_collect()
861 .unwrap();
862 assert_equal(records, iter::once(expected));
863 }
864
865 #[rstest]
866 fn write_prices(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
867 let milestone_year = 2020;
868 let price = MoneyPerFlow(42.0);
869 let mut prices = PriceMap::default();
870 prices.insert(&commodity_id, ®ion_id, &time_slice, price);
871
872 let dir = tempdir().unwrap();
873
874 {
876 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
877 writer.write_prices(milestone_year, &prices).unwrap();
878 writer.flush().unwrap();
879 }
880
881 let expected = CommodityPriceRow {
883 milestone_year,
884 commodity_id,
885 region_id,
886 time_slice,
887 price,
888 };
889 let records: Vec<CommodityPriceRow> =
890 csv::Reader::from_path(dir.path().join(COMMODITY_PRICES_FILE_NAME))
891 .unwrap()
892 .into_deserialize()
893 .try_collect()
894 .unwrap();
895 assert_equal(records, iter::once(expected));
896 }
897
898 #[rstest]
899 fn write_commodity_balance_duals(
900 commodity_id: CommodityID,
901 region_id: RegionID,
902 time_slice: TimeSliceID,
903 ) {
904 let milestone_year = 2020;
905 let run_description = "test_run".to_string();
906 let value = MoneyPerFlow(0.5);
907 let dir = tempdir().unwrap();
908
909 {
911 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
912 writer
913 .write_commodity_balance_duals(
914 milestone_year,
915 &run_description,
916 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
917 )
918 .unwrap();
919 writer.flush().unwrap();
920 }
921
922 let expected = CommodityBalanceDualsRow {
924 milestone_year,
925 run_description,
926 commodity_id,
927 region_id,
928 time_slice,
929 value,
930 };
931 let records: Vec<CommodityBalanceDualsRow> =
932 csv::Reader::from_path(dir.path().join(COMMODITY_BALANCE_DUALS_FILE_NAME))
933 .unwrap()
934 .into_deserialize()
935 .try_collect()
936 .unwrap();
937 assert_equal(records, iter::once(expected));
938 }
939
940 #[rstest]
941 fn write_unmet_demand(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
942 let milestone_year = 2020;
943 let run_description = "test_run".to_string();
944 let value = Flow(0.5);
945 let dir = tempdir().unwrap();
946
947 {
949 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
950 writer
951 .write_unmet_demand(
952 milestone_year,
953 &run_description,
954 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
955 )
956 .unwrap();
957 writer.flush().unwrap();
958 }
959
960 let expected = UnmetDemandRow {
962 milestone_year,
963 run_description,
964 commodity_id,
965 region_id,
966 time_slice,
967 value,
968 };
969 let records: Vec<UnmetDemandRow> =
970 csv::Reader::from_path(dir.path().join(UNMET_DEMAND_FILE_NAME))
971 .unwrap()
972 .into_deserialize()
973 .try_collect()
974 .unwrap();
975 assert_equal(records, iter::once(expected));
976 }
977
978 #[rstest]
979 fn write_dispatch(assets: AssetPool, time_slice: TimeSliceID) {
980 let milestone_year = 2020;
981 let run_description = "test_run".to_string();
982 let activity = Activity(100.5);
983 let activity_dual = MoneyPerActivity(-1.5);
984 let column_dual = MoneyPerActivity(5.0);
985 let dir = tempdir().unwrap();
986 let asset = assets.iter().next().unwrap();
987
988 {
990 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
991 writer
992 .write_dispatch(
993 milestone_year,
994 &run_description,
995 iter::once((asset, &time_slice, activity)),
996 iter::once((asset, &time_slice, activity_dual)),
997 iter::once((asset, &time_slice, column_dual)),
998 )
999 .unwrap();
1000 writer.flush().unwrap();
1001 }
1002
1003 let expected = DispatchRow {
1005 milestone_year,
1006 run_description,
1007 asset_id: asset.id(),
1008 process_id: asset.process_id().clone(),
1009 region_id: asset.region_id().clone(),
1010 time_slice,
1011 activity: Some(activity),
1012 activity_dual: Some(activity_dual),
1013 column_dual: Some(column_dual),
1014 };
1015 let records: Vec<DispatchRow> =
1016 csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1017 .unwrap()
1018 .into_deserialize()
1019 .try_collect()
1020 .unwrap();
1021 assert_equal(records, iter::once(expected));
1022 }
1023
1024 #[rstest]
1025 fn write_dispatch_with_missing_keys(assets: AssetPool, time_slice: TimeSliceID) {
1026 let milestone_year = 2020;
1027 let run_description = "test_run".to_string();
1028 let activity = Activity(100.5);
1029 let dir = tempdir().unwrap();
1030 let asset = assets.iter().next().unwrap();
1031
1032 {
1034 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1035 writer
1036 .write_dispatch(
1037 milestone_year,
1038 &run_description,
1039 iter::once((asset, &time_slice, activity)),
1040 iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
1041 iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
1042 )
1043 .unwrap();
1044 writer.flush().unwrap();
1045 }
1046
1047 let expected = DispatchRow {
1049 milestone_year,
1050 run_description,
1051 asset_id: asset.id(),
1052 process_id: asset.process_id().clone(),
1053 region_id: asset.region_id().clone(),
1054 time_slice,
1055 activity: Some(activity),
1056 activity_dual: None,
1057 column_dual: None,
1058 };
1059 let records: Vec<DispatchRow> =
1060 csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1061 .unwrap()
1062 .into_deserialize()
1063 .try_collect()
1064 .unwrap();
1065 assert_equal(records, iter::once(expected));
1066 }
1067
1068 #[rstest]
1069 fn write_solver_values() {
1070 let milestone_year = 2020;
1071 let run_description = "test_run".to_string();
1072 let objective_value = Money(1234.56);
1073 let dir = tempdir().unwrap();
1074
1075 {
1077 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1078 writer
1079 .write_solver_values(milestone_year, &run_description, objective_value)
1080 .unwrap();
1081 writer.flush().unwrap();
1082 }
1083
1084 let expected = SolverValuesRow {
1086 milestone_year,
1087 run_description,
1088 objective_value,
1089 };
1090 let records: Vec<SolverValuesRow> =
1091 csv::Reader::from_path(dir.path().join(SOLVER_VALUES_FILE_NAME))
1092 .unwrap()
1093 .into_deserialize()
1094 .try_collect()
1095 .unwrap();
1096 assert_equal(records, iter::once(expected));
1097 }
1098
1099 #[rstest]
1100 fn write_appraisal_results(asset: Asset, appraisal_output: AppraisalOutput) {
1101 let milestone_year = 2020;
1102 let run_description = "test_run".to_string();
1103 let dir = tempdir().unwrap();
1104
1105 {
1107 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1108 writer
1109 .write_appraisal_results(milestone_year, &run_description, &[appraisal_output])
1110 .unwrap();
1111 writer.flush().unwrap();
1112 }
1113
1114 let expected = AppraisalResultsRow {
1116 milestone_year,
1117 run_description,
1118 asset_id: None,
1119 process_id: asset.process_id().clone(),
1120 region_id: asset.region_id().clone(),
1121 capacity: Capacity(2.0),
1122 metric: Some(4.14),
1123 };
1124 let records: Vec<AppraisalResultsRow> =
1125 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
1126 .unwrap()
1127 .into_deserialize()
1128 .try_collect()
1129 .unwrap();
1130 assert_equal(records, iter::once(expected));
1131 }
1132
1133 #[rstest]
1134 fn write_appraisal_time_slice_results(
1135 asset: Asset,
1136 appraisal_output: AppraisalOutput,
1137 time_slice: TimeSliceID,
1138 ) {
1139 let milestone_year = 2020;
1140 let run_description = "test_run".to_string();
1141 let dir = tempdir().unwrap();
1142 let demand = indexmap! {TimeSliceSelection::Single(time_slice.clone()) => Flow(100.0) };
1143
1144 {
1146 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1147 writer
1148 .write_appraisal_time_slice_results(
1149 milestone_year,
1150 &run_description,
1151 &[appraisal_output],
1152 &demand,
1153 TimeSliceLevel::DayNight,
1154 )
1155 .unwrap();
1156 writer.flush().unwrap();
1157 }
1158
1159 let expected = AppraisalResultsTimeSliceRow {
1161 milestone_year,
1162 run_description,
1163 asset_id: None,
1164 process_id: asset.process_id().clone(),
1165 region_id: asset.region_id().clone(),
1166 time_slice: time_slice.clone(),
1167 activity: Activity(10.0),
1168 activity_coefficient: MoneyPerActivity(0.5),
1169 time_slice_level: TimeSliceLevel::DayNight,
1170 demand_for_selection: Flow(100.0),
1171 unmet_demand_for_selection: Flow(5.0),
1172 };
1173 let records: Vec<AppraisalResultsTimeSliceRow> =
1174 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME))
1175 .unwrap()
1176 .into_deserialize()
1177 .try_collect()
1178 .unwrap();
1179 assert_equal(records, iter::once(expected));
1180 }
1181
1182 #[test]
1183 fn create_output_directory_new_directory() {
1184 let temp_dir = tempdir().unwrap();
1185 let output_dir = temp_dir.path().join("new_output");
1186
1187 let result = create_output_directory(&output_dir, false).unwrap();
1189 assert!(!result);
1190 assert!(output_dir.exists());
1191 assert!(output_dir.is_dir());
1192 }
1193
1194 #[test]
1195 fn create_output_directory_existing_empty_directory() {
1196 let temp_dir = tempdir().unwrap();
1197 let output_dir = temp_dir.path().join("empty_output");
1198
1199 fs::create_dir(&output_dir).unwrap();
1201
1202 let result = create_output_directory(&output_dir, false).unwrap();
1204 assert!(!result);
1205 assert!(output_dir.exists());
1206 assert!(output_dir.is_dir());
1207 }
1208
1209 #[test]
1210 fn create_output_directory_existing_with_files_no_overwrite() {
1211 let temp_dir = tempdir().unwrap();
1212 let output_dir = temp_dir.path().join("output_with_files");
1213
1214 fs::create_dir(&output_dir).unwrap();
1216 fs::write(output_dir.join("existing_file.txt"), "some content").unwrap();
1217
1218 let result = create_output_directory(&output_dir, false);
1220 assert!(result.is_err());
1221 assert!(
1222 result
1223 .unwrap_err()
1224 .to_string()
1225 .contains("Output folder already exists")
1226 );
1227 }
1228
1229 #[test]
1230 fn create_output_directory_existing_with_files_allow_overwrite() {
1231 let temp_dir = tempdir().unwrap();
1232 let output_dir = temp_dir.path().join("output_with_files");
1233
1234 fs::create_dir(&output_dir).unwrap();
1236 let file_path = output_dir.join("existing_file.txt");
1237 fs::write(&file_path, "some content").unwrap();
1238
1239 let result = create_output_directory(&output_dir, true).unwrap();
1241 assert!(result);
1242 assert!(output_dir.exists());
1243 assert!(output_dir.is_dir());
1244 assert!(!file_path.exists()); }
1246
1247 #[test]
1248 fn create_output_directory_nested_path() {
1249 let temp_dir = tempdir().unwrap();
1250 let output_dir = temp_dir.path().join("nested").join("path").join("output");
1251
1252 let result = create_output_directory(&output_dir, false).unwrap();
1254 assert!(!result);
1255 assert!(output_dir.exists());
1256 assert!(output_dir.is_dir());
1257 }
1258
1259 #[test]
1260 fn create_output_directory_existing_subdirs_with_files_allow_overwrite() {
1261 let temp_dir = tempdir().unwrap();
1262 let output_dir = temp_dir.path().join("output_with_subdirs");
1263
1264 fs::create_dir_all(output_dir.join("subdir")).unwrap();
1266 fs::write(output_dir.join("file1.txt"), "content1").unwrap();
1267 fs::write(output_dir.join("subdir").join("file2.txt"), "content2").unwrap();
1268
1269 let result = create_output_directory(&output_dir, true).unwrap();
1271 assert!(result);
1272 assert!(output_dir.exists());
1273 assert!(output_dir.is_dir());
1274 assert!(!output_dir.join("file1.txt").exists());
1276 assert!(!output_dir.join("subdir").exists());
1277 }
1278}