Skip to main content

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::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
23/// The output file name for commodity flows
24const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv";
25
26/// The output file name for commodity prices
27const COMMODITY_PRICES_FILE_NAME: &str = "commodity_prices.csv";
28
29/// The output file name for assets
30const ASSETS_FILE_NAME: &str = "assets.csv";
31
32/// The output file name for asset capacities
33const ASSET_CAPACITIES_FILE_NAME: &str = "asset_capacities.csv";
34
35/// Debug output file for asset dispatch
36const ACTIVITY_ASSET_DISPATCH: &str = "debug_dispatch_assets.csv";
37
38/// The output file name for commodity balance duals
39const COMMODITY_BALANCE_DUALS_FILE_NAME: &str = "debug_commodity_balance_duals.csv";
40
41/// The output file name for unmet demand values
42const UNMET_DEMAND_FILE_NAME: &str = "debug_unmet_demand.csv";
43
44/// The output file name for extra solver output values
45const SOLVER_VALUES_FILE_NAME: &str = "debug_solver.csv";
46
47/// The output file name for appraisal results
48const APPRAISAL_RESULTS_FILE_NAME: &str = "debug_appraisal_results.csv";
49
50/// The output file name for appraisal time slice results
51const APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME: &str = "debug_appraisal_results_time_slices.csv";
52
53/// Get the default output directory for the model
54pub fn get_output_dir(model_dir: &Path, results_root: PathBuf) -> Result<PathBuf> {
55    // Get the model name from the dir path. This ends up being convoluted because we need to check
56    // for all possible errors. Ugh.
57    let model_dir = model_dir
58        .canonicalize() // canonicalise in case the user has specified "."
59        .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    // Construct path
68    Ok([results_root, model_name.into()].iter().collect())
69}
70
71/// Get the default output directory for commodity flow graphs for the model
72pub fn get_graphs_dir(model_dir: &Path, graph_results_root: PathBuf) -> Result<PathBuf> {
73    let model_dir = model_dir
74        .canonicalize() // canonicalise in case the user has specified "."
75        .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
84/// Create a new output directory for the model, optionally overwriting existing data
85///
86/// # Arguments
87///
88/// * `output_dir` - The output directory to create/overwrite
89/// * `allow_overwrite` - Whether to delete and recreate the folder if it is non-empty
90///
91/// # Returns
92///
93/// True if the output dir contained existing data that was deleted, false if not, or an error.
94pub fn create_output_directory(output_dir: &Path, allow_overwrite: bool) -> Result<bool> {
95    // If the folder already exists, then delete it
96    let overwrite = if let Ok(mut it) = fs::read_dir(output_dir) {
97        if it.next().is_none() {
98            // Folder exists and is empty: nothing to do
99            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    // Try to create the directory, with parents
115    fs::create_dir_all(output_dir)?;
116
117    Ok(overwrite)
118}
119
120/// Copy input files to output directory
121pub fn copy_input_files(model_dir: &Path, output_dir: &Path, model_name: &str) -> Result<()> {
122    // Get the model name from the dir path.
123    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/// Represents a row in the assets output CSV file.
140#[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    /// Create a new [`AssetRow`] for the given asset
151    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/// Represents a row in the asset capacities output CSV file.
163#[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/// Represents the flow-related data in a row of the commodity flows CSV file.
174#[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/// Represents a row in the commodity prices CSV file
184#[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/// Represents the activity in a row of the dispatch CSV file
194#[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/// Represents the commodity balance duals data in a row of the commodity balance duals CSV file
208#[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/// Represents the unmet demand data in a row of the unmet demand CSV file
219#[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/// Represents solver output values
230#[derive(Serialize, Deserialize, Debug, PartialEq)]
231struct SolverValuesRow {
232    milestone_year: u32,
233    run_description: String,
234    objective_value: Money,
235}
236
237/// Represents the appraisal results in a row of the appraisal results CSV file
238#[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/// Represents the appraisal results in a row of the appraisal results CSV file
250#[derive(Serialize, Deserialize, Debug, PartialEq)]
251struct AppraisalResultsTimeSliceRow {
252    milestone_year: u32,
253    run_description: String,
254    asset_id: Option<AssetID>,
255    process_id: ProcessID,
256    region_id: RegionID,
257    time_slice: TimeSliceID,
258    activity: Activity,
259    activity_coefficient: MoneyPerActivity,
260    demand: Flow,
261    unmet_demand: Flow,
262}
263
264/// For writing extra debug information about the model
265struct DebugDataWriter {
266    context: Option<String>,
267    unmet_demand_file_path: PathBuf,
268    commodity_balance_duals_writer: csv::Writer<File>,
269    unmet_demand_writer: Option<csv::Writer<File>>,
270    solver_values_writer: csv::Writer<File>,
271    appraisal_results_writer: csv::Writer<File>,
272    appraisal_results_time_slice_writer: csv::Writer<File>,
273    dispatch_asset_writer: csv::Writer<File>,
274}
275
276impl DebugDataWriter {
277    /// Open CSV files to write debug info to
278    ///
279    /// # Arguments
280    ///
281    /// * `output_path` - Folder where files will be saved
282    fn create(output_path: &Path) -> Result<Self> {
283        let new_writer = |file_name| {
284            let file_path = output_path.join(file_name);
285            csv::Writer::from_path(file_path)
286        };
287
288        Ok(Self {
289            context: None,
290            unmet_demand_file_path: output_path.join(UNMET_DEMAND_FILE_NAME),
291            commodity_balance_duals_writer: new_writer(COMMODITY_BALANCE_DUALS_FILE_NAME)?,
292            unmet_demand_writer: None,
293            solver_values_writer: new_writer(SOLVER_VALUES_FILE_NAME)?,
294            appraisal_results_writer: new_writer(APPRAISAL_RESULTS_FILE_NAME)?,
295            appraisal_results_time_slice_writer: new_writer(
296                APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME,
297            )?,
298            dispatch_asset_writer: new_writer(ACTIVITY_ASSET_DISPATCH)?,
299        })
300    }
301
302    /// Prepend the current context to the run description
303    fn with_context(&self, run_description: &str) -> String {
304        if let Some(context) = &self.context {
305            format!("{context}; {run_description}")
306        } else {
307            run_description.to_string()
308        }
309    }
310
311    /// Write debug info about the dispatch optimisation
312    fn write_dispatch_debug_info(
313        &mut self,
314        milestone_year: u32,
315        run_description: &str,
316        solution: &Solution,
317    ) -> Result<()> {
318        self.write_dispatch(
319            milestone_year,
320            run_description,
321            solution.iter_activity(),
322            solution.iter_activity_duals(),
323            solution.iter_column_duals(),
324        )?;
325        self.write_commodity_balance_duals(
326            milestone_year,
327            run_description,
328            solution.iter_commodity_balance_duals(),
329        )?;
330        self.write_unmet_demand(
331            milestone_year,
332            run_description,
333            solution.iter_unmet_demand(),
334        )?;
335        self.write_solver_values(milestone_year, run_description, solution.objective_value)?;
336        Ok(())
337    }
338
339    // Write activity to file
340    fn write_dispatch<'a, I, J, K>(
341        &mut self,
342        milestone_year: u32,
343        run_description: &str,
344        iter_activity: I,
345        iter_activity_duals: J,
346        iter_column_duals: K,
347    ) -> Result<()>
348    where
349        I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
350        J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
351        K: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
352    {
353        // To account for different order of entries or missing ones, we first compile data in hash map
354        type CompiledActivityData = (
355            Option<Activity>,
356            Option<MoneyPerActivity>,
357            Option<MoneyPerActivity>,
358        );
359        let mut map: IndexMap<(&AssetRef, &TimeSliceID), CompiledActivityData> = IndexMap::new();
360
361        // For the activities
362        for (asset, time_slice, activity) in iter_activity {
363            map.entry((asset, time_slice)).or_default().0 = Some(activity);
364        }
365        // The activity duals
366        for (asset, time_slice, activity_dual) in iter_activity_duals {
367            map.entry((asset, time_slice)).or_default().1 = Some(activity_dual);
368        }
369        // And the column duals
370        for (asset, time_slice, column_dual) in iter_column_duals {
371            map.entry((asset, time_slice)).or_default().2 = Some(column_dual);
372        }
373
374        for ((asset, time_slice), (activity, activity_dual, column_dual)) in map {
375            let row = DispatchRow {
376                milestone_year,
377                run_description: self.with_context(run_description),
378                asset_id: asset.id(),
379                process_id: asset.process_id().clone(),
380                region_id: asset.region_id().clone(),
381                time_slice: time_slice.clone(),
382                activity,
383                activity_dual,
384                column_dual,
385            };
386            self.dispatch_asset_writer.serialize(row)?;
387        }
388
389        Ok(())
390    }
391
392    /// Write commodity balance duals to file
393    fn write_commodity_balance_duals<'a, I>(
394        &mut self,
395        milestone_year: u32,
396        run_description: &str,
397        iter: I,
398    ) -> Result<()>
399    where
400        I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
401    {
402        for (commodity_id, region_id, time_slice, value) in iter {
403            let row = CommodityBalanceDualsRow {
404                milestone_year,
405                run_description: self.with_context(run_description),
406                commodity_id: commodity_id.clone(),
407                region_id: region_id.clone(),
408                time_slice: time_slice.clone(),
409                value,
410            };
411            self.commodity_balance_duals_writer.serialize(row)?;
412        }
413
414        Ok(())
415    }
416
417    /// Write unmet demand values to file
418    fn write_unmet_demand<'a, I>(
419        &mut self,
420        milestone_year: u32,
421        run_description: &str,
422        iter: I,
423    ) -> Result<()>
424    where
425        I: Iterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, Flow)>,
426    {
427        let mut rows = iter.peekable();
428
429        if rows.peek().is_none() {
430            return Ok(());
431        }
432
433        // If the unmet demand writer already exist, we panic, as it should not happen
434        assert!(
435            self.unmet_demand_writer.is_none(),
436            "Unmet demand file already exists!"
437        );
438
439        let run_description = self.with_context(run_description);
440        let writer = self
441            .unmet_demand_writer
442            .insert(csv::Writer::from_path(&self.unmet_demand_file_path)?);
443        for (commodity_id, region_id, time_slice, value) in rows {
444            let row = UnmetDemandRow {
445                milestone_year,
446                run_description: run_description.clone(),
447                commodity_id: commodity_id.clone(),
448                region_id: region_id.clone(),
449                time_slice: time_slice.clone(),
450                value,
451            };
452            writer.serialize(row)?;
453        }
454
455        Ok(())
456    }
457
458    /// Write additional solver output values to file
459    fn write_solver_values(
460        &mut self,
461        milestone_year: u32,
462        run_description: &str,
463        objective_value: Money,
464    ) -> Result<()> {
465        let row = SolverValuesRow {
466            milestone_year,
467            run_description: self.with_context(run_description),
468            objective_value,
469        };
470        self.solver_values_writer.serialize(row)?;
471        self.solver_values_writer.flush()?;
472
473        Ok(())
474    }
475
476    /// Write appraisal results to file
477    fn write_appraisal_results(
478        &mut self,
479        milestone_year: u32,
480        run_description: &str,
481        appraisal_results: &[AppraisalOutput],
482    ) -> Result<()> {
483        for result in appraisal_results {
484            let row = AppraisalResultsRow {
485                milestone_year,
486                run_description: self.with_context(run_description),
487                asset_id: result.asset.id(),
488                process_id: result.asset.process_id().clone(),
489                region_id: result.asset.region_id().clone(),
490                capacity: result.asset.total_capacity(),
491                metric: result.metric.as_ref().map(|m| m.value()),
492            };
493            self.appraisal_results_writer.serialize(row)?;
494        }
495
496        Ok(())
497    }
498
499    /// Write appraisal results to file
500    fn write_appraisal_time_slice_results(
501        &mut self,
502        milestone_year: u32,
503        run_description: &str,
504        appraisal_results: &[AppraisalOutput],
505        demand: &IndexMap<TimeSliceID, Flow>,
506    ) -> Result<()> {
507        for result in appraisal_results {
508            for (time_slice, activity) in &result.activity {
509                let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
510                let demand = demand[time_slice];
511                let unmet_demand = result.unmet_demand[time_slice];
512                let row = AppraisalResultsTimeSliceRow {
513                    milestone_year,
514                    run_description: self.with_context(run_description),
515                    asset_id: result.asset.id(),
516                    process_id: result.asset.process_id().clone(),
517                    region_id: result.asset.region_id().clone(),
518                    time_slice: time_slice.clone(),
519                    activity: *activity,
520                    activity_coefficient,
521                    demand,
522                    unmet_demand,
523                };
524                self.appraisal_results_time_slice_writer.serialize(row)?;
525            }
526        }
527
528        Ok(())
529    }
530
531    /// Flush the underlying streams
532    fn flush(&mut self) -> Result<()> {
533        if let Some(wrt) = &mut self.unmet_demand_writer {
534            wrt.flush()?;
535        }
536        self.commodity_balance_duals_writer.flush()?;
537        self.solver_values_writer.flush()?;
538        self.appraisal_results_writer.flush()?;
539        self.appraisal_results_time_slice_writer.flush()?;
540        self.dispatch_asset_writer.flush()?;
541
542        Ok(())
543    }
544}
545
546/// An object for writing output data to file
547pub struct DataWriter {
548    assets: csv::Writer<File>,
549    asset_capacities: csv::Writer<File>,
550    flows: csv::Writer<File>,
551    prices: csv::Writer<File>,
552    debug: Option<DebugDataWriter>,
553}
554
555impl DataWriter {
556    /// Open CSV files to write output data to
557    ///
558    /// # Arguments
559    ///
560    /// * `output_path` - Folder where files will be saved
561    /// * `model_path` - Path to input model
562    /// * `save_debug_info` - Whether to include extra CSV files for debugging model
563    pub fn create(output_path: &Path, model_path: &Path, save_debug_info: bool) -> Result<Self> {
564        write_metadata(output_path, model_path).context("Failed to save metadata")?;
565
566        let new_writer = |file_name| {
567            let file_path = output_path.join(file_name);
568            csv::Writer::from_path(file_path)
569        };
570
571        let debug_writer = if save_debug_info {
572            // Create debug CSV files
573            Some(DebugDataWriter::create(output_path)?)
574        } else {
575            None
576        };
577
578        Ok(Self {
579            assets: new_writer(ASSETS_FILE_NAME)?,
580            asset_capacities: new_writer(ASSET_CAPACITIES_FILE_NAME)?,
581            flows: new_writer(COMMODITY_FLOWS_FILE_NAME)?,
582            prices: new_writer(COMMODITY_PRICES_FILE_NAME)?,
583            debug: debug_writer,
584        })
585    }
586
587    /// Write debug info about the dispatch optimisation
588    pub fn write_dispatch_debug_info(
589        &mut self,
590        milestone_year: u32,
591        run_description: &str,
592        solution: &Solution,
593    ) -> Result<()> {
594        if let Some(wtr) = &mut self.debug {
595            wtr.write_dispatch_debug_info(milestone_year, run_description, solution)?;
596        }
597
598        Ok(())
599    }
600
601    /// Write debug info about the investment appraisal
602    pub fn write_appraisal_debug_info(
603        &mut self,
604        milestone_year: u32,
605        run_description: &str,
606        appraisal_results: &[AppraisalOutput],
607        demand: &IndexMap<TimeSliceID, Flow>,
608    ) -> Result<()> {
609        if let Some(wtr) = &mut self.debug {
610            wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
611            wtr.write_appraisal_time_slice_results(
612                milestone_year,
613                run_description,
614                appraisal_results,
615                demand,
616            )?;
617        }
618
619        Ok(())
620    }
621
622    /// Append newly commissioned asset definitions to the assets CSV file
623    pub fn write_assets(&mut self, assets: &[AssetRef]) -> Result<()> {
624        for asset in assets {
625            self.assets.serialize(AssetRow::new(asset))?;
626        }
627
628        Ok(())
629    }
630
631    /// Write asset capacities for the current milestone year to a CSV file
632    pub fn write_asset_capacities(
633        &mut self,
634        milestone_year: u32,
635        assets: &[AssetRef],
636    ) -> Result<()> {
637        for asset in assets {
638            let row = AssetCapacityRow {
639                milestone_year,
640                asset_id: asset.id().unwrap(),
641                capacity: asset.total_capacity(),
642                num_tranches: asset.capacity().num_tranches(),
643                mothballed_capacity: asset.mothballed_capacity(),
644                mothballed_tranches: asset.get_num_mothballed_tranches(),
645            };
646            self.asset_capacities.serialize(row)?;
647        }
648
649        Ok(())
650    }
651
652    /// Write commodity flows to a CSV file
653    pub fn write_flows(&mut self, milestone_year: u32, flow_map: &FlowMap) -> Result<()> {
654        for ((asset, commodity_id, time_slice), flow) in flow_map {
655            let row = CommodityFlowRow {
656                milestone_year,
657                asset_id: asset.id().unwrap(),
658                commodity_id: commodity_id.clone(),
659                time_slice: time_slice.clone(),
660                flow: *flow,
661            };
662            self.flows.serialize(row)?;
663        }
664
665        Ok(())
666    }
667
668    /// Write commodity prices to a CSV file
669    pub fn write_prices(&mut self, milestone_year: u32, prices: &PriceMap) -> Result<()> {
670        for (commodity_id, region_id, time_slice, price) in prices.iter() {
671            let row = CommodityPriceRow {
672                milestone_year,
673                commodity_id: commodity_id.clone(),
674                region_id: region_id.clone(),
675                time_slice: time_slice.clone(),
676                price,
677            };
678            self.prices.serialize(row)?;
679        }
680
681        Ok(())
682    }
683
684    /// Flush the underlying streams
685    pub fn flush(&mut self) -> Result<()> {
686        self.assets.flush()?;
687        self.asset_capacities.flush()?;
688        self.flows.flush()?;
689        self.prices.flush()?;
690        if let Some(wtr) = &mut self.debug {
691            wtr.flush()?;
692        }
693
694        Ok(())
695    }
696
697    /// Add context to the debug writer
698    pub fn set_debug_context(&mut self, context: String) {
699        if let Some(wtr) = &mut self.debug {
700            wtr.context = Some(context);
701        }
702    }
703
704    /// Clear context from the debug writer
705    pub fn clear_debug_context(&mut self) {
706        if let Some(wtr) = &mut self.debug {
707            wtr.context = None;
708        }
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715    use crate::asset::AssetPool;
716    use crate::fixture::{
717        appraisal_output, asset, assets, commodity_id, multi_tranche_asset, region_id, time_slice,
718    };
719    use crate::simulation::investment::appraisal::AppraisalOutput;
720    use crate::time_slice::TimeSliceID;
721    use indexmap::indexmap;
722    use itertools::{Itertools, assert_equal};
723    use rstest::rstest;
724    use std::iter;
725    use tempfile::tempdir;
726
727    #[rstest]
728    fn write_assets(assets: AssetPool) {
729        let dir = tempdir().unwrap();
730
731        // Write an asset
732        {
733            let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
734            writer.write_assets(&assets).unwrap();
735            writer.flush().unwrap();
736        }
737
738        // Read back and compare
739        let asset = assets.iter().next().unwrap();
740        let expected = AssetRow::new(asset);
741        let records: Vec<AssetRow> = csv::Reader::from_path(dir.path().join(ASSETS_FILE_NAME))
742            .unwrap()
743            .into_deserialize()
744            .try_collect()
745            .unwrap();
746        assert_equal(records, iter::once(expected));
747    }
748
749    #[rstest]
750    fn write_asset_capacities(assets: AssetPool) {
751        let milestone_year = 2020;
752        let dir = tempdir().unwrap();
753
754        // Write asset capacities
755        {
756            let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
757            writer
758                .write_asset_capacities(milestone_year, &assets)
759                .unwrap();
760            writer.flush().unwrap();
761        }
762
763        // Read back and compare
764        let asset = assets.iter().next().unwrap();
765        let expected = AssetCapacityRow {
766            milestone_year,
767            asset_id: asset.id().unwrap(),
768            capacity: asset.total_capacity(),
769            num_tranches: 1,
770            mothballed_capacity: Capacity(0.0),
771            mothballed_tranches: 0,
772        };
773        let records: Vec<AssetCapacityRow> =
774            csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
775                .unwrap()
776                .into_deserialize()
777                .try_collect()
778                .unwrap();
779        assert_equal(records, iter::once(expected));
780    }
781
782    #[rstest]
783    fn write_asset_capacities_with_mothballed_tranches(multi_tranche_asset: Asset) {
784        let milestone_year = 2020;
785        let dir = tempdir().unwrap();
786        let mut assets = AssetPool::new();
787        assets.commission_new(2010, &mut vec![multi_tranche_asset.into()]);
788        let asset = assets
789            .iter()
790            .next()
791            .unwrap()
792            .clone()
793            .with_mothballed_tranches(1, Some(milestone_year));
794
795        {
796            let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
797            writer
798                .write_asset_capacities(milestone_year, std::slice::from_ref(&asset))
799                .unwrap();
800            writer.flush().unwrap();
801        }
802
803        let expected = AssetCapacityRow {
804            milestone_year,
805            asset_id: asset.id().unwrap(),
806            capacity: Capacity(12.0),
807            num_tranches: 3,
808            mothballed_capacity: Capacity(4.0),
809            mothballed_tranches: 1,
810        };
811        let records: Vec<AssetCapacityRow> =
812            csv::Reader::from_path(dir.path().join(ASSET_CAPACITIES_FILE_NAME))
813                .unwrap()
814                .into_deserialize()
815                .try_collect()
816                .unwrap();
817        assert_equal(records, iter::once(expected));
818    }
819
820    #[rstest]
821    fn write_flows(assets: AssetPool, commodity_id: CommodityID, time_slice: TimeSliceID) {
822        let milestone_year = 2020;
823        let asset = assets.iter().next().unwrap();
824        let flow_map = indexmap! {
825            (asset.clone(), commodity_id.clone(), time_slice.clone()) => Flow(42.0)
826        };
827
828        // Write a flow
829        let dir = tempdir().unwrap();
830        {
831            let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
832            writer.write_flows(milestone_year, &flow_map).unwrap();
833            writer.flush().unwrap();
834        }
835
836        // Read back and compare
837        let expected = CommodityFlowRow {
838            milestone_year,
839            asset_id: asset.id().unwrap(),
840            commodity_id,
841            time_slice,
842            flow: Flow(42.0),
843        };
844        let records: Vec<CommodityFlowRow> =
845            csv::Reader::from_path(dir.path().join(COMMODITY_FLOWS_FILE_NAME))
846                .unwrap()
847                .into_deserialize()
848                .try_collect()
849                .unwrap();
850        assert_equal(records, iter::once(expected));
851    }
852
853    #[rstest]
854    fn write_prices(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
855        let milestone_year = 2020;
856        let price = MoneyPerFlow(42.0);
857        let mut prices = PriceMap::default();
858        prices.insert(&commodity_id, &region_id, &time_slice, price);
859
860        let dir = tempdir().unwrap();
861
862        // Write a price
863        {
864            let mut writer = DataWriter::create(dir.path(), dir.path(), false).unwrap();
865            writer.write_prices(milestone_year, &prices).unwrap();
866            writer.flush().unwrap();
867        }
868
869        // Read back and compare
870        let expected = CommodityPriceRow {
871            milestone_year,
872            commodity_id,
873            region_id,
874            time_slice,
875            price,
876        };
877        let records: Vec<CommodityPriceRow> =
878            csv::Reader::from_path(dir.path().join(COMMODITY_PRICES_FILE_NAME))
879                .unwrap()
880                .into_deserialize()
881                .try_collect()
882                .unwrap();
883        assert_equal(records, iter::once(expected));
884    }
885
886    #[rstest]
887    fn write_commodity_balance_duals(
888        commodity_id: CommodityID,
889        region_id: RegionID,
890        time_slice: TimeSliceID,
891    ) {
892        let milestone_year = 2020;
893        let run_description = "test_run".to_string();
894        let value = MoneyPerFlow(0.5);
895        let dir = tempdir().unwrap();
896
897        // Write commodity balance dual
898        {
899            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
900            writer
901                .write_commodity_balance_duals(
902                    milestone_year,
903                    &run_description,
904                    iter::once((&commodity_id, &region_id, &time_slice, value)),
905                )
906                .unwrap();
907            writer.flush().unwrap();
908        }
909
910        // Read back and compare
911        let expected = CommodityBalanceDualsRow {
912            milestone_year,
913            run_description,
914            commodity_id,
915            region_id,
916            time_slice,
917            value,
918        };
919        let records: Vec<CommodityBalanceDualsRow> =
920            csv::Reader::from_path(dir.path().join(COMMODITY_BALANCE_DUALS_FILE_NAME))
921                .unwrap()
922                .into_deserialize()
923                .try_collect()
924                .unwrap();
925        assert_equal(records, iter::once(expected));
926    }
927
928    #[rstest]
929    fn write_unmet_demand(commodity_id: CommodityID, region_id: RegionID, time_slice: TimeSliceID) {
930        let milestone_year = 2020;
931        let run_description = "test_run".to_string();
932        let value = Flow(0.5);
933        let dir = tempdir().unwrap();
934
935        // Write unmet demand
936        {
937            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
938            writer
939                .write_unmet_demand(
940                    milestone_year,
941                    &run_description,
942                    iter::once((&commodity_id, &region_id, &time_slice, value)),
943                )
944                .unwrap();
945            writer.flush().unwrap();
946        }
947
948        // Read back and compare
949        let expected = UnmetDemandRow {
950            milestone_year,
951            run_description,
952            commodity_id,
953            region_id,
954            time_slice,
955            value,
956        };
957        let records: Vec<UnmetDemandRow> =
958            csv::Reader::from_path(dir.path().join(UNMET_DEMAND_FILE_NAME))
959                .unwrap()
960                .into_deserialize()
961                .try_collect()
962                .unwrap();
963        assert_equal(records, iter::once(expected));
964    }
965
966    #[rstest]
967    fn write_dispatch(assets: AssetPool, time_slice: TimeSliceID) {
968        let milestone_year = 2020;
969        let run_description = "test_run".to_string();
970        let activity = Activity(100.5);
971        let activity_dual = MoneyPerActivity(-1.5);
972        let column_dual = MoneyPerActivity(5.0);
973        let dir = tempdir().unwrap();
974        let asset = assets.iter().next().unwrap();
975
976        // Write activity
977        {
978            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
979            writer
980                .write_dispatch(
981                    milestone_year,
982                    &run_description,
983                    iter::once((asset, &time_slice, activity)),
984                    iter::once((asset, &time_slice, activity_dual)),
985                    iter::once((asset, &time_slice, column_dual)),
986                )
987                .unwrap();
988            writer.flush().unwrap();
989        }
990
991        // Read back and compare
992        let expected = DispatchRow {
993            milestone_year,
994            run_description,
995            asset_id: asset.id(),
996            process_id: asset.process_id().clone(),
997            region_id: asset.region_id().clone(),
998            time_slice,
999            activity: Some(activity),
1000            activity_dual: Some(activity_dual),
1001            column_dual: Some(column_dual),
1002        };
1003        let records: Vec<DispatchRow> =
1004            csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1005                .unwrap()
1006                .into_deserialize()
1007                .try_collect()
1008                .unwrap();
1009        assert_equal(records, iter::once(expected));
1010    }
1011
1012    #[rstest]
1013    fn write_dispatch_with_missing_keys(assets: AssetPool, time_slice: TimeSliceID) {
1014        let milestone_year = 2020;
1015        let run_description = "test_run".to_string();
1016        let activity = Activity(100.5);
1017        let dir = tempdir().unwrap();
1018        let asset = assets.iter().next().unwrap();
1019
1020        // Write activity
1021        {
1022            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1023            writer
1024                .write_dispatch(
1025                    milestone_year,
1026                    &run_description,
1027                    iter::once((asset, &time_slice, activity)),
1028                    iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
1029                    iter::empty::<(&AssetRef, &TimeSliceID, MoneyPerActivity)>(),
1030                )
1031                .unwrap();
1032            writer.flush().unwrap();
1033        }
1034
1035        // Read back and compare
1036        let expected = DispatchRow {
1037            milestone_year,
1038            run_description,
1039            asset_id: asset.id(),
1040            process_id: asset.process_id().clone(),
1041            region_id: asset.region_id().clone(),
1042            time_slice,
1043            activity: Some(activity),
1044            activity_dual: None,
1045            column_dual: None,
1046        };
1047        let records: Vec<DispatchRow> =
1048            csv::Reader::from_path(dir.path().join(ACTIVITY_ASSET_DISPATCH))
1049                .unwrap()
1050                .into_deserialize()
1051                .try_collect()
1052                .unwrap();
1053        assert_equal(records, iter::once(expected));
1054    }
1055
1056    #[rstest]
1057    fn write_solver_values() {
1058        let milestone_year = 2020;
1059        let run_description = "test_run".to_string();
1060        let objective_value = Money(1234.56);
1061        let dir = tempdir().unwrap();
1062
1063        // Write solver values
1064        {
1065            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1066            writer
1067                .write_solver_values(milestone_year, &run_description, objective_value)
1068                .unwrap();
1069            writer.flush().unwrap();
1070        }
1071
1072        // Read back and compare
1073        let expected = SolverValuesRow {
1074            milestone_year,
1075            run_description,
1076            objective_value,
1077        };
1078        let records: Vec<SolverValuesRow> =
1079            csv::Reader::from_path(dir.path().join(SOLVER_VALUES_FILE_NAME))
1080                .unwrap()
1081                .into_deserialize()
1082                .try_collect()
1083                .unwrap();
1084        assert_equal(records, iter::once(expected));
1085    }
1086
1087    #[rstest]
1088    fn write_appraisal_results(asset: Asset, appraisal_output: AppraisalOutput) {
1089        let milestone_year = 2020;
1090        let run_description = "test_run".to_string();
1091        let dir = tempdir().unwrap();
1092
1093        // Write appraisal results
1094        {
1095            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1096            writer
1097                .write_appraisal_results(milestone_year, &run_description, &[appraisal_output])
1098                .unwrap();
1099            writer.flush().unwrap();
1100        }
1101
1102        // Read back and compare
1103        let expected = AppraisalResultsRow {
1104            milestone_year,
1105            run_description,
1106            asset_id: None,
1107            process_id: asset.process_id().clone(),
1108            region_id: asset.region_id().clone(),
1109            capacity: Capacity(2.0),
1110            metric: Some(4.14),
1111        };
1112        let records: Vec<AppraisalResultsRow> =
1113            csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
1114                .unwrap()
1115                .into_deserialize()
1116                .try_collect()
1117                .unwrap();
1118        assert_equal(records, iter::once(expected));
1119    }
1120
1121    #[rstest]
1122    fn write_appraisal_time_slice_results(
1123        asset: Asset,
1124        appraisal_output: AppraisalOutput,
1125        time_slice: TimeSliceID,
1126    ) {
1127        let milestone_year = 2020;
1128        let run_description = "test_run".to_string();
1129        let dir = tempdir().unwrap();
1130        let demand = indexmap! {time_slice.clone() => Flow(100.0) };
1131
1132        // Write appraisal time slice results
1133        {
1134            let mut writer = DebugDataWriter::create(dir.path()).unwrap();
1135            writer
1136                .write_appraisal_time_slice_results(
1137                    milestone_year,
1138                    &run_description,
1139                    &[appraisal_output],
1140                    &demand,
1141                )
1142                .unwrap();
1143            writer.flush().unwrap();
1144        }
1145
1146        // Read back and compare
1147        let expected = AppraisalResultsTimeSliceRow {
1148            milestone_year,
1149            run_description,
1150            asset_id: None,
1151            process_id: asset.process_id().clone(),
1152            region_id: asset.region_id().clone(),
1153            time_slice: time_slice.clone(),
1154            activity: Activity(10.0),
1155            activity_coefficient: MoneyPerActivity(0.5),
1156            demand: Flow(100.0),
1157            unmet_demand: Flow(5.0),
1158        };
1159        let records: Vec<AppraisalResultsTimeSliceRow> =
1160            csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME))
1161                .unwrap()
1162                .into_deserialize()
1163                .try_collect()
1164                .unwrap();
1165        assert_equal(records, iter::once(expected));
1166    }
1167
1168    #[test]
1169    fn create_output_directory_new_directory() {
1170        let temp_dir = tempdir().unwrap();
1171        let output_dir = temp_dir.path().join("new_output");
1172
1173        // Create a new directory should succeed and return false (no overwrite)
1174        let result = create_output_directory(&output_dir, false).unwrap();
1175        assert!(!result);
1176        assert!(output_dir.exists());
1177        assert!(output_dir.is_dir());
1178    }
1179
1180    #[test]
1181    fn create_output_directory_existing_empty_directory() {
1182        let temp_dir = tempdir().unwrap();
1183        let output_dir = temp_dir.path().join("empty_output");
1184
1185        // Create the directory first
1186        fs::create_dir(&output_dir).unwrap();
1187
1188        // Creating again should succeed and return false (no overwrite needed)
1189        let result = create_output_directory(&output_dir, false).unwrap();
1190        assert!(!result);
1191        assert!(output_dir.exists());
1192        assert!(output_dir.is_dir());
1193    }
1194
1195    #[test]
1196    fn create_output_directory_existing_with_files_no_overwrite() {
1197        let temp_dir = tempdir().unwrap();
1198        let output_dir = temp_dir.path().join("output_with_files");
1199
1200        // Create directory with a file
1201        fs::create_dir(&output_dir).unwrap();
1202        fs::write(output_dir.join("existing_file.txt"), "some content").unwrap();
1203
1204        // Should fail when allow_overwrite is false
1205        let result = create_output_directory(&output_dir, false);
1206        assert!(result.is_err());
1207        assert!(
1208            result
1209                .unwrap_err()
1210                .to_string()
1211                .contains("Output folder already exists")
1212        );
1213    }
1214
1215    #[test]
1216    fn create_output_directory_existing_with_files_allow_overwrite() {
1217        let temp_dir = tempdir().unwrap();
1218        let output_dir = temp_dir.path().join("output_with_files");
1219
1220        // Create directory with a file
1221        fs::create_dir(&output_dir).unwrap();
1222        let file_path = output_dir.join("existing_file.txt");
1223        fs::write(&file_path, "some content").unwrap();
1224
1225        // Should succeed when allow_overwrite is true and return true (overwrite occurred)
1226        let result = create_output_directory(&output_dir, true).unwrap();
1227        assert!(result);
1228        assert!(output_dir.exists());
1229        assert!(output_dir.is_dir());
1230        assert!(!file_path.exists()); // File should be gone
1231    }
1232
1233    #[test]
1234    fn create_output_directory_nested_path() {
1235        let temp_dir = tempdir().unwrap();
1236        let output_dir = temp_dir.path().join("nested").join("path").join("output");
1237
1238        // Should create nested directories and return false (no overwrite)
1239        let result = create_output_directory(&output_dir, false).unwrap();
1240        assert!(!result);
1241        assert!(output_dir.exists());
1242        assert!(output_dir.is_dir());
1243    }
1244
1245    #[test]
1246    fn create_output_directory_existing_subdirs_with_files_allow_overwrite() {
1247        let temp_dir = tempdir().unwrap();
1248        let output_dir = temp_dir.path().join("output_with_subdirs");
1249
1250        // Create directory structure with files
1251        fs::create_dir_all(output_dir.join("subdir")).unwrap();
1252        fs::write(output_dir.join("file1.txt"), "content1").unwrap();
1253        fs::write(output_dir.join("subdir").join("file2.txt"), "content2").unwrap();
1254
1255        // Should succeed when allow_overwrite is true and return true (overwrite occurred)
1256        let result = create_output_directory(&output_dir, true).unwrap();
1257        assert!(result);
1258        assert!(output_dir.exists());
1259        assert!(output_dir.is_dir());
1260        // All previous content should be gone
1261        assert!(!output_dir.join("file1.txt").exists());
1262        assert!(!output_dir.join("subdir").exists());
1263    }
1264}