1use super::optimisation::{DispatchRun, FlowMap};
3use crate::agent::{Agent, AgentID};
4use crate::asset::{Asset, AssetCapacity, 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::{Context, Result, bail, ensure};
13use indexmap::IndexMap;
14use itertools::{Itertools, chain};
15use log::debug;
16use std::collections::{HashMap, HashSet};
17use std::fmt::Display;
18
19pub mod appraisal;
20use appraisal::coefficients::calculate_coefficients_for_assets;
21use appraisal::{
22 AppraisalOutput, appraise_investment, count_equal_and_best_appraisal_outputs,
23 sort_and_filter_appraisal_outputs,
24};
25
26type DemandMap = IndexMap<TimeSliceID, Flow>;
28
29type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceID), Flow>;
31
32#[derive(PartialEq, Debug, Clone, Eq, Hash)]
34pub enum InvestmentSet {
35 Single((CommodityID, RegionID)),
37 Cycle(Vec<(CommodityID, RegionID)>),
41 Layer(Vec<InvestmentSet>),
43}
44
45impl InvestmentSet {
46 pub fn iter_markets<'a>(
48 &'a self,
49 ) -> Box<dyn Iterator<Item = &'a (CommodityID, RegionID)> + 'a> {
50 match self {
51 InvestmentSet::Single(market) => Box::new(std::iter::once(market)),
52 InvestmentSet::Cycle(markets) => Box::new(markets.iter()),
53 InvestmentSet::Layer(set) => Box::new(set.iter().flat_map(|s| s.iter_markets())),
54 }
55 }
56
57 #[allow(clippy::too_many_arguments)]
71 fn select_assets(
72 &self,
73 model: &Model,
74 year: u32,
75 demand: &AllDemandMap,
76 existing_assets: &[AssetRef],
77 prices: &CommodityPrices,
78 seen_markets: &[(CommodityID, RegionID)],
79 previously_selected_assets: &[AssetRef],
80 writer: &mut DataWriter,
81 ) -> Result<Vec<AssetRef>> {
82 match self {
83 InvestmentSet::Single((commodity_id, region_id)) => select_assets_for_single_market(
84 model,
85 commodity_id,
86 region_id,
87 year,
88 demand,
89 existing_assets,
90 prices,
91 writer,
92 ),
93 InvestmentSet::Cycle(markets) => {
94 debug!("Starting investment for cycle '{self}'");
95 select_assets_for_cycle(
96 model,
97 markets,
98 year,
99 demand,
100 existing_assets,
101 prices,
102 seen_markets,
103 previously_selected_assets,
104 writer,
105 )
106 .with_context(|| {
107 format!(
108 "Investments failed for market set {self} with cyclical dependencies. \
109 Please note that the investment algorithm is currently experimental for \
110 models with circular commodity dependencies and may not be able to find \
111 a solution in all cases."
112 )
113 })
114 }
115 InvestmentSet::Layer(investment_sets) => {
116 debug!("Starting asset selection for layer '{self}'");
117 let mut all_assets = Vec::new();
118 for investment_set in investment_sets {
119 let assets = investment_set.select_assets(
120 model,
121 year,
122 demand,
123 existing_assets,
124 prices,
125 seen_markets,
126 previously_selected_assets,
127 writer,
128 )?;
129 all_assets.extend(assets);
130 }
131 debug!("Completed asset selection for layer '{self}'");
132 Ok(all_assets)
133 }
134 }
135 }
136}
137
138impl Display for InvestmentSet {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 match self {
141 InvestmentSet::Single((commodity_id, region_id)) => {
142 write!(f, "{commodity_id}|{region_id}")
143 }
144 InvestmentSet::Cycle(markets) => {
145 write!(
146 f,
147 "({})",
148 markets.iter().map(|(c, r)| format!("{c}|{r}")).join(", ")
149 )
150 }
151 InvestmentSet::Layer(ids) => {
152 write!(f, "[{}]", ids.iter().join(", "))
153 }
154 }
155 }
156}
157
158pub fn perform_agent_investment(
173 model: &Model,
174 year: u32,
175 existing_assets: &[AssetRef],
176 prices: &CommodityPrices,
177 writer: &mut DataWriter,
178) -> Result<Vec<AssetRef>> {
179 let mut net_demand =
181 flatten_preset_demands_for_year(&model.commodities, &model.time_slice_info, year);
182
183 let mut all_selected_assets = Vec::new();
186
187 let investment_order = &model.investment_order[&year];
188 debug!(
189 "Investment order for year '{year}': {}",
190 investment_order.iter().join(" -> ")
191 );
192
193 let mut seen_markets = Vec::new();
197
198 for investment_set in investment_order {
200 let selected_assets = investment_set.select_assets(
202 model,
203 year,
204 &net_demand,
205 existing_assets,
206 prices,
207 &seen_markets,
208 &all_selected_assets,
209 writer,
210 )?;
211
212 for market in investment_set.iter_markets() {
214 seen_markets.push(market.clone());
215 }
216
217 if selected_assets.is_empty() {
221 debug!("No assets selected for '{investment_set}'");
222 continue;
223 }
224
225 all_selected_assets.extend(selected_assets.iter().cloned());
227
228 debug!("Running post-investment dispatch for '{investment_set}'");
232
233 let solution = DispatchRun::new(model, &all_selected_assets, year)
236 .with_market_balance_subset(&seen_markets)
237 .with_input_prices(prices)
238 .run(&format!("post {investment_set} investment"), writer)?;
239
240 update_net_demand_map(
242 &mut net_demand,
243 &solution.create_flow_map(),
244 &selected_assets,
245 );
246 }
247
248 Ok(all_selected_assets)
249}
250
251#[allow(clippy::too_many_arguments)]
255fn select_assets_for_single_market(
256 model: &Model,
257 commodity_id: &CommodityID,
258 region_id: &RegionID,
259 year: u32,
260 demand: &AllDemandMap,
261 existing_assets: &[AssetRef],
262 prices: &CommodityPrices,
263 writer: &mut DataWriter,
264) -> Result<Vec<AssetRef>> {
265 let commodity = &model.commodities[commodity_id];
266
267 let mut selected_assets = Vec::new();
268 for (agent, commodity_portion) in
269 get_responsible_agents(model.agents.values(), commodity_id, region_id, year)
270 {
271 debug!(
272 "Running asset selection for agent '{}' in market '{}|{}'",
273 &agent.id, commodity_id, region_id
274 );
275
276 let demand_portion_for_market = get_demand_portion_for_market(
278 &model.time_slice_info,
279 demand,
280 commodity_id,
281 region_id,
282 commodity_portion,
283 );
284
285 let opt_assets = get_asset_options(
287 &model.time_slice_info,
288 existing_assets,
289 &demand_portion_for_market,
290 agent,
291 commodity,
292 region_id,
293 year,
294 )
295 .collect::<Vec<_>>();
296
297 let investment_limits =
299 calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
300
301 let best_assets = select_best_assets(
303 model,
304 opt_assets,
305 investment_limits,
306 commodity,
307 agent,
308 region_id,
309 prices,
310 demand_portion_for_market,
311 year,
312 writer,
313 )?;
314 selected_assets.extend(best_assets);
315 }
316
317 Ok(selected_assets)
318}
319
320#[allow(clippy::too_many_arguments)]
335fn select_assets_for_cycle(
336 model: &Model,
337 markets: &[(CommodityID, RegionID)],
338 year: u32,
339 demand: &AllDemandMap,
340 existing_assets: &[AssetRef],
341 prices: &CommodityPrices,
342 seen_markets: &[(CommodityID, RegionID)],
343 previously_selected_assets: &[AssetRef],
344 writer: &mut DataWriter,
345) -> Result<Vec<AssetRef>> {
346 let markets_str = markets.iter().map(|(c, r)| format!("{c}|{r}")).join(", ");
348
349 let mut current_demand = demand.clone();
351 let mut assets_for_cycle = IndexMap::new();
352 let mut last_solution = None;
353 for (idx, (commodity_id, region_id)) in markets.iter().enumerate() {
354 let assets = select_assets_for_single_market(
356 model,
357 commodity_id,
358 region_id,
359 year,
360 ¤t_demand,
361 existing_assets,
362 prices,
363 writer,
364 )?;
365 assets_for_cycle.insert((commodity_id.clone(), region_id.clone()), assets);
366
367 let mut all_assets = previously_selected_assets.to_vec();
369 let assets_for_cycle_flat: Vec<_> = assets_for_cycle
370 .values()
371 .flat_map(|v| v.iter().cloned())
372 .collect();
373 all_assets.extend_from_slice(&assets_for_cycle_flat);
374
375 let mut markets_to_balance = seen_markets.to_vec();
377 markets_to_balance.extend_from_slice(&markets[0..=idx]);
378
379 let flexible_capacity_assets: Vec<_> = assets_for_cycle_flat
381 .iter()
382 .filter(|asset| matches!(asset.state(), AssetState::Selected { .. }))
383 .cloned()
384 .collect();
385
386 let key = (commodity_id.clone(), year);
388 let mut agent_share_cache = HashMap::new();
389 let capacity_limits = flexible_capacity_assets
390 .iter()
391 .filter_map(|asset| {
392 let agent_id = asset.agent_id().unwrap();
393 let agent_share = *agent_share_cache
394 .entry(agent_id.clone())
395 .or_insert_with(|| model.agents[agent_id].commodity_portions[&key]);
396 asset
397 .max_installable_capacity(agent_share)
398 .map(|max_capacity| (asset.clone(), max_capacity))
399 })
400 .collect::<HashMap<_, _>>();
401
402 let solution = DispatchRun::new(model, &all_assets, year)
404 .with_market_balance_subset(&markets_to_balance)
405 .with_flexible_capacity_assets(
406 &flexible_capacity_assets,
407 Some(&capacity_limits),
408 model.parameters.capacity_margin,
410 )
411 .run(
412 &format!("cycle ({markets_str}) post {commodity_id}|{region_id} investment",),
413 writer,
414 )
415 .with_context(|| {
416 format!(
417 "Cycle balancing failed for cycle ({markets_str}), capacity_margin: {}. \
418 Try increasing the capacity_margin.",
419 model.parameters.capacity_margin
420 )
421 })?;
422
423 current_demand.clone_from(demand);
425 update_net_demand_map(
426 &mut current_demand,
427 &solution.create_flow_map(),
428 &assets_for_cycle_flat,
429 );
430 last_solution = Some(solution);
431 }
432
433 let mut all_cycle_assets: Vec<_> = assets_for_cycle.into_values().flatten().collect();
435 if let Some(solution) = last_solution {
436 let new_capacities: HashMap<_, _> = solution.iter_capacity().collect();
437 for asset in &mut all_cycle_assets {
438 if let Some(new_capacity) = new_capacities.get(asset) {
439 debug!(
440 "Capacity of asset '{}' modified during cycle balancing ({} to {})",
441 asset.process_id(),
442 asset.total_capacity(),
443 new_capacity.total_capacity()
444 );
445 asset.make_mut().set_capacity(*new_capacity);
446 }
447 }
448 }
449
450 Ok(all_cycle_assets)
451}
452
453fn flatten_preset_demands_for_year(
463 commodities: &CommodityMap,
464 time_slice_info: &TimeSliceInfo,
465 year: u32,
466) -> AllDemandMap {
467 let mut demand_map = AllDemandMap::new();
468 for (commodity_id, commodity) in commodities {
469 for ((region_id, data_year, time_slice_selection), demand) in &commodity.demand {
470 if *data_year != year {
471 continue;
472 }
473
474 #[allow(clippy::cast_precision_loss)]
478 let n_time_slices = time_slice_selection.iter(time_slice_info).count() as f64;
479 let demand_per_slice = *demand / Dimensionless(n_time_slices);
480 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
481 demand_map.insert(
482 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
483 demand_per_slice,
484 );
485 }
486 }
487 }
488 demand_map
489}
490
491fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
501 for ((asset, commodity_id, time_slice), flow) in flows {
502 if assets.contains(asset) {
503 let key = (
504 commodity_id.clone(),
505 asset.region_id().clone(),
506 time_slice.clone(),
507 );
508
509 if (flow < &Flow(0.0))
512 || asset
513 .primary_output()
514 .is_some_and(|p| &p.commodity.id == commodity_id)
515 {
516 demand
518 .entry(key)
519 .and_modify(|value| *value -= *flow)
520 .or_insert(-*flow);
521 }
522 }
523 }
524}
525
526fn get_demand_portion_for_market(
528 time_slice_info: &TimeSliceInfo,
529 demand: &AllDemandMap,
530 commodity_id: &CommodityID,
531 region_id: &RegionID,
532 commodity_portion: Dimensionless,
533) -> DemandMap {
534 time_slice_info
535 .iter_ids()
536 .map(|time_slice| {
537 (
538 time_slice.clone(),
539 commodity_portion
540 * *demand
541 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
542 .unwrap_or(&Flow(0.0)),
543 )
544 })
545 .collect()
546}
547
548fn get_responsible_agents<'a, I>(
551 agents: I,
552 commodity_id: &'a CommodityID,
553 region_id: &'a RegionID,
554 year: u32,
555) -> impl Iterator<Item = (&'a Agent, Dimensionless)>
556where
557 I: Iterator<Item = &'a Agent>,
558{
559 agents.filter_map(move |agent| {
560 if !agent.regions.contains(region_id) {
561 return None;
562 }
563 let portion = agent
564 .commodity_portions
565 .get(&(commodity_id.clone(), year))?;
566
567 Some((agent, *portion))
568 })
569}
570
571fn get_demand_limiting_capacity(
573 time_slice_info: &TimeSliceInfo,
574 asset: &Asset,
575 commodity: &Commodity,
576 demand: &DemandMap,
577) -> Capacity {
578 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
579 let mut capacity = Capacity(0.0);
580
581 for time_slice_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level)
582 {
583 let demand_for_selection: Flow = time_slice_selection
584 .iter(time_slice_info)
585 .map(|(time_slice, _)| demand[time_slice])
586 .sum();
587
588 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
592 let max_flow_per_cap =
593 *asset.get_activity_per_capacity_limits(time_slice).end() * coeff;
594 if max_flow_per_cap != FlowPerCapacity(0.0) {
595 capacity = capacity.max(demand_for_selection / max_flow_per_cap);
596 }
597 }
598 }
599
600 capacity
601}
602
603fn get_asset_options<'a>(
605 time_slice_info: &'a TimeSliceInfo,
606 all_existing_assets: &'a [AssetRef],
607 demand: &'a DemandMap,
608 agent: &'a Agent,
609 commodity: &'a Commodity,
610 region_id: &'a RegionID,
611 year: u32,
612) -> impl Iterator<Item = AssetRef> + 'a {
613 let existing_assets = all_existing_assets
615 .iter()
616 .filter_agent(&agent.id)
617 .filter_region(region_id)
618 .filter_primary_producers_of(&commodity.id)
619 .cloned();
620
621 let candidate_assets =
623 get_candidate_assets(time_slice_info, demand, agent, region_id, commodity, year);
624
625 chain(existing_assets, candidate_assets)
626}
627
628fn get_candidate_assets<'a>(
630 time_slice_info: &'a TimeSliceInfo,
631 demand: &'a DemandMap,
632 agent: &'a Agent,
633 region_id: &'a RegionID,
634 commodity: &'a Commodity,
635 year: u32,
636) -> impl Iterator<Item = AssetRef> + 'a {
637 agent
638 .iter_possible_producers_of(region_id, &commodity.id, year)
639 .map(move |process| {
640 let mut asset =
641 Asset::new_candidate(process.clone(), region_id.clone(), Capacity(0.0), year)
642 .unwrap();
643
644 let capacity = get_demand_limiting_capacity(time_slice_info, &asset, commodity, demand);
647 let asset_capacity = AssetCapacity::from_capacity(capacity, asset.unit_size());
648 asset.set_capacity(asset_capacity);
649
650 asset.into()
651 })
652}
653
654fn log_on_equal_appraisal_outputs(
656 outputs: &[AppraisalOutput],
657 agent_id: &AgentID,
658 commodity_id: &CommodityID,
659 region_id: &RegionID,
660) {
661 if outputs.is_empty() {
662 return;
663 }
664
665 let num_identical = count_equal_and_best_appraisal_outputs(outputs);
666
667 if num_identical > 0 {
668 let asset_details = outputs[..=num_identical]
669 .iter()
670 .map(|output| {
671 let asset = &output.asset;
672 format!(
673 "Process ID: '{}' (State: {}{}, Commission year: {})",
674 asset.process_id(),
675 asset.state(),
676 asset
677 .id()
678 .map(|id| format!(", Asset ID: {id}"))
679 .unwrap_or_default(),
680 asset.commission_year()
681 )
682 })
683 .join(", ");
684 debug!(
685 "Found equally good appraisals for Agent ID: {agent_id}, Commodity: '{commodity_id}', \
686 Region: {region_id}. Options: [{asset_details}]. Selecting first option.",
687 );
688 }
689}
690
691fn calculate_investment_limits_for_candidates(
698 opt_assets: &[AssetRef],
699 commodity_portion: Dimensionless,
700) -> HashMap<AssetRef, AssetCapacity> {
701 opt_assets
703 .iter()
704 .filter(|asset| !asset.is_commissioned())
705 .map(|asset| {
706 let mut cap = asset.capacity();
708
709 if let Some(limit_capacity) = asset.max_installable_capacity(commodity_portion) {
711 cap = cap.min(limit_capacity);
712 }
713
714 (asset.clone(), cap)
715 })
716 .collect()
717}
718
719#[allow(clippy::too_many_arguments)]
721fn select_best_assets(
722 model: &Model,
723 mut opt_assets: Vec<AssetRef>,
724 investment_limits: HashMap<AssetRef, AssetCapacity>,
725 commodity: &Commodity,
726 agent: &Agent,
727 region_id: &RegionID,
728 prices: &CommodityPrices,
729 mut demand: DemandMap,
730 year: u32,
731 writer: &mut DataWriter,
732) -> Result<Vec<AssetRef>> {
733 let objective_type = &agent.objectives[&year];
734 let mut remaining_candidate_capacity = investment_limits;
735
736 let coefficients =
738 calculate_coefficients_for_assets(model, objective_type, &opt_assets, prices, year);
739
740 let mut round = 0;
742 let mut best_assets: Vec<AssetRef> = Vec::new();
743 while is_any_remaining_demand(
744 &demand,
745 model.parameters.remaining_demand_absolute_tolerance,
746 ) {
747 ensure!(
748 !opt_assets.is_empty(),
749 "Failed to meet demand for commodity '{}' in region '{}' with provided investment \
750 options. This may be due to overly restrictive process investment constraints.",
751 &commodity.id,
752 region_id
753 );
754
755 let mut seen_groups = HashSet::new();
758
759 let mut outputs_for_opts = Vec::new();
761 for asset in &opt_assets {
762 let max_capacity = (!asset.is_commissioned()).then(|| {
765 let tranche_capacity = asset
766 .capacity()
767 .apply_limit_factor(model.parameters.capacity_limit_factor);
768 let remaining_capacity = remaining_candidate_capacity[asset];
769 tranche_capacity.min(remaining_capacity)
770 });
771
772 if let Some(group_id) = asset.group_id()
774 && !seen_groups.insert(group_id)
775 {
776 continue;
777 }
778
779 let output = appraise_investment(
780 model,
781 asset,
782 max_capacity,
783 commodity,
784 objective_type,
785 &coefficients[asset],
786 &demand,
787 )?;
788 outputs_for_opts.push(output);
789 }
790
791 writer.write_appraisal_debug_info(
793 year,
794 &format!("{} {} round {}", &commodity.id, &agent.id, round),
795 &outputs_for_opts,
796 &demand,
797 )?;
798
799 sort_and_filter_appraisal_outputs(&mut outputs_for_opts);
801
802 if outputs_for_opts.is_empty() {
805 let remaining_demands: Vec<_> = demand
806 .iter()
807 .filter(|(_, flow)| **flow > Flow(0.0))
808 .map(|(time_slice, flow)| format!("{} : {:e}", time_slice, flow.value()))
809 .collect();
810
811 bail!(
812 "No feasible investment options left for \
813 commodity '{}', region '{}', year '{}', agent '{}' after appraisal.\n\
814 Remaining unmet demand (time_slice : flow):\n{}",
815 &commodity.id,
816 region_id,
817 year,
818 agent.id,
819 remaining_demands.join("\n")
820 );
821 }
822
823 log_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id);
825
826 let best_output = outputs_for_opts.into_iter().next().unwrap();
827
828 debug!(
830 "Selected {} asset '{}' (capacity: {})",
831 &best_output.asset.state(),
832 &best_output.asset.process_id(),
833 best_output.capacity.total_capacity()
834 );
835
836 update_assets(
838 best_output.asset,
839 best_output.capacity,
840 &mut opt_assets,
841 &mut remaining_candidate_capacity,
842 &mut best_assets,
843 );
844
845 demand = best_output.unmet_demand;
846 round += 1;
847 }
848
849 for asset in &mut best_assets {
852 if let AssetState::Candidate = asset.state() {
853 asset
854 .make_mut()
855 .select_candidate_for_investment(agent.id.clone());
856 }
857 }
858
859 Ok(best_assets)
860}
861
862fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: Flow) -> bool {
864 demand.values().any(|flow| *flow > absolute_tolerance)
865}
866
867fn update_assets(
869 mut best_asset: AssetRef,
870 capacity: AssetCapacity,
871 opt_assets: &mut Vec<AssetRef>,
872 remaining_candidate_capacity: &mut HashMap<AssetRef, AssetCapacity>,
873 best_assets: &mut Vec<AssetRef>,
874) {
875 match best_asset.state() {
876 AssetState::Commissioned { .. } => {
877 opt_assets.retain(|asset| *asset != best_asset);
879 best_assets.push(best_asset);
880 }
881 AssetState::Candidate => {
882 let remaining_capacity = remaining_candidate_capacity.get_mut(&best_asset).unwrap();
884 *remaining_capacity = *remaining_capacity - capacity;
885
886 if remaining_capacity.total_capacity() <= Capacity(0.0) {
888 let old_idx = opt_assets
889 .iter()
890 .position(|asset| *asset == best_asset)
891 .unwrap();
892 opt_assets.swap_remove(old_idx);
893 remaining_candidate_capacity.remove(&best_asset);
894 }
895
896 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset)
897 {
898 existing_asset.make_mut().increase_capacity(capacity);
900 } else {
901 best_asset.make_mut().set_capacity(capacity);
903 best_assets.push(best_asset);
904 }
905 }
906 _ => panic!("update_assets should only be called with Commissioned or Candidate assets"),
907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use crate::commodity::Commodity;
914 use crate::fixture::{
915 agent_id, asset, process, process_activity_limits_map, process_flows_map,
916 process_investment_constraints, process_parameter_map, region_id, svd_commodity,
917 time_slice, time_slice_info, time_slice_info2,
918 };
919 use crate::process::{
920 ActivityLimits, FlowType, Process, ProcessActivityLimitsMap, ProcessFlow, ProcessFlowsMap,
921 ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessParameterMap,
922 };
923 use crate::region::RegionID;
924 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
925 use crate::units::Dimensionless;
926 use crate::units::{ActivityPerCapacity, Capacity, Flow, FlowPerActivity, MoneyPerFlow};
927 use indexmap::{IndexSet, indexmap};
928 use rstest::rstest;
929 use std::rc::Rc;
930
931 #[rstest]
932 fn get_demand_limiting_capacity_works(
933 time_slice: TimeSliceID,
934 time_slice_info: TimeSliceInfo,
935 svd_commodity: Commodity,
936 mut process: Process,
937 ) {
938 let commodity_rc = Rc::new(svd_commodity);
940 let process_flow = ProcessFlow {
941 commodity: Rc::clone(&commodity_rc),
942 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
944 cost: MoneyPerFlow(0.0),
945 };
946 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
947 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
948 process.flows = process_flows_map;
949
950 let asset = asset(process);
952
953 let demand = indexmap! { time_slice.clone() => Flow(10.0)};
955
956 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
958
959 assert_eq!(result, Capacity(5.0));
963 }
964
965 #[rstest]
966 fn get_demand_limiting_capacity_multiple_time_slices(
967 time_slice_info2: TimeSliceInfo,
968 svd_commodity: Commodity,
969 mut process: Process,
970 ) {
971 let (time_slice1, time_slice2) =
972 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
973
974 let commodity_rc = Rc::new(svd_commodity);
976 let process_flow = ProcessFlow {
977 commodity: Rc::clone(&commodity_rc),
978 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
980 cost: MoneyPerFlow(0.0),
981 };
982 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
983 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
984 process.flows = process_flows_map;
985
986 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
988 limits.add_time_slice_limit(time_slice1.clone(), Dimensionless(0.0)..=Dimensionless(0.2));
989 limits.add_time_slice_limit(time_slice2.clone(), Dimensionless(0.0)..=Dimensionless(0.0));
990 let limits_map = process_activity_limits_map(process.regions.clone(), limits);
991 process.activity_limits = limits_map;
992
993 let asset = asset(process);
995
996 let demand = indexmap! {
998 time_slice1.clone() => Flow(4.0), time_slice2.clone() => Flow(3.0), };
1001
1002 let result =
1004 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
1005
1006 assert_eq!(result, Capacity(20.0));
1011 }
1012
1013 #[rstest]
1014 fn calculate_investment_limits_for_candidates_empty_list() {
1015 let opt_assets: Vec<AssetRef> = vec![];
1017 let commodity_portion = Dimensionless(1.0);
1018
1019 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1020
1021 assert!(result.is_empty());
1022 }
1023
1024 #[rstest]
1025 fn calculate_investment_limits_for_candidates_commissioned_assets_filtered(
1026 process: Process,
1027 region_id: RegionID,
1028 agent_id: AgentID,
1029 ) {
1030 let process_rc = Rc::new(process);
1032 let capacity = Capacity(10.0);
1033
1034 let commissioned_asset = Asset::new_commissioned(
1036 agent_id.clone(),
1037 process_rc.clone(),
1038 region_id.clone(),
1039 capacity,
1040 2015,
1041 )
1042 .unwrap();
1043
1044 let candidate_asset =
1046 Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2015).unwrap();
1047
1048 let candidate_asset_ref = AssetRef::from(candidate_asset);
1049 let opt_assets = vec![
1050 AssetRef::from(commissioned_asset),
1051 candidate_asset_ref.clone(),
1052 ];
1053 let commodity_portion = Dimensionless(1.0);
1054
1055 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1056
1057 assert_eq!(result.len(), 1);
1059 assert!(result.contains_key(&candidate_asset_ref));
1060 }
1061
1062 #[rstest]
1063 fn calculate_investment_limits_for_candidates_no_investment_constraints(
1064 process: Process,
1065 region_id: RegionID,
1066 ) {
1067 let process_rc = Rc::new(process);
1069 let capacity = Capacity(15.0);
1070
1071 let candidate_asset = Asset::new_candidate(process_rc, region_id, capacity, 2015).unwrap();
1072
1073 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1074 let commodity_portion = Dimensionless(0.8);
1075
1076 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1077
1078 assert_eq!(result.len(), 1);
1080 let asset_ref = AssetRef::from(candidate_asset);
1081 assert_eq!(result[&asset_ref], AssetCapacity::Continuous(capacity));
1082 }
1083
1084 #[rstest]
1085 #[case(Capacity(15.0), Capacity(10.0))]
1087 #[case(Capacity(5.0), Capacity(5.0))]
1089 fn calculate_investment_limits_for_candidates_with_constraints(
1090 region_id: RegionID,
1091 process_activity_limits_map: ProcessActivityLimitsMap,
1092 process_flows_map: ProcessFlowsMap,
1093 process_parameter_map: ProcessParameterMap,
1094 #[case] asset_capacity: Capacity,
1095 #[case] expected_limit: Capacity,
1096 ) {
1097 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1098
1099 let constraint = ProcessInvestmentConstraint {
1101 addition_limit: Some(Capacity(10.0)),
1102 };
1103 let mut constraints = ProcessInvestmentConstraintsMap::new();
1104 constraints.insert((region_id.clone(), 2015), Rc::new(constraint));
1105
1106 let process = Process {
1107 id: "constrained_process".into(),
1108 description: "Process with constraints".into(),
1109 years: 2010..=2020,
1110 activity_limits: process_activity_limits_map,
1111 flows: process_flows_map,
1112 parameters: process_parameter_map,
1113 regions: region_ids,
1114 primary_output: None,
1115 capacity_to_activity: ActivityPerCapacity(1.0),
1116 investment_constraints: constraints,
1117 unit_size: None,
1118 };
1119
1120 let process_rc = Rc::new(process);
1121
1122 let candidate_asset =
1123 Asset::new_candidate(process_rc, region_id, asset_capacity, 2015).unwrap();
1124
1125 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1126 let commodity_portion = Dimensionless(1.0);
1127
1128 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1129
1130 assert_eq!(result.len(), 1);
1132 let asset_ref = AssetRef::from(candidate_asset);
1133 assert_eq!(
1134 result[&asset_ref],
1135 AssetCapacity::Continuous(expected_limit)
1136 );
1137 }
1138
1139 #[rstest]
1140 fn calculate_investment_limits_for_candidates_multiple_assets(
1141 region_id: RegionID,
1142 process_activity_limits_map: ProcessActivityLimitsMap,
1143 process_flows_map: ProcessFlowsMap,
1144 process_parameter_map: ProcessParameterMap,
1145 ) {
1146 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1147
1148 let constraint1 = ProcessInvestmentConstraint {
1150 addition_limit: Some(Capacity(12.0)),
1151 };
1152 let mut constraints1 = ProcessInvestmentConstraintsMap::new();
1153 constraints1.insert((region_id.clone(), 2015), Rc::new(constraint1));
1154
1155 let process1 = Process {
1156 id: "process1".into(),
1157 description: "First process".into(),
1158 years: 2010..=2020,
1159 activity_limits: process_activity_limits_map.clone(),
1160 flows: process_flows_map.clone(),
1161 parameters: process_parameter_map.clone(),
1162 regions: region_ids.clone(),
1163 primary_output: None,
1164 capacity_to_activity: ActivityPerCapacity(1.0),
1165 investment_constraints: constraints1,
1166 unit_size: None,
1167 };
1168
1169 let process2 = Process {
1171 id: "process2".into(),
1172 description: "Second process".into(),
1173 years: 2010..=2020,
1174 activity_limits: process_activity_limits_map,
1175 flows: process_flows_map,
1176 parameters: process_parameter_map,
1177 regions: region_ids,
1178 primary_output: None,
1179 capacity_to_activity: ActivityPerCapacity(1.0),
1180 investment_constraints: process_investment_constraints(),
1181 unit_size: None,
1182 };
1183
1184 let process1_rc = Rc::new(process1);
1185 let process2_rc = Rc::new(process2);
1186
1187 let candidate1 =
1188 Asset::new_candidate(process1_rc, region_id.clone(), Capacity(20.0), 2015).unwrap();
1189
1190 let candidate2 = Asset::new_candidate(process2_rc, region_id, Capacity(8.0), 2015).unwrap();
1191
1192 let opt_assets = vec![
1193 AssetRef::from(candidate1.clone()),
1194 AssetRef::from(candidate2.clone()),
1195 ];
1196 let commodity_portion = Dimensionless(0.75);
1197
1198 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1199
1200 assert_eq!(result.len(), 2);
1202
1203 let asset1_ref = AssetRef::from(candidate1);
1205 assert_eq!(
1206 result[&asset1_ref],
1207 AssetCapacity::Continuous(Capacity(9.0))
1208 );
1209
1210 let asset2_ref = AssetRef::from(candidate2);
1212 assert_eq!(
1213 result[&asset2_ref],
1214 AssetCapacity::Continuous(Capacity(8.0))
1215 );
1216 }
1217
1218 #[rstest]
1219 fn calculate_investment_limits_for_candidates_discrete_capacity(
1220 region_id: RegionID,
1221 process_activity_limits_map: crate::process::ProcessActivityLimitsMap,
1222 process_flows_map: crate::process::ProcessFlowsMap,
1223 process_parameter_map: crate::process::ProcessParameterMap,
1224 ) {
1225 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1226
1227 let constraint = ProcessInvestmentConstraint {
1229 addition_limit: Some(Capacity(35.0)), };
1231 let mut constraints = ProcessInvestmentConstraintsMap::new();
1232 constraints.insert((region_id.clone(), 2015), Rc::new(constraint));
1233
1234 let process = Process {
1235 id: "discrete_process".into(),
1236 description: "Process with discrete units".into(),
1237 years: 2010..=2020,
1238 activity_limits: process_activity_limits_map,
1239 flows: process_flows_map,
1240 parameters: process_parameter_map,
1241 regions: region_ids,
1242 primary_output: None,
1243 capacity_to_activity: ActivityPerCapacity(1.0),
1244 investment_constraints: constraints,
1245 unit_size: Some(Capacity(10.0)), };
1247
1248 let process_rc = Rc::new(process);
1249 let capacity = Capacity(50.0); let candidate_asset = Asset::new_candidate(process_rc, region_id, capacity, 2015).unwrap();
1252
1253 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1254 let commodity_portion = Dimensionless(1.0);
1255
1256 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1257
1258 assert_eq!(result.len(), 1);
1261 let asset_ref = AssetRef::from(candidate_asset);
1262 assert_eq!(
1263 result[&asset_ref],
1264 AssetCapacity::Discrete(3, Capacity(10.0))
1265 );
1266 assert_eq!(result[&asset_ref].total_capacity(), Capacity(30.0));
1267 }
1268}