muse2/
simulation.rs

1//! Functionality for running the MUSE 2.0 simulation.
2use crate::asset::{Asset, AssetPool, AssetRef};
3use crate::model::Model;
4use crate::output::DataWriter;
5use crate::process::ProcessMap;
6use crate::simulation::prices::{ReducedCosts, calculate_prices_and_reduced_costs};
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    // There shouldn't be assets already commissioned, but let's do this just in case
43    assets.decommission_old(year);
44
45    // Commission assets for base year
46    assets.commission_new(year);
47
48    // Write assets to file
49    writer.write_assets(assets.iter_all())?;
50
51    // Gather candidates for the next year, if any
52    let next_year = year_iter.peek().copied();
53    let mut candidates = next_year
54        .map(|next_year| {
55            candidate_assets_for_year(
56                &model.processes,
57                next_year,
58                model.parameters.candidate_asset_capacity,
59            )
60        })
61        .unwrap_or_default();
62
63    // Run dispatch optimisation
64    info!("Running dispatch optimisation...");
65    let (flow_map, mut prices, mut reduced_costs) =
66        run_dispatch_for_year(&model, assets.as_slice(), &candidates, year, &mut writer)?;
67
68    // Write results of dispatch optimisation to file
69    writer.write_flows(year, &flow_map)?;
70    writer.write_prices(year, &prices)?;
71    writer.write_debug_reduced_costs(year, &reduced_costs)?;
72
73    while let Some(year) = year_iter.next() {
74        info!("Milestone year: {year}");
75
76        // Decommission assets whose lifetime has passed. We do this *before* agent investment, to
77        // prevent agents from selecting assets that are being decommissioned in this milestone
78        // year.
79        assets.decommission_old(year);
80
81        // Commission pre-defined assets for this year
82        assets.commission_new(year);
83
84        // Take all the active assets as a list of existing assets
85        let existing_assets = assets.take();
86
87        // Ironing out loop
88        let mut ironing_out_iter = 0;
89        let selected_assets: Vec<AssetRef> = loop {
90            // Add context to the writer
91            writer.set_debug_context(format!("ironing out iteration {ironing_out_iter}"));
92
93            // Perform agent investment
94            info!("Running agent investment...");
95            let selected_assets = perform_agent_investment(
96                &model,
97                year,
98                &existing_assets,
99                &prices,
100                &reduced_costs,
101                &mut writer,
102            )
103            .context("Agent investment failed")?;
104
105            // We need to add candidates from all existing_assets that aren't in selected_assets as
106            // these may be re-chosen in the next iteration
107            let mut all_candidates = candidates.clone();
108            all_candidates.extend(
109                existing_assets
110                    .iter()
111                    .filter(|asset| !selected_assets.contains(asset))
112                    .map(|asset| {
113                        asset
114                            .as_candidate(Some(model.parameters.candidate_asset_capacity))
115                            .into()
116                    }),
117            );
118
119            // Run dispatch optimisation to get updated reduced costs and prices for the next
120            // iteration
121            info!("Running dispatch optimisation...");
122            let (_flow_map, new_prices, new_reduced_costs) = run_dispatch_for_year(
123                &model,
124                &selected_assets,
125                &all_candidates,
126                year,
127                &mut writer,
128            )?;
129
130            // Check if prices have converged
131            let prices_stable =
132                prices.within_tolerance(&new_prices, model.parameters.price_tolerance);
133
134            // Update prices and reduced costs for the next iteration
135            prices = new_prices;
136            reduced_costs = new_reduced_costs;
137
138            // Clear writer context
139            writer.clear_debug_context();
140
141            // Break early if prices have converged
142            if prices_stable {
143                info!("Prices converged after {} iterations", ironing_out_iter + 1);
144                break selected_assets;
145            }
146
147            // Break if max iterations reached
148            ironing_out_iter += 1;
149            if ironing_out_iter == model.parameters.max_ironing_out_iterations {
150                info!(
151                    "Max ironing out iterations ({}) reached",
152                    model.parameters.max_ironing_out_iterations
153                );
154                break selected_assets;
155            }
156        };
157
158        // Add selected_assets to the active pool
159        assets.extend(selected_assets);
160
161        // Decommission unused assets
162        assets.decommission_if_not_active(existing_assets, year);
163
164        // Write assets
165        writer.write_assets(assets.iter_all())?;
166
167        // Gather candidates for the next year, if any
168        let next_year = year_iter.peek().copied();
169        candidates = next_year
170            .map(|next_year| {
171                candidate_assets_for_year(
172                    &model.processes,
173                    next_year,
174                    model.parameters.candidate_asset_capacity,
175                )
176            })
177            .unwrap_or_default();
178
179        // Run dispatch optimisation
180        info!("Running final dispatch optimisation for year {year}...");
181        let (flow_map, new_prices, new_reduced_costs) =
182            run_dispatch_for_year(&model, assets.as_slice(), &candidates, year, &mut writer)?;
183
184        // Write results of dispatch optimisation to file
185        writer.write_flows(year, &flow_map)?;
186        writer.write_prices(year, &new_prices)?;
187        writer.write_debug_reduced_costs(year, &new_reduced_costs)?;
188
189        // Reduced cost and prices for the next year
190        reduced_costs = new_reduced_costs;
191        prices = new_prices;
192    }
193
194    writer.flush()?;
195
196    Ok(())
197}
198
199// Run dispatch to get flows, prices and reduced costs for a milestone year
200fn run_dispatch_for_year(
201    model: &Model,
202    assets: &[AssetRef],
203    candidates: &[AssetRef],
204    year: u32,
205    writer: &mut DataWriter,
206) -> Result<(FlowMap, CommodityPrices, ReducedCosts)> {
207    // Dispatch optimisation with existing assets only
208    let solution_existing =
209        DispatchRun::new(model, assets, year).run("final without candidates", writer)?;
210    let flow_map = solution_existing.create_flow_map();
211
212    // Perform a separate dispatch run with existing assets and candidates (if there are any)
213    let solution = if candidates.is_empty() {
214        solution_existing
215    } else {
216        DispatchRun::new(model, assets, year)
217            .with_candidates(candidates)
218            .run("final with candidates", writer)?
219    };
220
221    // Calculate commodity prices and asset reduced costs
222    let (prices, reduced_costs) =
223        calculate_prices_and_reduced_costs(model, &solution, assets, year);
224
225    Ok((flow_map, prices, reduced_costs))
226}
227
228/// Create candidate assets for all potential processes in a specified year
229fn candidate_assets_for_year(
230    processes: &ProcessMap,
231    year: u32,
232    candidate_asset_capacity: Capacity,
233) -> Vec<AssetRef> {
234    let mut candidates = Vec::new();
235    for process in processes
236        .values()
237        .filter(move |process| process.active_for_year(year))
238    {
239        for region_id in process.regions.iter() {
240            candidates.push(
241                Asset::new_candidate(
242                    Rc::clone(process),
243                    region_id.clone(),
244                    candidate_asset_capacity,
245                    year,
246                )
247                .unwrap()
248                .into(),
249            );
250        }
251    }
252
253    candidates
254}