1use super::optimisation::DispatchRun;
3use crate::agent::Agent;
4use crate::asset::{Asset, AssetCapacity, AssetIterator, AssetRef, AssetState};
5use crate::commodity::{Commodity, CommodityID};
6use crate::model::Model;
7use crate::output::DataWriter;
8use crate::process::{Process, ProcessID};
9use crate::region::RegionID;
10use crate::simulation::investment::{
11 AllDemandMap, DemandMap, calculate_candidate_asset_capacity_scale, select_best_assets,
12 update_net_demand_map,
13};
14use crate::simulation::prices::Prices;
15use crate::time_slice::TimeSliceInfo;
16use crate::units::{Capacity, Dimensionless, Flow};
17use anyhow::{Context, Result};
18use indexmap::IndexMap;
19use itertools::{Itertools, chain};
20use log::debug;
21use std::collections::HashMap;
22use std::fmt::Display;
23
24#[derive(PartialEq, Debug, Clone, Eq, Hash)]
26pub enum MarketSet {
27 Single((CommodityID, RegionID)),
29 Cycle(Vec<(CommodityID, RegionID)>),
33 Layer(Vec<MarketSet>),
35}
36
37impl MarketSet {
38 pub fn iter_markets<'a>(
40 &'a self,
41 ) -> Box<dyn Iterator<Item = &'a (CommodityID, RegionID)> + 'a> {
42 match self {
43 MarketSet::Single(market) => Box::new(std::iter::once(market)),
44 MarketSet::Cycle(markets) => Box::new(markets.iter()),
45 MarketSet::Layer(set) => Box::new(set.iter().flat_map(|s| s.iter_markets())),
46 }
47 }
48
49 #[allow(clippy::too_many_arguments)]
63 pub fn select_assets(
64 &self,
65 model: &Model,
66 year: u32,
67 demand: &AllDemandMap,
68 existing_assets: &[AssetRef],
69 prices: &Prices,
70 seen_markets: &[(CommodityID, RegionID)],
71 previously_selected_assets: &[AssetRef],
72 writer: &mut DataWriter,
73 ) -> Result<Vec<AssetRef>> {
74 match self {
75 MarketSet::Single((commodity_id, region_id)) => select_assets_for_single_market(
76 model,
77 commodity_id,
78 region_id,
79 year,
80 demand,
81 existing_assets,
82 prices,
83 writer,
84 ),
85 MarketSet::Cycle(markets) => {
86 debug!("Starting investment for cycle '{self}'");
87 select_assets_for_cycle(
88 model,
89 markets,
90 year,
91 demand,
92 existing_assets,
93 prices,
94 seen_markets,
95 previously_selected_assets,
96 writer,
97 )
98 .with_context(|| {
99 format!(
100 "Investments failed for market set {self} with cyclical dependencies. \
101 Please note that the investment algorithm is currently experimental for \
102 models with circular commodity dependencies and may not be able to find \
103 a solution in all cases."
104 )
105 })
106 }
107 MarketSet::Layer(investment_sets) => {
108 debug!("Starting asset selection for layer '{self}'");
109 let mut all_assets = Vec::new();
110 for investment_set in investment_sets {
111 let assets = investment_set.select_assets(
112 model,
113 year,
114 demand,
115 existing_assets,
116 prices,
117 seen_markets,
118 previously_selected_assets,
119 writer,
120 )?;
121 all_assets.extend(assets);
122 }
123 debug!("Completed asset selection for layer '{self}'");
124 Ok(all_assets)
125 }
126 }
127 }
128}
129
130impl Display for MarketSet {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 match self {
133 MarketSet::Single((commodity_id, region_id)) => {
134 write!(f, "{commodity_id}|{region_id}")
135 }
136 MarketSet::Cycle(markets) => {
137 write!(
138 f,
139 "({})",
140 markets.iter().map(|(c, r)| format!("{c}|{r}")).join(", ")
141 )
142 }
143 MarketSet::Layer(ids) => {
144 write!(f, "[{}]", ids.iter().join(", "))
145 }
146 }
147 }
148}
149
150#[allow(clippy::too_many_arguments)]
154pub fn select_assets_for_single_market(
155 model: &Model,
156 commodity_id: &CommodityID,
157 region_id: &RegionID,
158 year: u32,
159 demand: &AllDemandMap,
160 existing_assets: &[AssetRef],
161 prices: &Prices,
162 writer: &mut DataWriter,
163) -> Result<Vec<AssetRef>> {
164 let commodity = &model.commodities[commodity_id];
165
166 let mut selected_assets = Vec::new();
167 for (agent, commodity_portion) in
168 get_responsible_agents(model.agents.values(), commodity_id, region_id, year)
169 {
170 debug!(
171 "Running asset selection for agent '{}' in market '{}|{}'",
172 agent.id, commodity_id, region_id
173 );
174
175 let demand_portion_for_market = get_demand_portion_for_market(
177 &model.time_slice_info,
178 demand,
179 commodity,
180 region_id,
181 commodity_portion,
182 );
183
184 let opt_assets = get_asset_options(
186 existing_assets,
187 &demand_portion_for_market,
188 agent,
189 commodity,
190 region_id,
191 year,
192 model.parameters.capacity_tranche_fraction,
193 )
194 .collect::<Vec<_>>();
195
196 let agent_addition_limits = collect_agent_limits(
198 agent,
199 region_id,
200 commodity_id,
201 year,
202 commodity_portion,
203 Process::agent_addition_limit,
204 );
205
206 let agent_total_limits = collect_agent_limits(
208 agent,
209 region_id,
210 commodity_id,
211 year,
212 commodity_portion,
213 Process::agent_total_limit,
214 );
215
216 let best_assets = select_best_assets(
218 model,
219 opt_assets,
220 agent_addition_limits,
221 agent_total_limits,
222 commodity,
223 agent,
224 region_id,
225 prices,
226 demand_portion_for_market,
227 year,
228 writer,
229 )?;
230 selected_assets.extend(best_assets);
231 }
232
233 Ok(selected_assets)
234}
235
236#[allow(clippy::too_many_arguments)]
251pub fn select_assets_for_cycle(
252 model: &Model,
253 markets: &[(CommodityID, RegionID)],
254 year: u32,
255 demand: &AllDemandMap,
256 existing_assets: &[AssetRef],
257 prices: &Prices,
258 seen_markets: &[(CommodityID, RegionID)],
259 previously_selected_assets: &[AssetRef],
260 writer: &mut DataWriter,
261) -> Result<Vec<AssetRef>> {
262 let markets_str = markets.iter().map(|(c, r)| format!("{c}|{r}")).join(", ");
264
265 let mut current_demand = demand.clone();
267 let mut assets_for_cycle = IndexMap::new();
268 let mut last_solution = None;
269 for (idx, (commodity_id, region_id)) in markets.iter().enumerate() {
270 let assets = select_assets_for_single_market(
272 model,
273 commodity_id,
274 region_id,
275 year,
276 ¤t_demand,
277 existing_assets,
278 prices,
279 writer,
280 )?;
281 assets_for_cycle.insert((commodity_id.clone(), region_id.clone()), assets);
282
283 let mut all_assets = previously_selected_assets.to_vec();
285 let assets_for_cycle_flat: Vec<_> = assets_for_cycle
286 .values()
287 .flat_map(|v| v.iter().cloned())
288 .collect();
289 all_assets.extend_from_slice(&assets_for_cycle_flat);
290
291 let mut markets_to_balance = seen_markets.to_vec();
293 markets_to_balance.extend_from_slice(&markets[0..=idx]);
294
295 let flexible_capacity_assets: Vec<_> = assets_for_cycle_flat
297 .iter()
298 .filter(|asset| matches!(asset.state(), AssetState::Ready { .. }))
299 .cloned()
300 .collect();
301
302 let mut agent_share_cache = HashMap::new();
304 let capacity_limits = flexible_capacity_assets
305 .iter()
306 .filter_map(|asset| {
307 let agent_id = asset.agent_id().unwrap();
308 let commodity_id = asset.primary_output_commodity().unwrap();
309 let agent_share = *agent_share_cache
310 .entry((agent_id, commodity_id))
311 .or_insert_with(|| {
312 model.agents[agent_id].commodity_portions[&(commodity_id.clone(), year)]
313 });
314 asset
315 .process()
316 .agent_addition_limit(asset.region_id(), asset.commission_year(), agent_share)
317 .map(|max_capacity| (asset.clone(), max_capacity))
318 })
319 .collect::<HashMap<_, _>>();
320
321 let solution = DispatchRun::new(model, &all_assets, year)
323 .without_commodity_constraints()
324 .with_market_balance_subset(&markets_to_balance)
325 .with_flexible_capacity_assets(
326 &flexible_capacity_assets,
327 Some(&capacity_limits),
328 model.parameters.capacity_margin,
330 )
331 .run(
332 &format!("cycle ({markets_str}) post {commodity_id}|{region_id} investment"),
333 writer,
334 )
335 .with_context(|| {
336 format!(
337 "Cycle balancing failed for cycle ({markets_str}), capacity_margin: {}. \
338 Try increasing the capacity_margin.",
339 model.parameters.capacity_margin
340 )
341 })?;
342
343 current_demand.clone_from(demand);
345 update_net_demand_map(
346 &mut current_demand,
347 &solution.create_flow_map(),
348 &assets_for_cycle_flat,
349 );
350 last_solution = Some(solution);
351 }
352
353 let mut all_cycle_assets: Vec<_> = assets_for_cycle.into_values().flatten().collect();
355 if let Some(solution) = last_solution {
356 let new_capacities: HashMap<_, _> = solution.iter_capacity().collect();
357 for asset in &mut all_cycle_assets {
358 if let Some(new_capacity) = new_capacities.get(asset) {
359 debug!(
360 "Capacity of asset '{}' modified during cycle balancing ({} to {})",
361 asset.process_id(),
362 asset.total_capacity(),
363 new_capacity.total_capacity()
364 );
365 asset.make_mut().set_capacity(*new_capacity);
366 }
367 }
368 }
369
370 all_cycle_assets.retain(|asset| asset.num_tranches() > 0);
372
373 Ok(all_cycle_assets)
374}
375
376pub fn get_demand_portion_for_market(
378 time_slice_info: &TimeSliceInfo,
379 demand: &AllDemandMap,
380 commodity: &Commodity,
381 region_id: &RegionID,
382 commodity_portion: Dimensionless,
383) -> DemandMap {
384 time_slice_info
385 .iter_selections_at_level(commodity.time_slice_level)
386 .map(|ts_selection| {
387 let demand_for_selection = *demand
388 .get(&(
389 commodity.id.clone(),
390 region_id.clone(),
391 ts_selection.clone(),
392 ))
393 .unwrap_or(&Flow(0.0));
394 (ts_selection, commodity_portion * demand_for_selection)
395 })
396 .collect()
397}
398
399pub fn get_responsible_agents<'a, I>(
402 agents: I,
403 commodity_id: &'a CommodityID,
404 region_id: &'a RegionID,
405 year: u32,
406) -> impl Iterator<Item = (&'a Agent, Dimensionless)>
407where
408 I: Iterator<Item = &'a Agent>,
409{
410 agents.filter_map(move |agent| {
411 if !agent.regions.contains(region_id) {
412 return None;
413 }
414 let portion = agent
415 .commodity_portions
416 .get(&(commodity_id.clone(), year))?;
417
418 Some((agent, *portion))
419 })
420}
421
422pub fn get_asset_options<'a>(
424 all_existing_assets: &'a [AssetRef],
425 demand: &'a DemandMap,
426 agent: &'a Agent,
427 commodity: &'a Commodity,
428 region_id: &'a RegionID,
429 year: u32,
430 capacity_tranche_fraction: Dimensionless,
431) -> impl Iterator<Item = AssetRef> + 'a {
432 let existing_assets = all_existing_assets
434 .iter()
435 .filter_agent(&agent.id)
436 .filter_region(region_id)
437 .filter_primary_producers_of(&commodity.id)
438 .cloned();
439
440 let candidate_assets = get_candidate_assets(
442 demand,
443 agent,
444 region_id,
445 commodity,
446 year,
447 capacity_tranche_fraction,
448 );
449
450 chain(existing_assets, candidate_assets)
451}
452
453fn get_candidate_assets<'a>(
462 demand: &'a DemandMap,
463 agent: &'a Agent,
464 region_id: &'a RegionID,
465 commodity: &'a Commodity,
466 year: u32,
467 capacity_tranche_fraction: Dimensionless,
468) -> impl Iterator<Item = AssetRef> + 'a {
469 agent
470 .iter_search_space(region_id, &commodity.id, year)
471 .map(move |process| {
472 let mut asset =
474 Asset::new_candidate(process.clone(), region_id.clone(), Capacity(0.0), year)
475 .unwrap();
476
477 let tranche_size = if let Some(tranche_size) = asset.process().tranche_size {
479 tranche_size
481 } else {
482 let capacity_scale =
484 calculate_candidate_asset_capacity_scale(&asset, commodity, demand);
485 capacity_scale * capacity_tranche_fraction
486 };
487 let asset_capacity = AssetCapacity::single(tranche_size);
488 asset.set_capacity(asset_capacity);
489 asset.into()
490 })
491}
492
493pub fn collect_agent_limits(
509 agent: &Agent,
510 region_id: &RegionID,
511 commodity_id: &CommodityID,
512 year: u32,
513 commodity_portion: Dimensionless,
514 get_agent_limit: fn(&Process, &RegionID, u32, Dimensionless) -> Option<Capacity>,
515) -> HashMap<ProcessID, Capacity> {
516 agent
517 .iter_search_space(region_id, commodity_id, year)
518 .filter_map(|process| {
519 get_agent_limit(process, region_id, year, commodity_portion)
520 .map(|limit| (process.id.clone(), limit))
521 })
522 .collect()
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use crate::agent::{Agent, AgentCommodityPortionsMap, AgentObjectiveMap, DecisionRule};
529 use crate::fixture::{process, region_id};
530 use crate::process::{Process, ProcessInvestmentConstraint};
531 use crate::region::RegionID;
532 use crate::units::{Capacity, Dimensionless};
533 use rstest::rstest;
534 use std::collections::HashMap;
535 use std::sync::Arc;
536
537 fn agent_with_process(
538 process: Process,
539 region_id: &RegionID,
540 commodity_id: &CommodityID,
541 ) -> Agent {
542 let mut search_space = HashMap::new();
543 search_space.insert(
544 (commodity_id.clone(), region_id.clone(), 2015),
545 Arc::new(vec![Arc::new(process)]),
546 );
547 Agent {
548 id: "agent1".into(),
549 description: String::new(),
550 commodity_portions: AgentCommodityPortionsMap::new(),
551 search_space,
552 decision_rule: DecisionRule::Single,
553 regions: [region_id.clone()].into(),
554 objectives: AgentObjectiveMap::new(),
555 }
556 }
557
558 #[rstest]
559 fn collect_agent_limits_uses_search_space(mut process: Process, region_id: RegionID) {
560 process.investment_constraints.insert(
561 (region_id.clone(), 2015),
562 Arc::new(ProcessInvestmentConstraint {
563 addition_limit: Some(Capacity(10.0)),
564 total_capacity_limit: Some(Capacity(100.0)),
565 }),
566 );
567 let commodity_id = "commodity".into();
568 let process_id = process.id.clone();
569 let agent = agent_with_process(process, ®ion_id, &commodity_id);
570
571 let result = collect_agent_limits(
572 &agent,
573 ®ion_id,
574 &commodity_id,
575 2015,
576 Dimensionless(0.5),
577 Process::agent_addition_limit,
578 );
579
580 assert_eq!(result.get(&process_id), Some(&Capacity(5.0)));
581
582 let result = collect_agent_limits(
583 &agent,
584 ®ion_id,
585 &commodity_id,
586 2015,
587 Dimensionless(0.5),
588 Process::agent_total_limit,
589 );
590
591 assert_eq!(result.get(&process_id), Some(&Capacity(50.0)));
592 }
593
594 #[rstest]
595 fn collect_agent_limits_excludes_processes_without_limits(
596 process: Process,
597 region_id: RegionID,
598 ) {
599 let commodity_id = "commodity".into();
600 let process_id = process.id.clone();
601 let agent = agent_with_process(process, ®ion_id, &commodity_id);
602
603 let result = collect_agent_limits(
604 &agent,
605 ®ion_id,
606 &commodity_id,
607 2015,
608 Dimensionless(1.0),
609 Process::agent_addition_limit,
610 );
611
612 assert!(!result.contains_key(&process_id));
613 }
614}