1use super::optimisation::{DispatchRun, FlowMap};
3use super::prices::ReducedCosts;
4use crate::agent::Agent;
5use crate::asset::{Asset, AssetIterator, AssetRef, AssetState};
6use crate::commodity::{Commodity, CommodityID, CommodityMap};
7use crate::model::Model;
8use crate::output::DataWriter;
9use crate::region::RegionID;
10use crate::simulation::CommodityPrices;
11use crate::time_slice::{TimeSliceID, TimeSliceInfo};
12use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity, MoneyPerFlow};
13use anyhow::{Result, ensure};
14use indexmap::IndexMap;
15use itertools::{chain, iproduct};
16use log::debug;
17use std::collections::HashMap;
18
19pub mod appraisal;
20use appraisal::appraise_investment;
21
22type DemandMap = IndexMap<TimeSliceID, Flow>;
24
25type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceID), Flow>;
27
28pub fn perform_agent_investment(
39 model: &Model,
40 year: u32,
41 existing_assets: &[AssetRef],
42 prices: &CommodityPrices,
43 reduced_costs: &ReducedCosts,
44 writer: &mut DataWriter,
45) -> Result<Vec<AssetRef>> {
46 let mut demand =
48 flatten_preset_demands_for_year(&model.commodities, &model.time_slice_info, year);
49
50 let mut all_selected_assets = Vec::new();
53
54 for region_id in model.iter_regions() {
55 let cur_commodities = &model.commodity_order[&(region_id.clone(), year)];
56
57 let mut external_prices =
59 get_prices_for_commodities(prices, &model.time_slice_info, region_id, cur_commodities);
60 let mut seen_commodities = Vec::new();
61 for commodity_id in cur_commodities.iter() {
62 seen_commodities.push(commodity_id.clone());
63 let commodity = &model.commodities[commodity_id];
64
65 for time_slice in model.time_slice_info.iter_ids() {
71 external_prices.remove(&(
72 commodity_id.clone(),
73 region_id.clone(),
74 time_slice.clone(),
75 ));
76 }
77
78 let mut selected_assets = Vec::new();
80
81 for (agent, commodity_portion) in
82 get_responsible_agents(model.agents.values(), commodity_id, region_id, year)
83 {
84 debug!(
85 "Running investment for agent '{}' with commodity '{}' in region '{}'",
86 &agent.id, commodity_id, region_id
87 );
88
89 let demand_portion_for_commodity = get_demand_portion_for_commodity(
91 &model.time_slice_info,
92 &demand,
93 commodity_id,
94 region_id,
95 commodity_portion,
96 );
97
98 let opt_assets = get_asset_options(
100 &model.time_slice_info,
101 existing_assets,
102 &demand_portion_for_commodity,
103 agent,
104 commodity,
105 region_id,
106 year,
107 )
108 .collect();
109
110 let best_assets = select_best_assets(
112 model,
113 opt_assets,
114 commodity,
115 agent,
116 reduced_costs,
117 demand_portion_for_commodity,
118 year,
119 writer,
120 )?;
121 selected_assets.extend(best_assets);
122 }
123
124 if selected_assets.is_empty() {
128 continue;
129 }
130
131 all_selected_assets.extend(selected_assets.clone());
133
134 debug!(
138 "Running post-investment dispatch for commodity '{commodity_id}' in region '{region_id}'"
139 );
140
141 let solution = DispatchRun::new(model, &all_selected_assets, year)
144 .with_commodity_subset(&seen_commodities)
145 .with_input_prices(&external_prices)
146 .run(
147 &format!("post {commodity_id}/{region_id} investment"),
148 writer,
149 )?;
150
151 update_demand_map(&mut demand, &solution.create_flow_map(), &selected_assets);
153 }
154 }
155
156 Ok(all_selected_assets)
157}
158
159fn flatten_preset_demands_for_year(
169 commodities: &CommodityMap,
170 time_slice_info: &TimeSliceInfo,
171 year: u32,
172) -> AllDemandMap {
173 let mut demand_map = AllDemandMap::new();
174 for (commodity_id, commodity) in commodities.iter() {
175 for ((region_id, data_year, time_slice_selection), demand) in commodity.demand.iter() {
176 if *data_year != year {
177 continue;
178 }
179
180 let n_timeslices = time_slice_selection.iter(time_slice_info).count() as f64;
184 let demand_per_slice = *demand / Dimensionless(n_timeslices);
185 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
186 demand_map.insert(
187 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
188 demand_per_slice,
189 );
190 }
191 }
192 }
193 demand_map
194}
195
196fn update_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
198 for ((asset, commodity_id, time_slice), flow) in flows.iter() {
199 if assets.contains(asset) {
200 let key = (
201 commodity_id.clone(),
202 asset.region_id().clone(),
203 time_slice.clone(),
204 );
205
206 demand
208 .entry(key)
209 .and_modify(|value| *value -= *flow)
210 .or_insert(-*flow);
211 }
212 }
213}
214
215fn get_demand_portion_for_commodity(
217 time_slice_info: &TimeSliceInfo,
218 demand: &AllDemandMap,
219 commodity_id: &CommodityID,
220 region_id: &RegionID,
221 commodity_portion: Dimensionless,
222) -> DemandMap {
223 time_slice_info
224 .iter_ids()
225 .map(|time_slice| {
226 (
227 time_slice.clone(),
228 commodity_portion
229 * *demand
230 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
231 .unwrap_or(&Flow(0.0)),
232 )
233 })
234 .collect()
235}
236
237fn get_responsible_agents<'a, I>(
240 agents: I,
241 commodity_id: &'a CommodityID,
242 region_id: &'a RegionID,
243 year: u32,
244) -> impl Iterator<Item = (&'a Agent, Dimensionless)>
245where
246 I: Iterator<Item = &'a Agent>,
247{
248 agents.filter_map(move |agent| {
249 if !agent.regions.contains(region_id) {
250 return None;
251 }
252 let portion = agent
253 .commodity_portions
254 .get(&(commodity_id.clone(), year))?;
255
256 Some((agent, *portion))
257 })
258}
259
260fn get_demand_limiting_capacity(
262 time_slice_info: &TimeSliceInfo,
263 asset: &Asset,
264 commodity: &Commodity,
265 demand: &DemandMap,
266) -> Capacity {
267 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
268 let mut capacity = Capacity(0.0);
269
270 for time_slice_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level)
271 {
272 let demand_for_selection: Flow = time_slice_selection
273 .iter(time_slice_info)
274 .map(|(time_slice, _)| demand[time_slice])
275 .sum();
276
277 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
281 let max_flow_per_cap =
282 *asset.get_activity_per_capacity_limits(time_slice).end() * coeff;
283 if max_flow_per_cap != FlowPerCapacity(0.0) {
284 capacity = capacity.max(demand_for_selection / max_flow_per_cap);
285 }
286 }
287 }
288
289 capacity
290}
291
292fn get_asset_options<'a>(
294 time_slice_info: &'a TimeSliceInfo,
295 all_existing_assets: &'a [AssetRef],
296 demand: &'a DemandMap,
297 agent: &'a Agent,
298 commodity: &'a Commodity,
299 region_id: &'a RegionID,
300 year: u32,
301) -> impl Iterator<Item = AssetRef> + 'a {
302 let existing_assets = all_existing_assets
304 .iter()
305 .filter_agent(&agent.id)
306 .filter_region(region_id)
307 .filter_primary_producers_of(&commodity.id)
308 .cloned();
309
310 let candidate_assets =
312 get_candidate_assets(time_slice_info, demand, agent, region_id, commodity, year);
313
314 chain(existing_assets, candidate_assets)
315}
316
317fn get_candidate_assets<'a>(
319 time_slice_info: &'a TimeSliceInfo,
320 demand: &'a DemandMap,
321 agent: &'a Agent,
322 region_id: &'a RegionID,
323 commodity: &'a Commodity,
324 year: u32,
325) -> impl Iterator<Item = AssetRef> + 'a {
326 agent
327 .iter_possible_producers_of(region_id, &commodity.id, year)
328 .map(move |process| {
329 let mut asset =
330 Asset::new_candidate(process.clone(), region_id.clone(), Capacity(0.0), year)
331 .unwrap();
332 asset.set_capacity(get_demand_limiting_capacity(
333 time_slice_info,
334 &asset,
335 commodity,
336 demand,
337 ));
338
339 asset.into()
340 })
341}
342
343fn get_prices_for_commodities(
345 prices: &CommodityPrices,
346 time_slice_info: &TimeSliceInfo,
347 region_id: &RegionID,
348 commodities: &[CommodityID],
349) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow> {
350 iproduct!(commodities.iter(), time_slice_info.iter_ids())
351 .map(|(commodity_id, time_slice)| {
352 let price = prices.get(commodity_id, region_id, time_slice).unwrap();
353 (
354 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
355 price,
356 )
357 })
358 .collect()
359}
360
361#[allow(clippy::too_many_arguments)]
363fn select_best_assets(
364 model: &Model,
365 mut opt_assets: Vec<AssetRef>,
366 commodity: &Commodity,
367 agent: &Agent,
368 reduced_costs: &ReducedCosts,
369 mut demand: DemandMap,
370 year: u32,
371 writer: &mut DataWriter,
372) -> Result<Vec<AssetRef>> {
373 let mut best_assets: Vec<AssetRef> = Vec::new();
374
375 let mut remaining_candidate_capacity = HashMap::from_iter(
376 opt_assets
377 .iter()
378 .filter(|asset| !asset.is_commissioned())
379 .map(|asset| (asset.clone(), asset.capacity())),
380 );
381
382 let mut round = 0;
383 while is_any_remaining_demand(&demand) {
384 ensure!(
385 !opt_assets.is_empty(),
386 "Failed to meet demand for commodity '{}' with provided assets",
387 &commodity.id
388 );
389
390 let mut outputs_for_opts = Vec::new();
392 for asset in opt_assets.iter() {
393 let max_capacity = (!asset.is_commissioned()).then(|| {
394 let max_capacity = model.parameters.capacity_limit_factor * asset.capacity();
395 let remaining_capacity = remaining_candidate_capacity[asset];
396 max_capacity.min(remaining_capacity)
397 });
398
399 let output = appraise_investment(
400 model,
401 asset,
402 max_capacity,
403 commodity,
404 &agent.objectives[&year],
405 reduced_costs,
406 &demand,
407 )?;
408
409 if output.capacity > Capacity(0.0) {
415 outputs_for_opts.push(output);
416 } else {
417 debug!(
418 "Skipping candidate '{}' with zero capacity",
419 asset.process_id()
420 );
421 }
422 }
423
424 ensure!(
426 !outputs_for_opts.is_empty(),
427 "No feasible investment options for commodity '{}'",
428 &commodity.id
429 );
430
431 writer.write_appraisal_debug_info(
433 year,
434 &format!("{} {} round {}", &commodity.id, &agent.id, round),
435 &outputs_for_opts,
436 )?;
437
438 let best_output = outputs_for_opts
440 .into_iter()
441 .min_by(|a, b| a.metric.partial_cmp(&b.metric).unwrap())
442 .unwrap();
443
444 let commissioned_txt = match best_output.asset.state() {
446 AssetState::Commissioned { .. } => "existing",
447 AssetState::Candidate => "candidate",
448 _ => panic!("Selected asset should be either Commissioned or Candidate"),
449 };
450 debug!(
451 "Selected {} asset '{}' (capacity: {})",
452 commissioned_txt,
453 &best_output.asset.process_id(),
454 best_output.capacity
455 );
456
457 update_assets(
459 best_output.asset,
460 best_output.capacity,
461 &mut opt_assets,
462 &mut remaining_candidate_capacity,
463 &mut best_assets,
464 );
465
466 demand = best_output.unmet_demand;
467 round += 1;
468 }
469
470 for asset in best_assets.iter_mut() {
473 if let AssetState::Candidate = asset.state() {
474 asset
475 .make_mut()
476 .select_candidate_for_investment(agent.id.clone());
477 }
478 }
479
480 Ok(best_assets)
481}
482
483fn is_any_remaining_demand(demand: &DemandMap) -> bool {
485 demand.values().any(|flow| *flow > Flow(0.0))
486}
487
488fn update_assets(
490 mut best_asset: AssetRef,
491 capacity: Capacity,
492 opt_assets: &mut Vec<AssetRef>,
493 remaining_candidate_capacity: &mut HashMap<AssetRef, Capacity>,
494 best_assets: &mut Vec<AssetRef>,
495) {
496 match best_asset.state() {
497 AssetState::Commissioned { .. } => {
498 opt_assets.retain(|asset| *asset != best_asset);
500 best_assets.push(best_asset);
501 }
502 AssetState::Candidate => {
503 let remaining_capacity = remaining_candidate_capacity.get_mut(&best_asset).unwrap();
505 *remaining_capacity -= capacity;
506
507 if *remaining_capacity <= Capacity(0.0) {
509 let old_idx = opt_assets
510 .iter()
511 .position(|asset| *asset == best_asset)
512 .unwrap();
513 opt_assets.swap_remove(old_idx);
514 remaining_candidate_capacity.remove(&best_asset);
515 }
516
517 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset)
518 {
519 existing_asset.make_mut().increase_capacity(capacity);
521 } else {
522 best_asset.make_mut().set_capacity(capacity);
524 best_assets.push(best_asset);
525 };
526 }
527 _ => panic!("update_assets should only be called with Commissioned or Candidate assets"),
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::commodity::Commodity;
535 use crate::fixture::{
536 asset, process, process_parameter_map, region_id, svd_commodity, time_slice,
537 time_slice_info, time_slice_info2,
538 };
539 use crate::process::{FlowType, ProcessFlow, ProcessParameter};
540 use crate::region::RegionID;
541 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
542 use crate::units::{
543 ActivityPerCapacity, Dimensionless, Flow, FlowPerActivity, MoneyPerActivity,
544 MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
545 };
546 use indexmap::indexmap;
547 use itertools::Itertools;
548 use rstest::rstest;
549 use std::rc::Rc;
550
551 fn process_parameter_with_capacity_to_activity() -> Rc<ProcessParameter> {
553 Rc::new(ProcessParameter {
554 capital_cost: MoneyPerCapacity(0.0),
555 fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
556 variable_operating_cost: MoneyPerActivity(0.0),
557 lifetime: 1,
558 discount_rate: Dimensionless(1.0),
559 capacity_to_activity: ActivityPerCapacity(1.0), })
561 }
562
563 #[rstest]
564 fn test_get_demand_limiting_capacity(
565 time_slice: TimeSliceID,
566 region_id: RegionID,
567 time_slice_info: TimeSliceInfo,
568 svd_commodity: Commodity,
569 ) {
570 let commodity_rc = Rc::new(svd_commodity);
572 let process_flow = ProcessFlow {
573 commodity: Rc::clone(&commodity_rc),
574 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
576 cost: MoneyPerFlow(0.0),
577 };
578
579 let mut process = process(
581 [region_id.clone()].into_iter().collect(),
582 process_parameter_map([region_id.clone()].into_iter().collect()),
583 );
584
585 process.flows.insert(
587 (region_id.clone(), 2015), [(commodity_rc.id.clone(), process_flow)]
589 .into_iter()
590 .collect(),
591 );
592
593 process.activity_limits.insert(
595 (region_id.clone(), 2015, time_slice.clone()),
596 Dimensionless(0.0)..=Dimensionless(1.0),
597 );
598
599 let updated_parameter = process_parameter_with_capacity_to_activity();
601 process
602 .parameters
603 .insert((region_id.clone(), 2015), updated_parameter);
604
605 let asset = asset(process);
607
608 let demand = indexmap! { time_slice.clone() => Flow(10.0)};
610
611 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
613
614 assert_eq!(result, Capacity(5.0));
618 }
619
620 #[rstest]
621 fn test_get_demand_limiting_capacity_multiple_time_slices(
622 time_slice_info2: TimeSliceInfo,
623 svd_commodity: Commodity,
624 region_id: RegionID,
625 ) {
626 let (time_slice1, time_slice2) =
628 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
629
630 let commodity_rc = Rc::new(svd_commodity);
632 let process_flow = ProcessFlow {
633 commodity: Rc::clone(&commodity_rc),
634 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
636 cost: MoneyPerFlow(0.0),
637 };
638
639 let mut process = process(
641 [region_id.clone()].into_iter().collect(),
642 process_parameter_map([region_id.clone()].into_iter().collect()),
643 );
644
645 process.flows.insert(
647 (region_id.clone(), 2015), [(commodity_rc.id.clone(), process_flow)]
649 .into_iter()
650 .collect(),
651 );
652
653 process.activity_limits.insert(
655 (region_id.clone(), 2015, time_slice1.clone()),
656 Dimensionless(0.0)..=Dimensionless(2.0), );
658 process.activity_limits.insert(
659 (region_id.clone(), 2015, time_slice2.clone()),
660 Dimensionless(0.0)..=Dimensionless(0.0), );
662
663 let updated_parameter = process_parameter_with_capacity_to_activity();
665 process
666 .parameters
667 .insert((region_id.clone(), 2015), updated_parameter);
668
669 let asset = asset(process);
671
672 let demand = indexmap! {
674 time_slice1.clone() => Flow(4.0), time_slice2.clone() => Flow(3.0), };
677
678 let result =
680 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
681
682 assert_eq!(result, Capacity(2.0));
687 }
688}