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_appraisal_outputs_by_investment_priority,
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_appraisal_outputs_by_investment_priority(&mut outputs_for_opts);
800
801 if outputs_for_opts.is_empty() {
810 let remaining_demands: Vec<_> = demand
811 .iter()
812 .filter(|(_, flow)| **flow > Flow(0.0))
813 .map(|(time_slice, flow)| format!("{} : {:e}", time_slice, flow.value()))
814 .collect();
815
816 bail!(
817 "No feasible investment options left for \
818 commodity '{}', region '{}', year '{}', agent '{}' after appraisal.\n\
819 Remaining unmet demand (time_slice : flow):\n{}",
820 &commodity.id,
821 region_id,
822 year,
823 agent.id,
824 remaining_demands.join("\n")
825 );
826 }
827
828 log_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id);
830
831 let best_output = outputs_for_opts.into_iter().next().unwrap();
832
833 debug!(
835 "Selected {} asset '{}' (capacity: {})",
836 &best_output.asset.state(),
837 &best_output.asset.process_id(),
838 best_output.capacity.total_capacity()
839 );
840
841 update_assets(
843 best_output.asset,
844 best_output.capacity,
845 &mut opt_assets,
846 &mut remaining_candidate_capacity,
847 &mut best_assets,
848 );
849
850 demand = best_output.unmet_demand;
851 round += 1;
852 }
853
854 for asset in &mut best_assets {
857 if let AssetState::Candidate = asset.state() {
858 asset
859 .make_mut()
860 .select_candidate_for_investment(agent.id.clone());
861 }
862 }
863
864 Ok(best_assets)
865}
866
867fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: Flow) -> bool {
869 demand.values().any(|flow| *flow > absolute_tolerance)
870}
871
872fn update_assets(
874 mut best_asset: AssetRef,
875 capacity: AssetCapacity,
876 opt_assets: &mut Vec<AssetRef>,
877 remaining_candidate_capacity: &mut HashMap<AssetRef, AssetCapacity>,
878 best_assets: &mut Vec<AssetRef>,
879) {
880 match best_asset.state() {
881 AssetState::Commissioned { .. } => {
882 opt_assets.retain(|asset| *asset != best_asset);
884 best_assets.push(best_asset);
885 }
886 AssetState::Candidate => {
887 let remaining_capacity = remaining_candidate_capacity.get_mut(&best_asset).unwrap();
889 *remaining_capacity = *remaining_capacity - capacity;
890
891 if remaining_capacity.total_capacity() <= Capacity(0.0) {
893 let old_idx = opt_assets
894 .iter()
895 .position(|asset| *asset == best_asset)
896 .unwrap();
897 opt_assets.swap_remove(old_idx);
898 remaining_candidate_capacity.remove(&best_asset);
899 }
900
901 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset)
902 {
903 existing_asset.make_mut().increase_capacity(capacity);
905 } else {
906 best_asset.make_mut().set_capacity(capacity);
908 best_assets.push(best_asset);
909 }
910 }
911 _ => panic!("update_assets should only be called with Commissioned or Candidate assets"),
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use super::*;
918 use crate::commodity::Commodity;
919 use crate::fixture::{
920 agent_id, asset, process, process_activity_limits_map, process_flows_map,
921 process_investment_constraints, process_parameter_map, region_id, svd_commodity,
922 time_slice, time_slice_info, time_slice_info2,
923 };
924 use crate::process::{
925 ActivityLimits, FlowType, Process, ProcessActivityLimitsMap, ProcessFlow, ProcessFlowsMap,
926 ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessParameterMap,
927 };
928 use crate::region::RegionID;
929 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
930 use crate::units::Dimensionless;
931 use crate::units::{ActivityPerCapacity, Capacity, Flow, FlowPerActivity, MoneyPerFlow};
932 use indexmap::{IndexSet, indexmap};
933 use rstest::rstest;
934 use std::rc::Rc;
935
936 #[rstest]
937 fn get_demand_limiting_capacity_works(
938 time_slice: TimeSliceID,
939 time_slice_info: TimeSliceInfo,
940 svd_commodity: Commodity,
941 mut process: Process,
942 ) {
943 let commodity_rc = Rc::new(svd_commodity);
945 let process_flow = ProcessFlow {
946 commodity: Rc::clone(&commodity_rc),
947 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
949 cost: MoneyPerFlow(0.0),
950 };
951 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
952 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
953 process.flows = process_flows_map;
954
955 let asset = asset(process);
957
958 let demand = indexmap! { time_slice.clone() => Flow(10.0)};
960
961 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
963
964 assert_eq!(result, Capacity(5.0));
968 }
969
970 #[rstest]
971 fn get_demand_limiting_capacity_multiple_time_slices(
972 time_slice_info2: TimeSliceInfo,
973 svd_commodity: Commodity,
974 mut process: Process,
975 ) {
976 let (time_slice1, time_slice2) =
977 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
978
979 let commodity_rc = Rc::new(svd_commodity);
981 let process_flow = ProcessFlow {
982 commodity: Rc::clone(&commodity_rc),
983 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
985 cost: MoneyPerFlow(0.0),
986 };
987 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
988 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
989 process.flows = process_flows_map;
990
991 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
993 limits.add_time_slice_limit(time_slice1.clone(), Dimensionless(0.0)..=Dimensionless(0.2));
994 limits.add_time_slice_limit(time_slice2.clone(), Dimensionless(0.0)..=Dimensionless(0.0));
995 let limits_map = process_activity_limits_map(process.regions.clone(), limits);
996 process.activity_limits = limits_map;
997
998 let asset = asset(process);
1000
1001 let demand = indexmap! {
1003 time_slice1.clone() => Flow(4.0), time_slice2.clone() => Flow(3.0), };
1006
1007 let result =
1009 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
1010
1011 assert_eq!(result, Capacity(20.0));
1016 }
1017
1018 #[rstest]
1019 fn calculate_investment_limits_for_candidates_empty_list() {
1020 let opt_assets: Vec<AssetRef> = vec![];
1022 let commodity_portion = Dimensionless(1.0);
1023
1024 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1025
1026 assert!(result.is_empty());
1027 }
1028
1029 #[rstest]
1030 fn calculate_investment_limits_for_candidates_commissioned_assets_filtered(
1031 process: Process,
1032 region_id: RegionID,
1033 agent_id: AgentID,
1034 ) {
1035 let process_rc = Rc::new(process);
1037 let capacity = Capacity(10.0);
1038
1039 let commissioned_asset = Asset::new_commissioned(
1041 agent_id.clone(),
1042 process_rc.clone(),
1043 region_id.clone(),
1044 capacity,
1045 2015,
1046 )
1047 .unwrap();
1048
1049 let candidate_asset =
1051 Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2015).unwrap();
1052
1053 let candidate_asset_ref = AssetRef::from(candidate_asset);
1054 let opt_assets = vec![
1055 AssetRef::from(commissioned_asset),
1056 candidate_asset_ref.clone(),
1057 ];
1058 let commodity_portion = Dimensionless(1.0);
1059
1060 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1061
1062 assert_eq!(result.len(), 1);
1064 assert!(result.contains_key(&candidate_asset_ref));
1065 }
1066
1067 #[rstest]
1068 fn calculate_investment_limits_for_candidates_no_investment_constraints(
1069 process: Process,
1070 region_id: RegionID,
1071 ) {
1072 let process_rc = Rc::new(process);
1074 let capacity = Capacity(15.0);
1075
1076 let candidate_asset = Asset::new_candidate(process_rc, region_id, capacity, 2015).unwrap();
1077
1078 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1079 let commodity_portion = Dimensionless(0.8);
1080
1081 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1082
1083 assert_eq!(result.len(), 1);
1085 let asset_ref = AssetRef::from(candidate_asset);
1086 assert_eq!(result[&asset_ref], AssetCapacity::Continuous(capacity));
1087 }
1088
1089 #[rstest]
1090 #[case(Capacity(15.0), Capacity(10.0))]
1092 #[case(Capacity(5.0), Capacity(5.0))]
1094 fn calculate_investment_limits_for_candidates_with_constraints(
1095 region_id: RegionID,
1096 process_activity_limits_map: ProcessActivityLimitsMap,
1097 process_flows_map: ProcessFlowsMap,
1098 process_parameter_map: ProcessParameterMap,
1099 #[case] asset_capacity: Capacity,
1100 #[case] expected_limit: Capacity,
1101 ) {
1102 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1103
1104 let constraint = ProcessInvestmentConstraint {
1106 addition_limit: Some(Capacity(10.0)),
1107 };
1108 let mut constraints = ProcessInvestmentConstraintsMap::new();
1109 constraints.insert((region_id.clone(), 2015), Rc::new(constraint));
1110
1111 let process = Process {
1112 id: "constrained_process".into(),
1113 description: "Process with constraints".into(),
1114 years: 2010..=2020,
1115 activity_limits: process_activity_limits_map,
1116 flows: process_flows_map,
1117 parameters: process_parameter_map,
1118 regions: region_ids,
1119 primary_output: None,
1120 capacity_to_activity: ActivityPerCapacity(1.0),
1121 investment_constraints: constraints,
1122 unit_size: None,
1123 };
1124
1125 let process_rc = Rc::new(process);
1126
1127 let candidate_asset =
1128 Asset::new_candidate(process_rc, region_id, asset_capacity, 2015).unwrap();
1129
1130 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1131 let commodity_portion = Dimensionless(1.0);
1132
1133 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1134
1135 assert_eq!(result.len(), 1);
1137 let asset_ref = AssetRef::from(candidate_asset);
1138 assert_eq!(
1139 result[&asset_ref],
1140 AssetCapacity::Continuous(expected_limit)
1141 );
1142 }
1143
1144 #[rstest]
1145 fn calculate_investment_limits_for_candidates_multiple_assets(
1146 region_id: RegionID,
1147 process_activity_limits_map: ProcessActivityLimitsMap,
1148 process_flows_map: ProcessFlowsMap,
1149 process_parameter_map: ProcessParameterMap,
1150 ) {
1151 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1152
1153 let constraint1 = ProcessInvestmentConstraint {
1155 addition_limit: Some(Capacity(12.0)),
1156 };
1157 let mut constraints1 = ProcessInvestmentConstraintsMap::new();
1158 constraints1.insert((region_id.clone(), 2015), Rc::new(constraint1));
1159
1160 let process1 = Process {
1161 id: "process1".into(),
1162 description: "First process".into(),
1163 years: 2010..=2020,
1164 activity_limits: process_activity_limits_map.clone(),
1165 flows: process_flows_map.clone(),
1166 parameters: process_parameter_map.clone(),
1167 regions: region_ids.clone(),
1168 primary_output: None,
1169 capacity_to_activity: ActivityPerCapacity(1.0),
1170 investment_constraints: constraints1,
1171 unit_size: None,
1172 };
1173
1174 let process2 = Process {
1176 id: "process2".into(),
1177 description: "Second process".into(),
1178 years: 2010..=2020,
1179 activity_limits: process_activity_limits_map,
1180 flows: process_flows_map,
1181 parameters: process_parameter_map,
1182 regions: region_ids,
1183 primary_output: None,
1184 capacity_to_activity: ActivityPerCapacity(1.0),
1185 investment_constraints: process_investment_constraints(),
1186 unit_size: None,
1187 };
1188
1189 let process1_rc = Rc::new(process1);
1190 let process2_rc = Rc::new(process2);
1191
1192 let candidate1 =
1193 Asset::new_candidate(process1_rc, region_id.clone(), Capacity(20.0), 2015).unwrap();
1194
1195 let candidate2 = Asset::new_candidate(process2_rc, region_id, Capacity(8.0), 2015).unwrap();
1196
1197 let opt_assets = vec![
1198 AssetRef::from(candidate1.clone()),
1199 AssetRef::from(candidate2.clone()),
1200 ];
1201 let commodity_portion = Dimensionless(0.75);
1202
1203 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1204
1205 assert_eq!(result.len(), 2);
1207
1208 let asset1_ref = AssetRef::from(candidate1);
1210 assert_eq!(
1211 result[&asset1_ref],
1212 AssetCapacity::Continuous(Capacity(9.0))
1213 );
1214
1215 let asset2_ref = AssetRef::from(candidate2);
1217 assert_eq!(
1218 result[&asset2_ref],
1219 AssetCapacity::Continuous(Capacity(8.0))
1220 );
1221 }
1222
1223 #[rstest]
1224 fn calculate_investment_limits_for_candidates_discrete_capacity(
1225 region_id: RegionID,
1226 process_activity_limits_map: crate::process::ProcessActivityLimitsMap,
1227 process_flows_map: crate::process::ProcessFlowsMap,
1228 process_parameter_map: crate::process::ProcessParameterMap,
1229 ) {
1230 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1231
1232 let constraint = ProcessInvestmentConstraint {
1234 addition_limit: Some(Capacity(35.0)), };
1236 let mut constraints = ProcessInvestmentConstraintsMap::new();
1237 constraints.insert((region_id.clone(), 2015), Rc::new(constraint));
1238
1239 let process = Process {
1240 id: "discrete_process".into(),
1241 description: "Process with discrete units".into(),
1242 years: 2010..=2020,
1243 activity_limits: process_activity_limits_map,
1244 flows: process_flows_map,
1245 parameters: process_parameter_map,
1246 regions: region_ids,
1247 primary_output: None,
1248 capacity_to_activity: ActivityPerCapacity(1.0),
1249 investment_constraints: constraints,
1250 unit_size: Some(Capacity(10.0)), };
1252
1253 let process_rc = Rc::new(process);
1254 let capacity = Capacity(50.0); let candidate_asset = Asset::new_candidate(process_rc, region_id, capacity, 2015).unwrap();
1257
1258 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1259 let commodity_portion = Dimensionless(1.0);
1260
1261 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1262
1263 assert_eq!(result.len(), 1);
1266 let asset_ref = AssetRef::from(candidate_asset);
1267 assert_eq!(
1268 result[&asset_ref],
1269 AssetCapacity::Discrete(3, Capacity(10.0))
1270 );
1271 assert_eq!(result[&asset_ref].total_capacity(), Capacity(30.0));
1272 }
1273}