1use super::optimisation::{DispatchRun, FlowMap};
3use crate::agent::Agent;
4use crate::asset::{Asset, AssetIterator, AssetRef, AssetState};
5use crate::commodity::{Commodity, CommodityID, CommodityMap};
6use crate::model::Model;
7use crate::output::DataWriter;
8use crate::region::RegionID;
9use crate::simulation::CommodityPrices;
10use crate::time_slice::{TimeSliceID, TimeSliceInfo};
11use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity};
12use anyhow::{Result, ensure};
13use indexmap::IndexMap;
14use itertools::{chain, iproduct};
15use log::debug;
16use std::collections::HashMap;
17
18pub mod appraisal;
19use appraisal::coefficients::calculate_coefficients_for_assets;
20use appraisal::{AppraisalOutput, appraise_investment};
21
22type DemandMap = IndexMap<TimeSliceID, Flow>;
24
25type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceID), Flow>;
27
28pub fn perform_agent_investment(
38 model: &Model,
39 year: u32,
40 existing_assets: &[AssetRef],
41 prices: &CommodityPrices,
42 writer: &mut DataWriter,
43) -> Result<Vec<AssetRef>> {
44 let mut demand =
46 flatten_preset_demands_for_year(&model.commodities, &model.time_slice_info, year);
47
48 let mut all_selected_assets = Vec::new();
51
52 for region_id in model.iter_regions() {
53 let cur_commodities = &model.commodity_order[&(region_id.clone(), year)];
54
55 let mut external_prices =
57 get_prices_for_commodities(prices, &model.time_slice_info, region_id, cur_commodities);
58 let mut seen_commodities = Vec::new();
59 for commodity_id in cur_commodities {
60 seen_commodities.push(commodity_id.clone());
61 let commodity = &model.commodities[commodity_id];
62
63 for time_slice in model.time_slice_info.iter_ids() {
69 external_prices.remove(commodity_id, region_id, time_slice);
70 }
71
72 let mut selected_assets = Vec::new();
74
75 for (agent, commodity_portion) in
76 get_responsible_agents(model.agents.values(), commodity_id, region_id, year)
77 {
78 debug!(
79 "Running investment for agent '{}' with commodity '{}' in region '{}'",
80 &agent.id, commodity_id, region_id
81 );
82
83 let demand_portion_for_commodity = get_demand_portion_for_commodity(
85 &model.time_slice_info,
86 &demand,
87 commodity_id,
88 region_id,
89 commodity_portion,
90 );
91
92 let opt_assets = get_asset_options(
94 &model.time_slice_info,
95 existing_assets,
96 &demand_portion_for_commodity,
97 agent,
98 commodity,
99 region_id,
100 year,
101 )
102 .collect();
103
104 let best_assets = select_best_assets(
106 model,
107 opt_assets,
108 commodity,
109 agent,
110 prices,
111 demand_portion_for_commodity,
112 year,
113 writer,
114 )?;
115 selected_assets.extend(best_assets);
116 }
117
118 if selected_assets.is_empty() {
122 continue;
123 }
124
125 all_selected_assets.extend(selected_assets.iter().cloned());
127
128 debug!(
132 "Running post-investment dispatch for commodity '{commodity_id}' in region '{region_id}'"
133 );
134
135 let solution = DispatchRun::new(model, &all_selected_assets, year)
138 .with_commodity_subset(&seen_commodities)
139 .with_input_prices(&external_prices)
140 .run(
141 &format!("post {commodity_id}/{region_id} investment"),
142 writer,
143 )?;
144
145 update_demand_map(&mut demand, &solution.create_flow_map(), &selected_assets);
147 }
148 }
149
150 Ok(all_selected_assets)
151}
152
153fn flatten_preset_demands_for_year(
163 commodities: &CommodityMap,
164 time_slice_info: &TimeSliceInfo,
165 year: u32,
166) -> AllDemandMap {
167 let mut demand_map = AllDemandMap::new();
168 for (commodity_id, commodity) in commodities {
169 for ((region_id, data_year, time_slice_selection), demand) in &commodity.demand {
170 if *data_year != year {
171 continue;
172 }
173
174 #[allow(clippy::cast_precision_loss)]
178 let n_time_slices = time_slice_selection.iter(time_slice_info).count() as f64;
179 let demand_per_slice = *demand / Dimensionless(n_time_slices);
180 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
181 demand_map.insert(
182 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
183 demand_per_slice,
184 );
185 }
186 }
187 }
188 demand_map
189}
190
191fn update_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
193 for ((asset, commodity_id, time_slice), flow) in flows {
194 if assets.contains(asset) {
195 let key = (
196 commodity_id.clone(),
197 asset.region_id().clone(),
198 time_slice.clone(),
199 );
200
201 demand
203 .entry(key)
204 .and_modify(|value| *value -= *flow)
205 .or_insert(-*flow);
206 }
207 }
208}
209
210fn get_demand_portion_for_commodity(
212 time_slice_info: &TimeSliceInfo,
213 demand: &AllDemandMap,
214 commodity_id: &CommodityID,
215 region_id: &RegionID,
216 commodity_portion: Dimensionless,
217) -> DemandMap {
218 time_slice_info
219 .iter_ids()
220 .map(|time_slice| {
221 (
222 time_slice.clone(),
223 commodity_portion
224 * *demand
225 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
226 .unwrap_or(&Flow(0.0)),
227 )
228 })
229 .collect()
230}
231
232fn get_responsible_agents<'a, I>(
235 agents: I,
236 commodity_id: &'a CommodityID,
237 region_id: &'a RegionID,
238 year: u32,
239) -> impl Iterator<Item = (&'a Agent, Dimensionless)>
240where
241 I: Iterator<Item = &'a Agent>,
242{
243 agents.filter_map(move |agent| {
244 if !agent.regions.contains(region_id) {
245 return None;
246 }
247 let portion = agent
248 .commodity_portions
249 .get(&(commodity_id.clone(), year))?;
250
251 Some((agent, *portion))
252 })
253}
254
255fn get_demand_limiting_capacity(
257 time_slice_info: &TimeSliceInfo,
258 asset: &Asset,
259 commodity: &Commodity,
260 demand: &DemandMap,
261) -> Capacity {
262 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
263 let mut capacity = Capacity(0.0);
264
265 for time_slice_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level)
266 {
267 let demand_for_selection: Flow = time_slice_selection
268 .iter(time_slice_info)
269 .map(|(time_slice, _)| demand[time_slice])
270 .sum();
271
272 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
276 let max_flow_per_cap =
277 *asset.get_activity_per_capacity_limits(time_slice).end() * coeff;
278 if max_flow_per_cap != FlowPerCapacity(0.0) {
279 capacity = capacity.max(demand_for_selection / max_flow_per_cap);
280 }
281 }
282 }
283
284 capacity
285}
286
287fn get_asset_options<'a>(
289 time_slice_info: &'a TimeSliceInfo,
290 all_existing_assets: &'a [AssetRef],
291 demand: &'a DemandMap,
292 agent: &'a Agent,
293 commodity: &'a Commodity,
294 region_id: &'a RegionID,
295 year: u32,
296) -> impl Iterator<Item = AssetRef> + 'a {
297 let existing_assets = all_existing_assets
299 .iter()
300 .filter_agent(&agent.id)
301 .filter_region(region_id)
302 .filter_primary_producers_of(&commodity.id)
303 .cloned();
304
305 let candidate_assets =
307 get_candidate_assets(time_slice_info, demand, agent, region_id, commodity, year);
308
309 chain(existing_assets, candidate_assets)
310}
311
312fn get_candidate_assets<'a>(
314 time_slice_info: &'a TimeSliceInfo,
315 demand: &'a DemandMap,
316 agent: &'a Agent,
317 region_id: &'a RegionID,
318 commodity: &'a Commodity,
319 year: u32,
320) -> impl Iterator<Item = AssetRef> + 'a {
321 agent
322 .iter_possible_producers_of(region_id, &commodity.id, year)
323 .map(move |process| {
324 let mut asset =
325 Asset::new_candidate(process.clone(), region_id.clone(), Capacity(0.0), year)
326 .unwrap();
327 asset.set_capacity(get_demand_limiting_capacity(
328 time_slice_info,
329 &asset,
330 commodity,
331 demand,
332 ));
333
334 asset.into()
335 })
336}
337
338fn get_prices_for_commodities(
340 prices: &CommodityPrices,
341 time_slice_info: &TimeSliceInfo,
342 region_id: &RegionID,
343 commodities: &[CommodityID],
344) -> CommodityPrices {
345 iproduct!(commodities.iter(), time_slice_info.iter_ids())
346 .map(|(commodity_id, time_slice)| {
347 let price = prices.get(commodity_id, region_id, time_slice).unwrap();
348 (commodity_id, region_id, time_slice, price)
349 })
350 .collect()
351}
352
353#[allow(clippy::too_many_arguments)]
355fn select_best_assets(
356 model: &Model,
357 mut opt_assets: Vec<AssetRef>,
358 commodity: &Commodity,
359 agent: &Agent,
360 prices: &CommodityPrices,
361 mut demand: DemandMap,
362 year: u32,
363 writer: &mut DataWriter,
364) -> Result<Vec<AssetRef>> {
365 let objective_type = &agent.objectives[&year];
366
367 let coefficients =
369 calculate_coefficients_for_assets(model, objective_type, &opt_assets, prices, year);
370
371 let mut remaining_candidate_capacity = HashMap::from_iter(
372 opt_assets
373 .iter()
374 .filter(|asset| !asset.is_commissioned())
375 .map(|asset| (asset.clone(), asset.capacity())),
376 );
377
378 let mut round = 0;
379 let mut best_assets: Vec<AssetRef> = Vec::new();
380 while is_any_remaining_demand(&demand) {
381 ensure!(
382 !opt_assets.is_empty(),
383 "Failed to meet demand for commodity '{}' with provided assets",
384 &commodity.id
385 );
386
387 let mut outputs_for_opts = Vec::new();
389 for asset in &opt_assets {
390 let max_capacity = (!asset.is_commissioned()).then(|| {
391 let max_capacity = model.parameters.capacity_limit_factor * asset.capacity();
392 let remaining_capacity = remaining_candidate_capacity[asset];
393 max_capacity.min(remaining_capacity)
394 });
395
396 let output = appraise_investment(
397 model,
398 asset,
399 max_capacity,
400 commodity,
401 objective_type,
402 &coefficients[asset],
403 &demand,
404 )?;
405
406 if output.capacity > Capacity(0.0) {
412 outputs_for_opts.push(output);
413 } else {
414 debug!(
415 "Skipping candidate '{}' with zero capacity",
416 asset.process_id()
417 );
418 }
419 }
420
421 ensure!(
423 !outputs_for_opts.is_empty(),
424 "No feasible investment options for commodity '{}'",
425 &commodity.id
426 );
427
428 writer.write_appraisal_debug_info(
430 year,
431 &format!("{} {} round {}", &commodity.id, &agent.id, round),
432 &outputs_for_opts,
433 )?;
434
435 let best_output = outputs_for_opts
437 .into_iter()
438 .min_by(AppraisalOutput::compare_metric)
439 .unwrap();
440
441 debug!(
443 "Selected {} asset '{}' (capacity: {})",
444 &best_output.asset.state(),
445 &best_output.asset.process_id(),
446 best_output.capacity
447 );
448
449 update_assets(
451 best_output.asset,
452 best_output.capacity,
453 &mut opt_assets,
454 &mut remaining_candidate_capacity,
455 &mut best_assets,
456 );
457
458 demand = best_output.unmet_demand;
459 round += 1;
460 }
461
462 for asset in &mut best_assets {
465 if let AssetState::Candidate = asset.state() {
466 asset
467 .make_mut()
468 .select_candidate_for_investment(agent.id.clone());
469 }
470 }
471
472 Ok(best_assets)
473}
474
475fn is_any_remaining_demand(demand: &DemandMap) -> bool {
477 demand.values().any(|flow| *flow > Flow(0.0))
478}
479
480fn update_assets(
482 mut best_asset: AssetRef,
483 capacity: Capacity,
484 opt_assets: &mut Vec<AssetRef>,
485 remaining_candidate_capacity: &mut HashMap<AssetRef, Capacity>,
486 best_assets: &mut Vec<AssetRef>,
487) {
488 match best_asset.state() {
489 AssetState::Commissioned { .. } => {
490 opt_assets.retain(|asset| *asset != best_asset);
492 best_assets.push(best_asset);
493 }
494 AssetState::Candidate => {
495 let remaining_capacity = remaining_candidate_capacity.get_mut(&best_asset).unwrap();
497 *remaining_capacity -= capacity;
498
499 if *remaining_capacity <= Capacity(0.0) {
501 let old_idx = opt_assets
502 .iter()
503 .position(|asset| *asset == best_asset)
504 .unwrap();
505 opt_assets.swap_remove(old_idx);
506 remaining_candidate_capacity.remove(&best_asset);
507 }
508
509 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset)
510 {
511 existing_asset.make_mut().increase_capacity(capacity);
513 } else {
514 best_asset.make_mut().set_capacity(capacity);
516 best_assets.push(best_asset);
517 }
518 }
519 _ => panic!("update_assets should only be called with Commissioned or Candidate assets"),
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::commodity::Commodity;
527 use crate::fixture::{
528 asset, process, process_parameter_map, region_id, svd_commodity, time_slice,
529 time_slice_info, time_slice_info2,
530 };
531 use crate::process::{FlowType, ProcessFlow};
532 use crate::region::RegionID;
533 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
534 use crate::units::{Dimensionless, Flow, FlowPerActivity, MoneyPerFlow};
535 use indexmap::indexmap;
536 use itertools::Itertools;
537 use map_macro::hash_map;
538 use rstest::rstest;
539 use std::rc::Rc;
540
541 #[rstest]
542 fn test_get_demand_limiting_capacity(
543 time_slice: TimeSliceID,
544 region_id: RegionID,
545 time_slice_info: TimeSliceInfo,
546 svd_commodity: Commodity,
547 ) {
548 let commodity_rc = Rc::new(svd_commodity);
550 let process_flow = ProcessFlow {
551 commodity: Rc::clone(&commodity_rc),
552 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
554 cost: MoneyPerFlow(0.0),
555 };
556
557 let mut process = process(
559 [region_id.clone()].into_iter().collect(),
560 process_parameter_map([region_id.clone()].into_iter().collect()),
561 );
562
563 process.flows.insert(
565 (region_id.clone(), 2015), Rc::new(
567 [(commodity_rc.id.clone(), process_flow)]
568 .into_iter()
569 .collect(),
570 ),
571 );
572
573 process.activity_limits.insert(
575 (region_id.clone(), 2015),
576 Rc::new(hash_map! {time_slice.clone() => Dimensionless(0.0)..=Dimensionless(1.0)}),
577 );
578
579 let asset = asset(process);
581
582 let demand = indexmap! { time_slice.clone() => Flow(10.0)};
584
585 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
587
588 assert_eq!(result, Capacity(5.0));
592 }
593
594 #[rstest]
595 fn test_get_demand_limiting_capacity_multiple_time_slices(
596 time_slice_info2: TimeSliceInfo,
597 svd_commodity: Commodity,
598 region_id: RegionID,
599 ) {
600 let (time_slice1, time_slice2) =
602 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
603
604 let commodity_rc = Rc::new(svd_commodity);
606 let process_flow = ProcessFlow {
607 commodity: Rc::clone(&commodity_rc),
608 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
610 cost: MoneyPerFlow(0.0),
611 };
612
613 let mut process = process(
615 [region_id.clone()].into_iter().collect(),
616 process_parameter_map([region_id.clone()].into_iter().collect()),
617 );
618
619 process.flows.insert(
621 (region_id.clone(), 2015), Rc::new(
623 [(commodity_rc.id.clone(), process_flow)]
624 .into_iter()
625 .collect(),
626 ),
627 );
628
629 let limits = hash_map! {
631 time_slice1.clone() => Dimensionless(0.0)..=Dimensionless(2.0),
633 time_slice2.clone() => Dimensionless(0.0)..=Dimensionless(0.0)
635 };
636 process
637 .activity_limits
638 .insert((region_id.clone(), 2015), limits.into());
639
640 let asset = asset(process);
642
643 let demand = indexmap! {
645 time_slice1.clone() => Flow(4.0), time_slice2.clone() => Flow(3.0), };
648
649 let result =
651 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
652
653 assert_eq!(result, Capacity(2.0));
658 }
659}