1use 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
20pub 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 let mut year_iter = model.iter_years().peekable();
38 let year = year_iter.next().unwrap(); info!("Milestone year: {year}");
41
42 assets.decommission_old(year);
44
45 assets.commission_new(year);
47
48 writer.write_assets(assets.iter_all())?;
50
51 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 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 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 assets.decommission_old(year);
80
81 assets.commission_new(year);
83
84 let existing_assets = assets.take();
86
87 let mut ironing_out_iter = 0;
89 let selected_assets: Vec<AssetRef> = loop {
90 writer.set_debug_context(format!("ironing out iteration {ironing_out_iter}"));
92
93 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 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 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 let prices_stable =
132 prices.within_tolerance(&new_prices, model.parameters.price_tolerance);
133
134 prices = new_prices;
136 reduced_costs = new_reduced_costs;
137
138 writer.clear_debug_context();
140
141 if prices_stable {
143 info!("Prices converged after {} iterations", ironing_out_iter + 1);
144 break selected_assets;
145 }
146
147 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 assets.extend(selected_assets);
160
161 assets.decommission_if_not_active(existing_assets, year);
163
164 writer.write_assets(assets.iter_all())?;
166
167 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 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 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_costs = new_reduced_costs;
191 prices = new_prices;
192 }
193
194 writer.flush()?;
195
196 Ok(())
197}
198
199fn 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 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 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 let (prices, reduced_costs) =
223 calculate_prices_and_reduced_costs(model, &solution, assets, year);
224
225 Ok((flow_map, prices, reduced_costs))
226}
227
228fn 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}