Skip to main content

muse2/
simulation.rs

1//! Functionality for running the MUSE2 simulation across milestone years.
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/// * `output_path` - The folder to which output files will be written
26/// * `debug_model` - Whether to write additional information (e.g. duals) to output files
27pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> {
28    let mut writer = DataWriter::create(output_path, &model.model_path, debug_model)?;
29    let mut user_assets = model.user_assets.clone();
30    let mut asset_pool = AssetPool::new(); // active assets
31
32    // Iterate over milestone years
33    let mut year_iter = model.iter_years().peekable();
34    let year = year_iter.next().unwrap(); // Unwrap is safe: model must contain at least one milestone year
35
36    info!("Milestone year: {year}");
37
38    // Commission assets for base year
39    let new_assets = asset_pool.commission_new(year, &mut user_assets);
40
41    // Write assets to file
42    writer.write_assets(new_assets.iter())?;
43    writer.write_asset_capacities(year, asset_pool.iter())?;
44
45    // Gather candidates for the next year, if any
46    let next_year = year_iter.peek().copied();
47    let mut candidates = candidate_assets_for_next_year(
48        &model.processes,
49        next_year,
50        model.parameters.candidate_asset_capacity,
51    );
52
53    // Run dispatch optimisation
54    info!("Running dispatch optimisation...");
55    let (flow_map, mut prices) =
56        run_dispatch_for_year(model, asset_pool.as_slice(), &candidates, year, &mut writer)?;
57
58    // Write results of dispatch optimisation to file
59    writer.write_flows(year, &flow_map)?;
60    writer.write_prices(year, &prices)?;
61
62    while let Some(year) = year_iter.next() {
63        info!("Milestone year: {year}");
64
65        // Decommission assets whose lifetime has passed
66        asset_pool.decommission_old(year);
67
68        // Commission user-defined assets for this year
69        let new_user_assets = asset_pool.commission_new(year, &mut user_assets).to_vec();
70
71        // Take all the active assets as a list of existing assets
72        let existing_assets = asset_pool.take();
73
74        // Iterative loop to "iron out" prices via repeated investment and dispatch
75        let mut ironing_out_iter = 0;
76        let selected_assets: Vec<AssetRef> = loop {
77            // Add context to the writer
78            writer.set_debug_context(format!("ironing out iteration {ironing_out_iter}"));
79
80            // Perform agent investment
81            info!("Running agent investment...");
82            let selected_assets =
83                perform_agent_investment(model, year, &existing_assets, &prices, &mut writer)
84                    .context("Agent investment failed")?;
85
86            // Run dispatch optimisation to get updated prices for the next iteration
87            info!("Running dispatch optimisation...");
88            let (_flow_map, new_prices) =
89                run_dispatch_for_year(model, &selected_assets, &candidates, year, &mut writer)?;
90
91            // Check if prices have converged using time slice-weighted averages
92            let prices_stable = prices.within_tolerance_weighted(
93                &new_prices,
94                model.parameters.price_tolerance,
95                &model.time_slice_info,
96            );
97
98            // Update prices for the next iteration
99            prices = new_prices;
100
101            // Clear writer context
102            writer.clear_debug_context();
103
104            // Break early if prices have converged
105            if prices_stable {
106                info!("Prices converged after {} iterations", ironing_out_iter + 1);
107                break selected_assets;
108            }
109
110            // Break if max iterations reached
111            ironing_out_iter += 1;
112            if ironing_out_iter == model.parameters.max_ironing_out_iterations {
113                info!(
114                    "Max ironing out iterations ({}) reached",
115                    model.parameters.max_ironing_out_iterations
116                );
117                break selected_assets;
118            }
119        };
120
121        // Add selected_assets to the active pool, receiving the newly commissioned ones
122        let newly_commissioned = asset_pool.extend(selected_assets).to_vec();
123
124        // Decommission unused assets
125        asset_pool.mothball_unretained(existing_assets, year);
126        asset_pool.decommission_mothballed(year, model.parameters.mothball_years);
127
128        // Write newly commissioned assets
129        writer.write_assets(new_user_assets.iter().chain(newly_commissioned.iter()))?;
130        writer.write_asset_capacities(year, asset_pool.iter())?;
131
132        // Gather candidates for the next year, if any
133        let next_year = year_iter.peek().copied();
134        candidates = candidate_assets_for_next_year(
135            &model.processes,
136            next_year,
137            model.parameters.candidate_asset_capacity,
138        );
139
140        // Run dispatch optimisation
141        info!("Running final dispatch optimisation for year {year}...");
142        let (flow_map, new_prices) =
143            run_dispatch_for_year(model, asset_pool.as_slice(), &candidates, year, &mut writer)?;
144
145        // Write results of dispatch optimisation to file
146        writer.write_flows(year, &flow_map)?;
147        writer.write_prices(year, &new_prices)?;
148
149        // Prices for the next year
150        prices = new_prices;
151    }
152
153    writer.flush()?;
154
155    Ok(())
156}
157
158// Run dispatch to get flows and prices for a milestone year
159fn run_dispatch_for_year(
160    model: &Model,
161    assets: &[AssetRef],
162    candidates: &[AssetRef],
163    year: u32,
164    writer: &mut DataWriter,
165) -> Result<(FlowMap, CommodityPrices)> {
166    // Run dispatch optimisation with existing assets only, if there are any. If not, then assume no
167    // flows (i.e. all are zero)
168    let (solution_existing, flow_map) = (!assets.is_empty())
169        .then(|| -> Result<_> {
170            let solution =
171                DispatchRun::new(model, assets, year).run("final without candidates", writer)?;
172            let flow_map = solution.create_flow_map();
173
174            Ok((Some(solution), flow_map))
175        })
176        .transpose()?
177        .unwrap_or_default();
178
179    // Perform a separate dispatch run with both existing assets and candidates, if there are any,
180    // to get prices. If not, use the previous solution.
181    let solution_for_prices = (!candidates.is_empty())
182        .then(|| {
183            DispatchRun::new(model, assets, year)
184                .with_candidates(candidates)
185                .run("final with candidates", writer)
186        })
187        .transpose()?
188        .or(solution_existing);
189
190    // If there were either existing or candidate assets, we can calculate prices.
191    // If not, return empty maps.
192    let prices = solution_for_prices
193        .map(|solution| calculate_prices(model, &solution, year))
194        .transpose()?
195        .unwrap_or_default();
196
197    Ok((flow_map, prices))
198}
199
200/// Create candidate assets for all potential processes in a specified year
201fn candidate_assets_for_next_year(
202    processes: &ProcessMap,
203    next_year: Option<u32>,
204    candidate_asset_capacity: Capacity,
205) -> Vec<AssetRef> {
206    let mut candidates = Vec::new();
207    let Some(next_year) = next_year else {
208        return candidates;
209    };
210
211    for process in processes
212        .values()
213        .filter(move |process| process.active_for_year(next_year))
214    {
215        for region_id in &process.regions {
216            candidates.push(
217                Asset::new_candidate_for_dispatch(
218                    Rc::clone(process),
219                    region_id.clone(),
220                    candidate_asset_capacity,
221                    next_year,
222                )
223                .unwrap()
224                .into(),
225            );
226        }
227    }
228
229    candidates
230}