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::CommodityPrices;
8use crate::simulation::investment::appraisal::AppraisalOutput;
9use crate::simulation::optimisation::{FlowMap, Solution};
10use crate::simulation::prices::ReducedCosts;
11use crate::time_slice::TimeSliceID;
12use crate::units::{Activity, Capacity, Flow, Money, MoneyPerActivity, MoneyPerFlow};
13use anyhow::{Context, Result};
14use csv;
15use itertools::Itertools;
16use serde::{Deserialize, Serialize};
17use std::fs;
18use std::fs::File;
19use std::path::{Path, PathBuf};
20
21pub mod metadata;
22use metadata::write_metadata;
23
24const OUTPUT_DIRECTORY_ROOT: &str = "muse2_results";
26
27const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv";
29
30const COMMODITY_PRICES_FILE_NAME: &str = "commodity_prices.csv";
32
33const ASSETS_FILE_NAME: &str = "assets.csv";
35
36const ACTIVITY_FILE_NAME: &str = "debug_activity.csv";
38
39const COMMODITY_BALANCE_DUALS_FILE_NAME: &str = "debug_commodity_balance_duals.csv";
41
42const ACTIVITY_DUALS_FILE_NAME: &str = "debug_activity_duals.csv";
44
45const SOLVER_VALUES_FILE_NAME: &str = "debug_solver.csv";
47
48const APPRAISAL_RESULTS_FILE_NAME: &str = "debug_appraisal_results.csv";
50
51const REDUCED_COSTS_FILE_NAME: &str = "debug_reduced_costs.csv";
53
54pub fn get_output_dir(model_dir: &Path) -> Result<PathBuf> {
56 let model_dir = model_dir
59 .canonicalize() .context("Could not resolve path to model")?;
61
62 let model_name = model_dir
63 .file_name()
64 .context("Model cannot be in root folder")?
65 .to_str()
66 .context("Invalid chars in model dir name")?;
67
68 Ok([OUTPUT_DIRECTORY_ROOT, model_name].iter().collect())
70}
71
72pub fn create_output_directory(output_dir: &Path) -> Result<()> {
74 if output_dir.is_dir() {
75 return Ok(());
77 }
78
79 fs::create_dir_all(output_dir)?;
81
82 Ok(())
83}
84
85#[derive(Serialize, Deserialize, Debug, PartialEq)]
87struct AssetRow {
88 asset_id: AssetID,
89 process_id: ProcessID,
90 region_id: RegionID,
91 agent_id: AgentID,
92 commission_year: u32,
93 decommission_year: Option<u32>,
94 capacity: Capacity,
95}
96
97impl AssetRow {
98 fn new(asset: &Asset) -> Self {
100 Self {
101 asset_id: asset.id().unwrap(),
102 process_id: asset.process_id().clone(),
103 region_id: asset.region_id().clone(),
104 agent_id: asset.agent_id().unwrap().clone(),
105 commission_year: asset.commission_year(),
106 decommission_year: asset.decommission_year(),
107 capacity: asset.capacity(),
108 }
109 }
110}
111
112#[derive(Serialize, Deserialize, Debug, PartialEq)]
114struct CommodityFlowRow {
115 milestone_year: u32,
116 asset_id: AssetID,
117 commodity_id: CommodityID,
118 time_slice: TimeSliceID,
119 flow: Flow,
120}
121
122#[derive(Serialize, Deserialize, Debug, PartialEq)]
124struct CommodityPriceRow {
125 milestone_year: u32,
126 commodity_id: CommodityID,
127 region_id: RegionID,
128 time_slice: TimeSliceID,
129 price: MoneyPerFlow,
130}
131
132#[derive(Serialize, Deserialize, Debug, PartialEq)]
134struct ActivityRow {
135 milestone_year: u32,
136 run_description: String,
137 asset_id: Option<AssetID>,
138 process_id: ProcessID,
139 region_id: RegionID,
140 time_slice: TimeSliceID,
141 activity: Activity,
142}
143
144#[derive(Serialize, Deserialize, Debug, PartialEq)]
146struct ActivityDualsRow {
147 milestone_year: u32,
148 run_description: String,
149 asset_id: Option<AssetID>,
150 process_id: ProcessID,
151 region_id: RegionID,
152 time_slice: TimeSliceID,
153 value: MoneyPerActivity,
154}
155
156#[derive(Serialize, Deserialize, Debug, PartialEq)]
158struct CommodityBalanceDualsRow {
159 milestone_year: u32,
160 run_description: String,
161 commodity_id: CommodityID,
162 region_id: RegionID,
163 time_slice: TimeSliceID,
164 value: MoneyPerFlow,
165}
166
167#[derive(Serialize, Deserialize, Debug, PartialEq)]
169struct SolverValuesRow {
170 milestone_year: u32,
171 run_description: String,
172 objective_value: Money,
173}
174
175#[derive(Serialize, Deserialize, Debug, PartialEq)]
177struct AppraisalResultsRow {
178 milestone_year: u32,
179 run_description: String,
180 asset_id: Option<AssetID>,
181 process_id: ProcessID,
182 region_id: RegionID,
183 capacity: Capacity,
184 unmet_demand: Flow,
185 metric: f64,
186}
187
188#[derive(Serialize, Deserialize, Debug, PartialEq)]
190struct ReducedCostsRow {
191 milestone_year: u32,
192 asset_id: Option<AssetID>,
193 process_id: ProcessID,
194 region_id: RegionID,
195 time_slice: TimeSliceID,
196 reduced_cost: MoneyPerActivity,
197}
198
199struct DebugDataWriter {
201 context: Option<String>,
202 activity_writer: csv::Writer<File>,
203 commodity_balance_duals_writer: csv::Writer<File>,
204 activity_duals_writer: csv::Writer<File>,
205 solver_values_writer: csv::Writer<File>,
206 appraisal_results_writer: csv::Writer<File>,
207 reduced_costs_writer: csv::Writer<File>,
208}
209
210impl DebugDataWriter {
211 fn create(output_path: &Path) -> Result<Self> {
217 let new_writer = |file_name| {
218 let file_path = output_path.join(file_name);
219 csv::Writer::from_path(file_path)
220 };
221
222 Ok(Self {
223 context: None,
224 activity_writer: new_writer(ACTIVITY_FILE_NAME)?,
225 commodity_balance_duals_writer: new_writer(COMMODITY_BALANCE_DUALS_FILE_NAME)?,
226 activity_duals_writer: new_writer(ACTIVITY_DUALS_FILE_NAME)?,
227 solver_values_writer: new_writer(SOLVER_VALUES_FILE_NAME)?,
228 appraisal_results_writer: new_writer(APPRAISAL_RESULTS_FILE_NAME)?,
229 reduced_costs_writer: new_writer(REDUCED_COSTS_FILE_NAME)?,
230 })
231 }
232
233 fn with_context(&self, run_description: &str) -> String {
235 if let Some(context) = &self.context {
236 format!("{context}; {run_description}")
237 } else {
238 run_description.to_string()
239 }
240 }
241
242 fn write_dispatch_debug_info(
244 &mut self,
245 milestone_year: u32,
246 run_description: &str,
247 solution: &Solution,
248 ) -> Result<()> {
249 self.write_activity(milestone_year, run_description, solution.iter_activity())?;
250 self.write_activity_duals(
251 milestone_year,
252 run_description,
253 solution.iter_activity_duals(),
254 )?;
255 self.write_commodity_balance_duals(
256 milestone_year,
257 run_description,
258 solution.iter_commodity_balance_duals(),
259 )?;
260 self.write_solver_values(milestone_year, run_description, solution.objective_value)?;
261 Ok(())
262 }
263
264 fn write_activity<'a, I>(
266 &mut self,
267 milestone_year: u32,
268 run_description: &str,
269 iter: I,
270 ) -> Result<()>
271 where
272 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
273 {
274 for (asset, time_slice, activity) in iter {
275 let row = ActivityRow {
276 milestone_year,
277 run_description: self.with_context(run_description),
278 asset_id: asset.id(),
279 process_id: asset.process_id().clone(),
280 region_id: asset.region_id().clone(),
281 time_slice: time_slice.clone(),
282 activity,
283 };
284 self.activity_writer.serialize(row)?;
285 }
286
287 Ok(())
288 }
289
290 fn write_activity_duals<'a, I>(
292 &mut self,
293 milestone_year: u32,
294 run_description: &str,
295 iter: I,
296 ) -> Result<()>
297 where
298 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
299 {
300 for (asset, time_slice, value) in iter {
301 let row = ActivityDualsRow {
302 milestone_year,
303 run_description: self.with_context(run_description),
304 asset_id: asset.id(),
305 process_id: asset.process_id().clone(),
306 region_id: asset.region_id().clone(),
307 time_slice: time_slice.clone(),
308 value,
309 };
310 self.activity_duals_writer.serialize(row)?;
311 }
312
313 Ok(())
314 }
315
316 fn write_commodity_balance_duals<'a, I>(
318 &mut self,
319 milestone_year: u32,
320 run_description: &str,
321 iter: I,
322 ) -> Result<()>
323 where
324 I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
325 {
326 for (commodity_id, region_id, time_slice, value) in iter {
327 let row = CommodityBalanceDualsRow {
328 milestone_year,
329 run_description: self.with_context(run_description),
330 commodity_id: commodity_id.clone(),
331 region_id: region_id.clone(),
332 time_slice: time_slice.clone(),
333 value,
334 };
335 self.commodity_balance_duals_writer.serialize(row)?;
336 }
337
338 Ok(())
339 }
340
341 fn write_solver_values(
343 &mut self,
344 milestone_year: u32,
345 run_description: &str,
346 objective_value: Money,
347 ) -> Result<()> {
348 let row = SolverValuesRow {
349 milestone_year,
350 run_description: self.with_context(run_description),
351 objective_value,
352 };
353 self.solver_values_writer.serialize(row)?;
354 self.solver_values_writer.flush()?;
355
356 Ok(())
357 }
358
359 fn write_appraisal_results(
361 &mut self,
362 milestone_year: u32,
363 run_description: &str,
364 appraisal_results: &[AppraisalOutput],
365 ) -> Result<()> {
366 for result in appraisal_results {
367 let row = AppraisalResultsRow {
368 milestone_year,
369 run_description: self.with_context(run_description),
370 asset_id: result.asset.id(),
371 process_id: result.asset.process_id().clone(),
372 region_id: result.asset.region_id().clone(),
373 capacity: result.capacity,
374 unmet_demand: result.unmet_demand.values().copied().sum(),
375 metric: result.metric,
376 };
377 self.appraisal_results_writer.serialize(row)?;
378 }
379
380 Ok(())
381 }
382
383 fn write_reduced_costs(
385 &mut self,
386 milestone_year: u32,
387 reduced_costs: &ReducedCosts,
388 ) -> Result<()> {
389 for ((asset, time_slice), reduced_cost) in reduced_costs.iter() {
390 let row = ReducedCostsRow {
391 milestone_year,
392 asset_id: asset.id(),
393 process_id: asset.process_id().clone(),
394 region_id: asset.region_id().clone(),
395 time_slice: time_slice.clone(),
396 reduced_cost: *reduced_cost,
397 };
398 self.reduced_costs_writer.serialize(row)?;
399 }
400
401 Ok(())
402 }
403
404 fn flush(&mut self) -> Result<()> {
406 self.activity_writer.flush()?;
407 self.commodity_balance_duals_writer.flush()?;
408 self.activity_duals_writer.flush()?;
409 self.solver_values_writer.flush()?;
410 self.appraisal_results_writer.flush()?;
411 self.reduced_costs_writer.flush()?;
412
413 Ok(())
414 }
415}
416
417pub struct DataWriter {
419 assets_path: PathBuf,
420 flows_writer: csv::Writer<File>,
421 prices_writer: csv::Writer<File>,
422 debug_writer: Option<DebugDataWriter>,
423}
424
425impl DataWriter {
426 pub fn create(output_path: &Path, model_path: &Path, save_debug_info: bool) -> Result<Self> {
434 write_metadata(output_path, model_path).context("Failed to save metadata")?;
435
436 let new_writer = |file_name| {
437 let file_path = output_path.join(file_name);
438 csv::Writer::from_path(file_path)
439 };
440
441 let debug_writer = if save_debug_info {
442 Some(DebugDataWriter::create(output_path)?)
444 } else {
445 None
446 };
447
448 Ok(Self {
449 assets_path: output_path.join(ASSETS_FILE_NAME),
450 flows_writer: new_writer(COMMODITY_FLOWS_FILE_NAME)?,
451 prices_writer: new_writer(COMMODITY_PRICES_FILE_NAME)?,
452 debug_writer,
453 })
454 }
455
456 pub fn write_dispatch_debug_info(
458 &mut self,
459 milestone_year: u32,
460 run_description: &str,
461 solution: &Solution,
462 ) -> Result<()> {
463 if let Some(wtr) = &mut self.debug_writer {
464 wtr.write_dispatch_debug_info(milestone_year, run_description, solution)?;
465 }
466
467 Ok(())
468 }
469
470 pub fn write_appraisal_debug_info(
472 &mut self,
473 milestone_year: u32,
474 run_description: &str,
475 appraisal_results: &[AppraisalOutput],
476 ) -> Result<()> {
477 if let Some(wtr) = &mut self.debug_writer {
478 wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
479 }
480
481 Ok(())
482 }
483
484 pub fn write_assets<'a, I>(&mut self, assets: I) -> Result<()>
496 where
497 I: Iterator<Item = &'a AssetRef>,
498 {
499 let mut writer = csv::Writer::from_path(&self.assets_path)?;
500 for asset in assets.sorted() {
501 let row = AssetRow::new(asset);
502 writer.serialize(row)?;
503 }
504 writer.flush()?;
505
506 Ok(())
507 }
508
509 pub fn write_flows(&mut self, milestone_year: u32, flow_map: &FlowMap) -> Result<()> {
511 for ((asset, commodity_id, time_slice), flow) in flow_map {
512 let row = CommodityFlowRow {
513 milestone_year,
514 asset_id: asset.id().unwrap(),
515 commodity_id: commodity_id.clone(),
516 time_slice: time_slice.clone(),
517 flow: *flow,
518 };
519 self.flows_writer.serialize(row)?;
520 }
521
522 Ok(())
523 }
524
525 pub fn write_prices(&mut self, milestone_year: u32, prices: &CommodityPrices) -> Result<()> {
527 for (commodity_id, region_id, time_slice, price) in prices.iter() {
528 let row = CommodityPriceRow {
529 milestone_year,
530 commodity_id: commodity_id.clone(),
531 region_id: region_id.clone(),
532 time_slice: time_slice.clone(),
533 price,
534 };
535 self.prices_writer.serialize(row)?;
536 }
537
538 Ok(())
539 }
540
541 pub fn write_debug_reduced_costs(
543 &mut self,
544 milestone_year: u32,
545 reduced_costs: &ReducedCosts,
546 ) -> Result<()> {
547 if let Some(wtr) = &mut self.debug_writer {
548 wtr.write_reduced_costs(milestone_year, reduced_costs)?;
549 }
550 Ok(())
551 }
552
553 pub fn flush(&mut self) -> Result<()> {
555 self.flows_writer.flush()?;
556 self.prices_writer.flush()?;
557 if let Some(wtr) = &mut self.debug_writer {
558 wtr.flush()?;
559 }
560
561 Ok(())
562 }
563
564 pub fn set_debug_context(&mut self, context: String) {
566 if let Some(wtr) = &mut self.debug_writer {
567 wtr.context = Some(context);
568 }
569 }
570
571 pub fn clear_debug_context(&mut self) {
573 if let Some(wtr) = &mut self.debug_writer {
574 wtr.context = None;
575 }
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::asset::AssetPool;
583 use crate::fixture::{assets, commodity_id, region_id, time_slice};
584 use crate::time_slice::TimeSliceID;
585 use indexmap::indexmap;
586 use itertools::{Itertools, assert_equal};
587 use rstest::rstest;
588 use std::iter;
589 use tempfile::tempdir;
590
591 #[rstest]
592 fn test_write_assets(assets: AssetPool) {
593 let dir = tempdir().unwrap();
594
595 {
597 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
598 writer.write_assets(assets.iter_active()).unwrap();
599 writer.flush().unwrap();
600 }
601
602 let asset = assets.iter_active().next().unwrap();
604 let expected = AssetRow::new(asset);
605 let records: Vec<AssetRow> = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME))
606 .unwrap()
607 .into_deserialize()
608 .try_collect()
609 .unwrap();
610 assert_equal(records, iter::once(expected));
611 }
612
613 #[rstest]
614 fn test_write_flows(assets: AssetPool, commodity_id: CommodityID, time_slice: TimeSliceID) {
615 let milestone_year = 2020;
616 let asset = assets.iter_active().next().unwrap();
617 let flow_map = indexmap! {
618 (asset.clone(), commodity_id.clone(), time_slice.clone()) => Flow(42.0)
619 };
620
621 let dir = tempdir().unwrap();
623 {
624 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
625 writer.write_flows(milestone_year, &flow_map).unwrap();
626 writer.flush().unwrap();
627 }
628
629 let expected = CommodityFlowRow {
631 milestone_year,
632 asset_id: asset.id().unwrap(),
633 commodity_id,
634 time_slice,
635 flow: Flow(42.0),
636 };
637 let records: Vec<CommodityFlowRow> =
638 csv::Reader::from_path(dir.path().join(COMMODITY_FLOWS_FILE_NAME))
639 .unwrap()
640 .into_deserialize()
641 .try_collect()
642 .unwrap();
643 assert_equal(records, iter::once(expected));
644 }
645
646 #[rstest]
647 fn test_write_prices(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
648 let milestone_year = 2020;
649 let price = MoneyPerFlow(42.0);
650 let mut prices = CommodityPrices::default();
651 prices.insert(&commodity_id, ®ion_id, &time_slice, price);
652
653 let dir = tempdir().unwrap();
654
655 {
657 let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
658 writer.write_prices(milestone_year, &prices).unwrap();
659 writer.flush().unwrap();
660 }
661
662 let expected = CommodityPriceRow {
664 milestone_year,
665 commodity_id,
666 region_id,
667 time_slice,
668 price,
669 };
670 let records: Vec<CommodityPriceRow> =
671 csv::Reader::from_path(dir.path().join(COMMODITY_PRICES_FILE_NAME))
672 .unwrap()
673 .into_deserialize()
674 .try_collect()
675 .unwrap();
676 assert_equal(records, iter::once(expected));
677 }
678
679 #[rstest]
680 fn test_write_commodity_balance_duals(
681 commodity_id: CommodityID,
682 region_id: RegionID,
683 time_slice: TimeSliceID,
684 ) {
685 let milestone_year = 2020;
686 let run_description = "test_run".to_string();
687 let value = MoneyPerFlow(0.5);
688 let dir = tempdir().unwrap();
689
690 {
692 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
693 writer
694 .write_commodity_balance_duals(
695 milestone_year,
696 &run_description,
697 iter::once((&commodity_id, ®ion_id, &time_slice, value)),
698 )
699 .unwrap();
700 writer.flush().unwrap();
701 }
702
703 let expected = CommodityBalanceDualsRow {
705 milestone_year,
706 run_description,
707 commodity_id,
708 region_id,
709 time_slice,
710 value,
711 };
712 let records: Vec<CommodityBalanceDualsRow> =
713 csv::Reader::from_path(dir.path().join(COMMODITY_BALANCE_DUALS_FILE_NAME))
714 .unwrap()
715 .into_deserialize()
716 .try_collect()
717 .unwrap();
718 assert_equal(records, iter::once(expected));
719 }
720
721 #[rstest]
722 fn test_write_activity_duals(assets: AssetPool, time_slice: TimeSliceID) {
723 let milestone_year = 2020;
724 let run_description = "test_run".to_string();
725 let value = MoneyPerActivity(0.5);
726 let dir = tempdir().unwrap();
727 let asset = assets.iter_active().next().unwrap();
728
729 {
731 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
732 writer
733 .write_activity_duals(
734 milestone_year,
735 &run_description,
736 iter::once((asset, &time_slice, value)),
737 )
738 .unwrap();
739 writer.flush().unwrap();
740 }
741
742 let expected = ActivityDualsRow {
744 milestone_year,
745 run_description,
746 asset_id: asset.id(),
747 process_id: asset.process_id().clone(),
748 region_id: asset.region_id().clone(),
749 time_slice,
750 value,
751 };
752 let records: Vec<ActivityDualsRow> =
753 csv::Reader::from_path(dir.path().join(ACTIVITY_DUALS_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 test_write_activity(assets: AssetPool, time_slice: TimeSliceID) {
763 let milestone_year = 2020;
764 let run_description = "test_run".to_string();
765 let activity = Activity(100.5);
766 let dir = tempdir().unwrap();
767 let asset = assets.iter_active().next().unwrap();
768
769 {
771 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
772 writer
773 .write_activity(
774 milestone_year,
775 &run_description,
776 iter::once((asset, &time_slice, activity)),
777 )
778 .unwrap();
779 writer.flush().unwrap();
780 }
781
782 let expected = ActivityRow {
784 milestone_year,
785 run_description,
786 asset_id: asset.id(),
787 process_id: asset.process_id().clone(),
788 region_id: asset.region_id().clone(),
789 time_slice,
790 activity,
791 };
792 let records: Vec<ActivityRow> = csv::Reader::from_path(dir.path().join(ACTIVITY_FILE_NAME))
793 .unwrap()
794 .into_deserialize()
795 .try_collect()
796 .unwrap();
797 assert_equal(records, iter::once(expected));
798 }
799
800 #[rstest]
801 fn test_write_solver_values() {
802 let milestone_year = 2020;
803 let run_description = "test_run".to_string();
804 let objective_value = Money(1234.56);
805 let dir = tempdir().unwrap();
806
807 {
809 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
810 writer
811 .write_solver_values(milestone_year, &run_description, objective_value)
812 .unwrap();
813 writer.flush().unwrap();
814 }
815
816 let expected = SolverValuesRow {
818 milestone_year,
819 run_description,
820 objective_value,
821 };
822 let records: Vec<SolverValuesRow> =
823 csv::Reader::from_path(dir.path().join(SOLVER_VALUES_FILE_NAME))
824 .unwrap()
825 .into_deserialize()
826 .try_collect()
827 .unwrap();
828 assert_equal(records, iter::once(expected));
829 }
830
831 #[rstest]
832 fn test_write_appraisal_results(assets: AssetPool) {
833 let milestone_year = 2020;
834 let run_description = "test_run".to_string();
835 let dir = tempdir().unwrap();
836 let asset = assets.iter_active().next().unwrap();
837
838 {
840 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
841 let appraisal = AppraisalOutput {
842 asset: asset.clone(),
843 capacity: Capacity(42.0),
844 unmet_demand: Default::default(),
845 metric: 4.14,
846 };
847 writer
848 .write_appraisal_results(milestone_year, &run_description, &[appraisal])
849 .unwrap();
850 writer.flush().unwrap();
851 }
852
853 let expected = AppraisalResultsRow {
855 milestone_year,
856 run_description,
857 asset_id: asset.id(),
858 process_id: asset.process_id().clone(),
859 region_id: asset.region_id().clone(),
860 capacity: Capacity(42.0),
861 unmet_demand: Flow(0.0),
862 metric: 4.14,
863 };
864 let records: Vec<AppraisalResultsRow> =
865 csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
866 .unwrap()
867 .into_deserialize()
868 .try_collect()
869 .unwrap();
870 assert_equal(records, iter::once(expected));
871 }
872
873 #[rstest]
874 fn test_write_reduced_costs(assets: AssetPool, time_slice: TimeSliceID) {
875 let milestone_year = 2020;
876 let dir = tempdir().unwrap();
877 let asset = assets.iter_active().next().unwrap();
878
879 {
881 let mut writer = DebugDataWriter::create(dir.path()).unwrap();
882 let reduced_costs = indexmap! {
883 (asset.clone(), time_slice.clone()) => MoneyPerActivity(0.5)
884 }
885 .into();
886 writer
887 .write_reduced_costs(milestone_year, &reduced_costs)
888 .unwrap();
889 writer.flush().unwrap();
890 }
891
892 let expected = ReducedCostsRow {
894 milestone_year,
895 asset_id: asset.id(),
896 process_id: asset.process_id().clone(),
897 region_id: asset.region_id().clone(),
898 time_slice,
899 reduced_cost: MoneyPerActivity(0.5),
900 };
901 let records: Vec<ReducedCostsRow> =
902 csv::Reader::from_path(dir.path().join(REDUCED_COSTS_FILE_NAME))
903 .unwrap()
904 .into_deserialize()
905 .try_collect()
906 .unwrap();
907 assert_equal(records, iter::once(expected));
908 }
909}