muse2/
simulation.rs

1//! Functionality for running the MUSE2 simulation.
2use crate::asset::{Asset, AssetPool, AssetRef};
3use crate::model::Model;
4use crate::output::DataWriter;
5use crate::process::ProcessMap;
6use crate::simulation::prices::calculate_prices;
7use crate::units::Capacity;
8use anyhow::{Context, Result};
9use log::info;
10use std::path::Path;
11use std::rc::Rc;
12
13pub mod optimisation;
14use optimisation::{DispatchRun, FlowMap};
15pub mod investment;
16use investment::perform_agent_investment;
17pub mod prices;
18pub use prices::CommodityPrices;
19
20/// Run the simulation.
21///
22/// # Arguments:
23///
24/// * `model` - The model to run
25/// * `assets` - The asset pool
26/// * `output_path` - The folder to which output files will be written
27/// * `debug_model` - Whether to write additional information (e.g. duals) to output files
28pub fn run(
29    model: &Model,
30    mut assets: AssetPool,
31    output_path: &Path,
32    debug_model: bool,
33) -> Result<()> {
34    let mut writer = DataWriter::create(output_path, &model.model_path, debug_model)?;
35
36    // Iterate over milestone years
37    let mut year_iter = model.iter_years().peekable();
38    let year = year_iter.next().unwrap(); // NB: There will be at least one year
39
40    info!("Milestone year: {year}");
41
42    // Commission assets for base year
43    assets.update_for_year(year);
44
45    // Write assets to file
46    writer.write_assets(assets.iter_all())?;
47
48    // Gather candidates for the next year, if any
49    let next_year = year_iter.peek().copied();
50    let mut candidates = candidate_assets_for_next_year(
51        &model.processes,
52        next_year,
53        model.parameters.candidate_asset_capacity,
54    );
55
56    // Run dispatch optimisation
57    info!("Running dispatch optimisation...");
58    let (flow_map, mut prices) =
59        run_dispatch_for_year(model, assets.as_slice(), &candidates, year, &mut writer)?;
60
61    // Write results of dispatch optimisation to file
62    writer.write_flows(year, &flow_map)?;
63    writer.write_prices(year, &prices)?;
64
65    while let Some(year) = year_iter.next() {
66        info!("Milestone year: {year}");
67
68        // Commission new assets and decommission those whose lifetime has passed. We do this
69        // *before* agent investment, to prevent agents from selecting assets that are being
70        // decommissioned in this milestone year.
71        assets.update_for_year(year);
72
73        // Take all the active assets as a list of existing assets
74        let existing_assets = assets.take();
75
76        // Ironing out loop
77        let mut ironing_out_iter = 0;
78        let selected_assets: Vec<AssetRef> = loop {
79            // Add context to the writer
80            writer.set_debug_context(format!("ironing out iteration {ironing_out_iter}"));
81
82            // Perform agent investment
83            info!("Running agent investment...");
84            let selected_assets =
85                perform_agent_investment(model, year, &existing_assets, &prices, &mut writer)
86                    .context("Agent investment failed")?;
87
88            // We need to add candidates from all existing_assets that aren't in selected_assets as
89            // these may be re-chosen in the next iteration
90            let mut all_candidates = candidates.clone();
91            all_candidates.extend(
92                existing_assets
93                    .iter()
94                    .filter(|asset| !selected_assets.contains(asset))
95                    .map(|asset| {
96                        let mut asset = Asset::new_candidate_from_commissioned(asset);
97                        asset.set_capacity(model.parameters.candidate_asset_capacity);
98                        asset.into()
99                    }),
100            );
101
102            // Run dispatch optimisation to get updated prices for the next iteration
103            info!("Running dispatch optimisation...");
104            let (_flow_map, new_prices) =
105                run_dispatch_for_year(model, &selected_assets, &all_candidates, year, &mut writer)?;
106
107            // Check if prices have converged using time slice-weighted averages
108            let prices_stable = prices.within_tolerance_weighted(
109                &new_prices,
110                model.parameters.price_tolerance,
111                &model.time_slice_info,
112            );
113
114            // Update prices for the next iteration
115            prices = new_prices;
116
117            // Clear writer context
118            writer.clear_debug_context();
119
120            // Break early if prices have converged
121            if prices_stable {
122                info!("Prices converged after {} iterations", ironing_out_iter + 1);
123                break selected_assets;
124            }
125
126            // Break if max iterations reached
127            ironing_out_iter += 1;
128            if ironing_out_iter == model.parameters.max_ironing_out_iterations {
129                info!(
130                    "Max ironing out iterations ({}) reached",
131                    model.parameters.max_ironing_out_iterations
132                );
133                break selected_assets;
134            }
135        };
136
137        // Add selected_assets to the active pool
138        assets.extend(selected_assets);
139
140        // Decommission unused assets
141        assets.decommission_if_not_active(existing_assets, year);
142
143        // Write assets
144        writer.write_assets(assets.iter_all())?;
145
146        // Gather candidates for the next year, if any
147        let next_year = year_iter.peek().copied();
148        candidates = candidate_assets_for_next_year(
149            &model.processes,
150            next_year,
151            model.parameters.candidate_asset_capacity,
152        );
153
154        // Run dispatch optimisation
155        info!("Running final dispatch optimisation for year {year}...");
156        let (flow_map, new_prices) =
157            run_dispatch_for_year(model, assets.as_slice(), &candidates, year, &mut writer)?;
158
159        // Write results of dispatch optimisation to file
160        writer.write_flows(year, &flow_map)?;
161        writer.write_prices(year, &new_prices)?;
162
163        // Prices for the next year
164        prices = new_prices;
165    }
166
167    writer.flush()?;
168
169    Ok(())
170}
171
172// Run dispatch to get flows and prices for a milestone year
173fn run_dispatch_for_year(
174    model: &Model,
175    assets: &[AssetRef],
176    candidates: &[AssetRef],
177    year: u32,
178    writer: &mut DataWriter,
179) -> Result<(FlowMap, CommodityPrices)> {
180    // Run dispatch optimisation with existing assets only, if there are any. If not, then assume no
181    // flows (i.e. all are zero)
182    let (solution_existing, flow_map) = (!assets.is_empty())
183        .then(|| -> Result<_> {
184            let solution =
185                DispatchRun::new(model, assets, year).run("final without candidates", writer)?;
186            let flow_map = solution.create_flow_map();
187
188            Ok((Some(solution), flow_map))
189        })
190        .transpose()?
191        .unwrap_or_default();
192
193    // Perform a separate dispatch run with both existing assets and candidates, if there are any,
194    // to get prices. If not, use the previous solution.
195    let solution_for_prices = (!candidates.is_empty())
196        .then(|| {
197            DispatchRun::new(model, assets, year)
198                .with_candidates(candidates)
199                .run("final with candidates", writer)
200        })
201        .transpose()?
202        .or(solution_existing);
203
204    // If there were either existing or candidate assets, we can calculate prices.
205    // If not, return empty maps.
206    let prices = solution_for_prices
207        .map(|solution| calculate_prices(model, &solution))
208        .unwrap_or_default();
209
210    Ok((flow_map, prices))
211}
212
213/// Create candidate assets for all potential processes in a specified year
214fn candidate_assets_for_next_year(
215    processes: &ProcessMap,
216    next_year: Option<u32>,
217    candidate_asset_capacity: Capacity,
218) -> Vec<AssetRef> {
219    let mut candidates = Vec::new();
220    let Some(next_year) = next_year else {
221        return candidates;
222    };
223
224    for process in processes
225        .values()
226        .filter(move |process| process.active_for_year(next_year))
227    {
228        for region_id in &process.regions {
229            candidates.push(
230                Asset::new_candidate(
231                    Rc::clone(process),
232                    region_id.clone(),
233                    candidate_asset_capacity,
234                    next_year,
235                )
236                .unwrap()
237                .into(),
238            );
239        }
240    }
241
242    candidates
243}