muse2/
output.rs

1//! The module responsible for writing output data to disk.
2use 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
24/// The root folder in which model-specific output folders will be created
25const OUTPUT_DIRECTORY_ROOT: &str = "muse2_results";
26
27/// The output file name for commodity flows
28const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv";
29
30/// The output file name for commodity prices
31const COMMODITY_PRICES_FILE_NAME: &str = "commodity_prices.csv";
32
33/// The output file name for assets
34const ASSETS_FILE_NAME: &str = "assets.csv";
35
36/// The output file name for raw activity
37const ACTIVITY_FILE_NAME: &str = "debug_activity.csv";
38
39/// The output file name for commodity balance duals
40const COMMODITY_BALANCE_DUALS_FILE_NAME: &str = "debug_commodity_balance_duals.csv";
41
42/// The output file name for activity duals
43const ACTIVITY_DUALS_FILE_NAME: &str = "debug_activity_duals.csv";
44
45/// The output file name for extra solver output values
46const SOLVER_VALUES_FILE_NAME: &str = "debug_solver.csv";
47
48/// The output file name for appraisal results
49const APPRAISAL_RESULTS_FILE_NAME: &str = "debug_appraisal_results.csv";
50
51/// The output file name for reduced costs
52const REDUCED_COSTS_FILE_NAME: &str = "debug_reduced_costs.csv";
53
54/// Get the model name from the specified directory path
55pub fn get_output_dir(model_dir: &Path) -> Result<PathBuf> {
56    // Get the model name from the dir path. This ends up being convoluted because we need to check
57    // for all possible errors. Ugh.
58    let model_dir = model_dir
59        .canonicalize() // canonicalise in case the user has specified "."
60        .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    // Construct path
69    Ok([OUTPUT_DIRECTORY_ROOT, model_name].iter().collect())
70}
71
72/// Create a new output directory for the model specified at `model_dir`.
73pub fn create_output_directory(output_dir: &Path) -> Result<()> {
74    if output_dir.is_dir() {
75        // already exists
76        return Ok(());
77    }
78
79    // Try to create the directory, with parents
80    fs::create_dir_all(output_dir)?;
81
82    Ok(())
83}
84
85/// Represents a row in the assets output CSV file.
86#[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    /// Create a new [`AssetRow`]
99    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/// Represents the flow-related data in a row of the commodity flows CSV file.
113#[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/// Represents a row in the commodity prices CSV file
123#[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/// Represents the activity in a row of the activity CSV file
133#[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/// Represents the activity duals data in a row of the activity duals CSV file
145#[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/// Represents the commodity balance duals data in a row of the commodity balance duals CSV file
157#[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/// Represents solver output values
168#[derive(Serialize, Deserialize, Debug, PartialEq)]
169struct SolverValuesRow {
170    milestone_year: u32,
171    run_description: String,
172    objective_value: Money,
173}
174
175/// Represents the appraisal results in a row of the appraisal results CSV file
176#[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/// Represents the reduced costs in a row of the reduced costs CSV file
189#[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
199/// For writing extra debug information about the model
200struct 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    /// Open CSV files to write debug info to
212    ///
213    /// # Arguments
214    ///
215    /// * `output_path` - Folder where files will be saved
216    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    /// Prepend the current context to the run description
234    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    /// Write debug info about the dispatch optimisation
243    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    // Write activity to file
265    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    /// Write activity duals to file
291    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    /// Write commodity balance duals to file
317    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    /// Write additional solver output values to file
342    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    /// Write appraisal results to file
360    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    /// Write reduced costs to file
384    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    /// Flush the underlying streams
405    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
417/// An object for writing commodity prices to file
418pub 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    /// Open CSV files to write output data to
427    ///
428    /// # Arguments
429    ///
430    /// * `output_path` - Folder where files will be saved
431    /// * `model_path` - Path to input model
432    /// * `save_debug_info` - Whether to include extra CSV files for debugging model
433    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            // Create debug CSV files
443            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    /// Write debug info about the dispatch optimisation
457    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    /// Write debug info about the investment appraisal
471    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    /// Write assets to a CSV file.
485    ///
486    /// The whole file is written at once and is overwritten with subsequent invocations. This is
487    /// done so that partial results will be written in the case of errors and so that the user can
488    /// see the results while the simulation is still running.
489    ///
490    /// The file is sorted by asset ID.
491    ///
492    /// # Panics
493    ///
494    /// Panics if any of the assets has not yet been commissioned (decommissioned assets are fine).
495    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    /// Write commodity flows to a CSV file
510    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    /// Write commodity prices to a CSV file
526    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    /// Write reduced costs to a CSV file
542    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    /// Flush the underlying streams
554    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    /// Add context to the debug writer
565    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    /// Clear context from the debug writer
572    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        // Write an asset
596        {
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        // Read back and compare
603        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        // Write a flow
622        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        // Read back and compare
630        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, &region_id, &time_slice, price);
652
653        let dir = tempdir().unwrap();
654
655        // Write a price
656        {
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        // Read back and compare
663        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        // Write commodity balance dual
691        {
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, &region_id, &time_slice, value)),
698                )
699                .unwrap();
700            writer.flush().unwrap();
701        }
702
703        // Read back and compare
704        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        // Write activity dual
730        {
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        // Read back and compare
743        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        // Write activity
770        {
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        // Read back and compare
783        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        // Write solver values
808        {
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        // Read back and compare
817        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        // Write appraisal results
839        {
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        // Read back and compare
854        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        // Write reduced costs
880        {
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        // Read back and compare
893        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}