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