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::{Prices, calculate_prices};
7use crate::timeit::DispatchTimer;
8use crate::units::Capacity;
9use anyhow::{Context, Result};
10use context_manager;
11use log::info;
12use std::path::Path;
13use std::rc::Rc;
14
15pub mod optimisation;
16use optimisation::{DispatchRun, FlowMap};
17pub mod investment;
18use investment::perform_agent_investment;
19pub mod market;
20pub mod prices;
21pub use prices::PriceMap;
22
23/// Run the simulation.
24///
25/// # Arguments:
26///
27/// * `model` - The model to run
28/// * `output_path` - The folder to which output files will be written
29/// * `debug_model` - Whether to write additional information (e.g. duals) to output files
30pub fn run(model: &Model, output_path: &Path, debug_model: bool) -> Result<()> {
31    let mut writer = DataWriter::create(output_path, &model.model_path, debug_model)?;
32    let mut user_assets = model.user_assets.clone();
33    let mut asset_pool = AssetPool::new(); // active assets
34
35    // Iterate over milestone years
36    let mut year_iter = model.iter_years().peekable();
37    let year = year_iter.next().unwrap(); // Unwrap is safe: model must contain at least one milestone year
38
39    info!("Milestone year: {year}");
40
41    // Commission assets for base year
42    let new_assets = asset_pool.commission_new(year, &mut user_assets);
43
44    // Write assets to file
45    writer.write_assets(new_assets)?;
46    writer.write_asset_capacities(year, &asset_pool)?;
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 (mut prices, flow_map) =
59        run_dispatch_for_year(model, &asset_pool, &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.market)?;
64
65    while let Some(year) = year_iter.next() {
66        info!("Milestone year: {year}");
67
68        // Decommission assets whose lifetime has passed
69        asset_pool.decommission_old(year);
70
71        // Commission user-defined assets for this year
72        let mut new_assets = asset_pool.commission_new(year, &mut user_assets).to_vec();
73
74        // Take all the active assets as a list of existing assets
75        let existing_assets = asset_pool.take();
76
77        // Iterative loop to "iron out" prices via repeated investment and dispatch
78        let mut ironing_out_iter = 0;
79        let selected_assets: Vec<AssetRef> = loop {
80            // Add context to the writer
81            writer.set_debug_context(format!("ironing out iteration {ironing_out_iter}"));
82
83            // Perform agent investment
84            info!("Running agent investment...");
85            let selected_assets =
86                perform_agent_investment(model, year, &existing_assets, &prices, &mut writer)
87                    .context("Agent investment failed")?;
88
89            // Run dispatch optimisation to get updated prices for the next iteration
90            info!("Running dispatch optimisation...");
91            let (new_prices, ..) =
92                run_dispatch_for_year(model, &selected_assets, &candidates, year, &mut writer)?;
93
94            // Check if prices have converged using time slice-weighted averages
95            let prices_stable = prices.market.within_tolerance_weighted(
96                &new_prices.market,
97                model.parameters.price_tolerance,
98                &model.time_slice_info,
99            );
100
101            // Update prices for the next iteration
102            prices = new_prices;
103
104            // Clear writer context
105            writer.clear_debug_context();
106
107            // Break early if prices have converged
108            if prices_stable {
109                info!("Prices converged after {} iterations", ironing_out_iter + 1);
110                break selected_assets;
111            }
112
113            // Break if max iterations reached
114            ironing_out_iter += 1;
115            if ironing_out_iter == model.parameters.max_ironing_out_iterations {
116                info!(
117                    "Max ironing out iterations ({}) reached",
118                    model.parameters.max_ironing_out_iterations
119                );
120                break selected_assets;
121            }
122        };
123
124        // Add selected_assets to the active pool, receiving the newly commissioned ones
125        let newly_selected = asset_pool.extend(selected_assets);
126        new_assets.extend_from_slice(newly_selected);
127
128        // Decommission unused assets
129        asset_pool.mothball_unretained(existing_assets, year);
130        asset_pool.decommission_mothballed(year, model.parameters.mothball_years);
131
132        // Write newly commissioned assets
133        writer.write_assets(&new_assets)?;
134        writer.write_asset_capacities(year, &asset_pool)?;
135
136        // Gather candidates for the next year, if any
137        let next_year = year_iter.peek().copied();
138        candidates = candidate_assets_for_next_year(
139            &model.processes,
140            next_year,
141            model.parameters.candidate_asset_capacity,
142        );
143
144        // Run dispatch optimisation
145        info!("Running final dispatch optimisation for year {year}...");
146        let (new_prices, flow_map) =
147            run_dispatch_for_year(model, &asset_pool, &candidates, year, &mut writer)?;
148
149        // Write results of dispatch optimisation to file
150        writer.write_flows(year, &flow_map)?;
151        writer.write_prices(year, &new_prices.market)?;
152
153        // Prices for the next year
154        prices = new_prices;
155    }
156
157    writer.flush()?;
158
159    Ok(())
160}
161
162/// Run dispatch to get flows and prices for a milestone year.
163///
164/// Tries to get commodity flows from a dispatch run which just includes existing assets and prices
165/// from a run with both existing and candidate assets. If either flows or prices cannot be
166/// calculated, empty maps are returned.
167///
168/// Fully mothballed assets or mothballed units thereof are excluded.
169#[context_manager::wrap(DispatchTimer)]
170fn run_dispatch_for_year(
171    model: &Model,
172    assets: &[AssetRef],
173    candidates: &[AssetRef],
174    year: u32,
175    writer: &mut DataWriter,
176) -> Result<(Prices, FlowMap)> {
177    // Sanity checks
178    debug_assert!(assets.iter().all(|asset| !asset.is_candidate()));
179    debug_assert!(candidates.iter().all(|asset| asset.is_candidate()));
180
181    // Only include non-mothballed units
182    let assets_vec: Vec<AssetRef>;
183    let assets = if assets.iter().any(|asset| asset.has_any_mothballed_units()) {
184        assets_vec = assets
185            .iter()
186            .cloned()
187            .filter_map(|asset| {
188                // Exclude fully mothballed assets entirely. If assets are partially mothballed,
189                // get a new asset without the mothballed units.
190                let num_units = asset.get_num_nonmothballed_units();
191                (num_units > 0).then(|| asset.with_subset_of_units(num_units))
192            })
193            .collect();
194        &assets_vec
195    } else {
196        assets
197    };
198
199    // Run dispatch optimisation with existing assets only, if there are any. If not, then assume no
200    // flows (i.e. all are zero)
201    let (solution_existing, flow_map) = if assets.is_empty() {
202        (None, FlowMap::default())
203    } else {
204        let solution =
205            DispatchRun::new(model, assets, year).run("final without candidates", writer)?;
206        let flow_map = solution.create_flow_map();
207        (Some(solution), flow_map)
208    };
209
210    // Perform a separate dispatch run with both existing assets and candidates, if there are any,
211    // to get shadow prices. If not, use the existing solution alone.
212    let solution_with_candidates = if candidates.is_empty() {
213        None
214    } else {
215        Some(
216            DispatchRun::new(model, assets, year)
217                .with_candidates(candidates)
218                .run("final with candidates", writer)?,
219        )
220    };
221
222    // Calculate prices from the appropriate solution(s). If there were candidates, use the solution
223    // that includes them; otherwise use the existing solution alone. If there were no assets at
224    // all, return empty prices.
225    let prices = match (
226        solution_existing.as_ref(),
227        solution_with_candidates.as_ref(),
228    ) {
229        (None, None) => Prices::default(),
230        (Some(existing), None) => calculate_prices(model, existing, existing, year)?,
231        (Some(existing), Some(with_candidates)) => {
232            calculate_prices(model, existing, with_candidates, year)?
233        }
234        (None, Some(with_candidates)) => {
235            calculate_prices(model, with_candidates, with_candidates, year)?
236        }
237    };
238
239    Ok((prices, flow_map))
240}
241
242/// Create candidate assets for all potential processes in a specified year
243fn candidate_assets_for_next_year(
244    processes: &ProcessMap,
245    next_year: Option<u32>,
246    candidate_asset_capacity: Capacity,
247) -> Vec<AssetRef> {
248    let mut candidates = Vec::new();
249    let Some(next_year) = next_year else {
250        return candidates;
251    };
252
253    for process in processes
254        .values()
255        .filter(move |process| process.active_for_year(next_year))
256    {
257        for region_id in &process.regions {
258            candidates.push(
259                Asset::new_candidate_for_dispatch(
260                    Rc::clone(process),
261                    region_id.clone(),
262                    candidate_asset_capacity,
263                    next_year,
264                )
265                .unwrap()
266                .into(),
267            );
268        }
269    }
270
271    candidates
272}