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 mut agent_share_cache = HashMap::new();
388 let capacity_limits = flexible_capacity_assets
389 .iter()
390 .filter_map(|asset| {
391 let agent_id = asset.agent_id().unwrap();
392 let commodity_id = asset.primary_output_commodity().unwrap();
393 let agent_share = *agent_share_cache
394 .entry((agent_id, commodity_id))
395 .or_insert_with(|| {
396 model.agents[agent_id].commodity_portions[&(commodity_id.clone(), year)]
397 });
398 asset
399 .max_installable_capacity(agent_share)
400 .map(|max_capacity| (asset.clone(), max_capacity))
401 })
402 .collect::<HashMap<_, _>>();
403
404 let solution = DispatchRun::new(model, &all_assets, year)
406 .with_market_balance_subset(&markets_to_balance)
407 .with_flexible_capacity_assets(
408 &flexible_capacity_assets,
409 Some(&capacity_limits),
410 model.parameters.capacity_margin,
412 )
413 .run(
414 &format!("cycle ({markets_str}) post {commodity_id}|{region_id} investment"),
415 writer,
416 )
417 .with_context(|| {
418 format!(
419 "Cycle balancing failed for cycle ({markets_str}), capacity_margin: {}. \
420 Try increasing the capacity_margin.",
421 model.parameters.capacity_margin
422 )
423 })?;
424
425 current_demand.clone_from(demand);
427 update_net_demand_map(
428 &mut current_demand,
429 &solution.create_flow_map(),
430 &assets_for_cycle_flat,
431 );
432 last_solution = Some(solution);
433 }
434
435 let mut all_cycle_assets: Vec<_> = assets_for_cycle.into_values().flatten().collect();
437 if let Some(solution) = last_solution {
438 let new_capacities: HashMap<_, _> = solution.iter_capacity().collect();
439 for asset in &mut all_cycle_assets {
440 if let Some(new_capacity) = new_capacities.get(asset) {
441 debug!(
442 "Capacity of asset '{}' modified during cycle balancing ({} to {})",
443 asset.process_id(),
444 asset.total_capacity(),
445 new_capacity.total_capacity()
446 );
447 asset.make_mut().set_capacity(*new_capacity);
448 }
449 }
450 }
451
452 Ok(all_cycle_assets)
453}
454
455fn flatten_preset_demands_for_year(
465 commodities: &CommodityMap,
466 time_slice_info: &TimeSliceInfo,
467 year: u32,
468) -> AllDemandMap {
469 let mut demand_map = AllDemandMap::new();
470 for (commodity_id, commodity) in commodities {
471 for ((region_id, data_year, time_slice_selection), demand) in &commodity.demand {
472 if *data_year != year {
473 continue;
474 }
475
476 #[allow(clippy::cast_precision_loss)]
480 let n_time_slices = time_slice_selection.iter(time_slice_info).count() as f64;
481 let demand_per_slice = *demand / Dimensionless(n_time_slices);
482 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
483 demand_map.insert(
484 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
485 demand_per_slice,
486 );
487 }
488 }
489 }
490 demand_map
491}
492
493fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
503 for ((asset, commodity_id, time_slice), flow) in flows {
504 if assets.contains(asset) {
505 let key = (
506 commodity_id.clone(),
507 asset.region_id().clone(),
508 time_slice.clone(),
509 );
510
511 if (flow < &Flow(0.0))
514 || asset
515 .primary_output()
516 .is_some_and(|p| &p.commodity.id == commodity_id)
517 {
518 demand
520 .entry(key)
521 .and_modify(|value| *value -= *flow)
522 .or_insert(-*flow);
523 }
524 }
525 }
526}
527
528fn get_demand_portion_for_market(
530 time_slice_info: &TimeSliceInfo,
531 demand: &AllDemandMap,
532 commodity_id: &CommodityID,
533 region_id: &RegionID,
534 commodity_portion: Dimensionless,
535) -> DemandMap {
536 time_slice_info
537 .iter_ids()
538 .map(|time_slice| {
539 (
540 time_slice.clone(),
541 commodity_portion
542 * *demand
543 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
544 .unwrap_or(&Flow(0.0)),
545 )
546 })
547 .collect()
548}
549
550fn get_responsible_agents<'a, I>(
553 agents: I,
554 commodity_id: &'a CommodityID,
555 region_id: &'a RegionID,
556 year: u32,
557) -> impl Iterator<Item = (&'a Agent, Dimensionless)>
558where
559 I: Iterator<Item = &'a Agent>,
560{
561 agents.filter_map(move |agent| {
562 if !agent.regions.contains(region_id) {
563 return None;
564 }
565 let portion = agent
566 .commodity_portions
567 .get(&(commodity_id.clone(), year))?;
568
569 Some((agent, *portion))
570 })
571}
572
573fn get_demand_limiting_capacity(
575 time_slice_info: &TimeSliceInfo,
576 asset: &Asset,
577 commodity: &Commodity,
578 demand: &DemandMap,
579) -> Capacity {
580 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
581 let mut capacity = Capacity(0.0);
582
583 for time_slice_selection in time_slice_info.iter_selections_at_level(commodity.time_slice_level)
584 {
585 let demand_for_selection: Flow = time_slice_selection
586 .iter(time_slice_info)
587 .map(|(time_slice, _)| demand[time_slice])
588 .sum();
589
590 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
594 let max_flow_per_cap =
595 *asset.get_activity_per_capacity_limits(time_slice).end() * coeff;
596 if max_flow_per_cap != FlowPerCapacity(0.0) {
597 capacity = capacity.max(demand_for_selection / max_flow_per_cap);
598 }
599 }
600 }
601
602 capacity
603}
604
605fn get_asset_options<'a>(
607 time_slice_info: &'a TimeSliceInfo,
608 all_existing_assets: &'a [AssetRef],
609 demand: &'a DemandMap,
610 agent: &'a Agent,
611 commodity: &'a Commodity,
612 region_id: &'a RegionID,
613 year: u32,
614) -> impl Iterator<Item = AssetRef> + 'a {
615 let existing_assets = all_existing_assets
617 .iter()
618 .filter_agent(&agent.id)
619 .filter_region(region_id)
620 .filter_primary_producers_of(&commodity.id)
621 .cloned();
622
623 let candidate_assets =
625 get_candidate_assets(time_slice_info, demand, agent, region_id, commodity, year);
626
627 chain(existing_assets, candidate_assets)
628}
629
630fn get_candidate_assets<'a>(
632 time_slice_info: &'a TimeSliceInfo,
633 demand: &'a DemandMap,
634 agent: &'a Agent,
635 region_id: &'a RegionID,
636 commodity: &'a Commodity,
637 year: u32,
638) -> impl Iterator<Item = AssetRef> + 'a {
639 agent
640 .iter_search_space(region_id, &commodity.id, year)
641 .map(move |process| {
642 let mut asset =
643 Asset::new_candidate(process.clone(), region_id.clone(), Capacity(0.0), year)
644 .unwrap();
645
646 let capacity = get_demand_limiting_capacity(time_slice_info, &asset, commodity, demand);
649 let asset_capacity = AssetCapacity::from_capacity(capacity, asset.unit_size());
650 asset.set_capacity(asset_capacity);
651
652 asset.into()
653 })
654}
655
656fn log_on_equal_appraisal_outputs(
658 outputs: &[AppraisalOutput],
659 agent_id: &AgentID,
660 commodity_id: &CommodityID,
661 region_id: &RegionID,
662) {
663 if outputs.is_empty() {
664 return;
665 }
666
667 let num_identical = count_equal_and_best_appraisal_outputs(outputs);
668
669 if num_identical > 0 {
670 let asset_details = outputs[..=num_identical]
671 .iter()
672 .map(|output| {
673 let asset = &output.asset;
674 format!(
675 "Process ID: '{}' (State: {}{}, Commission year: {})",
676 asset.process_id(),
677 asset.state(),
678 asset
679 .id()
680 .map(|id| format!(", Asset ID: {id}"))
681 .unwrap_or_default(),
682 asset.commission_year()
683 )
684 })
685 .join(", ");
686 debug!(
687 "Found equally good appraisals for Agent ID: {agent_id}, Commodity: '{commodity_id}', \
688 Region: {region_id}. Options: [{asset_details}]. Selecting first option.",
689 );
690 }
691}
692
693fn calculate_investment_limits_for_candidates(
700 opt_assets: &[AssetRef],
701 commodity_portion: Dimensionless,
702) -> HashMap<AssetRef, AssetCapacity> {
703 opt_assets
705 .iter()
706 .filter(|asset| !asset.is_commissioned())
707 .map(|asset| {
708 let mut cap = asset.capacity();
710
711 if let Some(limit_capacity) = asset.max_installable_capacity(commodity_portion) {
713 cap = cap.min(limit_capacity);
714 }
715
716 (asset.clone(), cap)
717 })
718 .collect()
719}
720
721#[allow(clippy::too_many_arguments)]
723fn select_best_assets(
724 model: &Model,
725 mut opt_assets: Vec<AssetRef>,
726 investment_limits: HashMap<AssetRef, AssetCapacity>,
727 commodity: &Commodity,
728 agent: &Agent,
729 region_id: &RegionID,
730 prices: &CommodityPrices,
731 mut demand: DemandMap,
732 year: u32,
733 writer: &mut DataWriter,
734) -> Result<Vec<AssetRef>> {
735 let objective_type = &agent.objectives[&year];
736 let mut remaining_candidate_capacity = investment_limits;
737
738 let coefficients =
740 calculate_coefficients_for_assets(model, objective_type, &opt_assets, prices, year);
741
742 let mut round = 0;
744 let mut best_assets: Vec<AssetRef> = Vec::new();
745 while is_any_remaining_demand(
746 &demand,
747 model.parameters.remaining_demand_absolute_tolerance,
748 ) {
749 ensure!(
750 !opt_assets.is_empty(),
751 "Failed to meet demand for commodity '{}' in region '{}' with provided investment \
752 options. This may be due to overly restrictive process investment constraints.",
753 &commodity.id,
754 region_id
755 );
756
757 let mut seen_groups = HashSet::new();
760
761 let mut outputs_for_opts = Vec::new();
763 for asset in &opt_assets {
764 if let Some(group_id) = asset.group_id()
766 && !seen_groups.insert(group_id)
767 {
768 continue;
769 }
770
771 let max_capacity = (!asset.is_commissioned()).then(|| {
777 let tranche_capacity = asset
778 .capacity()
779 .apply_limit_factor(model.parameters.capacity_limit_factor);
780 let dlc = AssetCapacity::from_capacity(
781 get_demand_limiting_capacity(&model.time_slice_info, asset, commodity, &demand),
782 asset.unit_size(),
783 );
784 let remaining_capacity = remaining_candidate_capacity[asset];
785
786 tranche_capacity.min(dlc).min(remaining_capacity)
787 });
788
789 let output = appraise_investment(
790 model,
791 asset,
792 max_capacity,
793 commodity,
794 objective_type,
795 &coefficients[asset],
796 &demand,
797 )?;
798 outputs_for_opts.push(output);
799 }
800
801 writer.write_appraisal_debug_info(
803 year,
804 &format!("{} {} round {}", &commodity.id, &agent.id, round),
805 &outputs_for_opts,
806 &demand,
807 )?;
808
809 sort_and_filter_appraisal_outputs(&mut outputs_for_opts);
811
812 if outputs_for_opts.is_empty() {
815 let remaining_demands: Vec<_> = demand
816 .iter()
817 .filter(|(_, flow)| **flow > Flow(0.0))
818 .map(|(time_slice, flow)| format!("{} : {:e}", time_slice, flow.value()))
819 .collect();
820
821 bail!(
822 "No feasible investment options left for \
823 commodity '{}', region '{}', year '{}', agent '{}' after appraisal.\n\
824 Remaining unmet demand (time_slice : flow):\n{}",
825 &commodity.id,
826 region_id,
827 year,
828 agent.id,
829 remaining_demands.join("\n")
830 );
831 }
832
833 log_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id);
835
836 let best_output = outputs_for_opts.into_iter().next().unwrap();
837
838 debug!(
840 "Selected {} asset '{}' (capacity: {})",
841 &best_output.asset.state(),
842 &best_output.asset.process_id(),
843 best_output.capacity.total_capacity()
844 );
845
846 update_assets(
848 best_output.asset,
849 best_output.capacity,
850 &mut opt_assets,
851 &mut remaining_candidate_capacity,
852 &mut best_assets,
853 );
854
855 demand = best_output.unmet_demand;
856 round += 1;
857 }
858
859 for asset in &mut best_assets {
862 if let AssetState::Candidate = asset.state() {
863 asset
864 .make_mut()
865 .select_candidate_for_investment(agent.id.clone());
866 }
867 }
868
869 Ok(best_assets)
870}
871
872fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: Flow) -> bool {
874 demand.values().any(|flow| *flow > absolute_tolerance)
875}
876
877fn update_assets(
879 mut best_asset: AssetRef,
880 capacity: AssetCapacity,
881 opt_assets: &mut Vec<AssetRef>,
882 remaining_candidate_capacity: &mut HashMap<AssetRef, AssetCapacity>,
883 best_assets: &mut Vec<AssetRef>,
884) {
885 match best_asset.state() {
886 AssetState::Commissioned { .. } => {
887 opt_assets.retain(|asset| *asset != best_asset);
889 best_assets.push(best_asset);
890 }
891 AssetState::Candidate => {
892 let remaining_capacity = remaining_candidate_capacity.get_mut(&best_asset).unwrap();
894 *remaining_capacity = *remaining_capacity - capacity;
895
896 if remaining_capacity.total_capacity() <= Capacity(0.0) {
898 let old_idx = opt_assets
899 .iter()
900 .position(|asset| *asset == best_asset)
901 .unwrap();
902 opt_assets.swap_remove(old_idx);
903 remaining_candidate_capacity.remove(&best_asset);
904 }
905
906 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset)
907 {
908 existing_asset.make_mut().increase_capacity(capacity);
910 } else {
911 best_asset.make_mut().set_capacity(capacity);
913 best_assets.push(best_asset);
914 }
915 }
916 _ => panic!("update_assets should only be called with Commissioned or Candidate assets"),
917 }
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923 use crate::commodity::Commodity;
924 use crate::fixture::{
925 agent_id, asset, process, process_activity_limits_map, process_flows_map,
926 process_investment_constraints, process_parameter_map, region_id, svd_commodity,
927 time_slice, time_slice_info, time_slice_info2,
928 };
929 use crate::process::{
930 ActivityLimits, FlowType, Process, ProcessActivityLimitsMap, ProcessFlow, ProcessFlowsMap,
931 ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessParameterMap,
932 };
933 use crate::region::RegionID;
934 use crate::time_slice::{TimeSliceID, TimeSliceInfo};
935 use crate::units::Dimensionless;
936 use crate::units::{ActivityPerCapacity, Capacity, Flow, FlowPerActivity, MoneyPerFlow};
937 use indexmap::{IndexSet, indexmap};
938 use rstest::rstest;
939 use std::rc::Rc;
940
941 #[rstest]
942 fn get_demand_limiting_capacity_works(
943 time_slice: TimeSliceID,
944 time_slice_info: TimeSliceInfo,
945 svd_commodity: Commodity,
946 mut process: Process,
947 ) {
948 let commodity_rc = Rc::new(svd_commodity);
950 let process_flow = ProcessFlow {
951 commodity: Rc::clone(&commodity_rc),
952 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
954 cost: MoneyPerFlow(0.0),
955 };
956 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
957 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
958 process.flows = process_flows_map;
959
960 let asset = asset(process);
962
963 let demand = indexmap! { time_slice.clone() => Flow(10.0)};
965
966 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
968
969 assert_eq!(result, Capacity(5.0));
973 }
974
975 #[rstest]
976 fn get_demand_limiting_capacity_multiple_time_slices(
977 time_slice_info2: TimeSliceInfo,
978 svd_commodity: Commodity,
979 mut process: Process,
980 ) {
981 let (time_slice1, time_slice2) =
982 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
983
984 let commodity_rc = Rc::new(svd_commodity);
986 let process_flow = ProcessFlow {
987 commodity: Rc::clone(&commodity_rc),
988 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
990 cost: MoneyPerFlow(0.0),
991 };
992 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
993 let process_flows_map = process_flows_map(process.regions.clone(), Rc::new(process_flows));
994 process.flows = process_flows_map;
995
996 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
998 limits.add_time_slice_limit(time_slice1.clone(), Dimensionless(0.0)..=Dimensionless(0.2));
999 limits.add_time_slice_limit(time_slice2.clone(), Dimensionless(0.0)..=Dimensionless(0.0));
1000 let limits_map = process_activity_limits_map(process.regions.clone(), limits);
1001 process.activity_limits = limits_map;
1002
1003 let asset = asset(process);
1005
1006 let demand = indexmap! {
1008 time_slice1.clone() => Flow(4.0), time_slice2.clone() => Flow(3.0), };
1011
1012 let result =
1014 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
1015
1016 assert_eq!(result, Capacity(20.0));
1021 }
1022
1023 #[rstest]
1024 fn calculate_investment_limits_for_candidates_empty_list() {
1025 let opt_assets: Vec<AssetRef> = vec![];
1027 let commodity_portion = Dimensionless(1.0);
1028
1029 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1030
1031 assert!(result.is_empty());
1032 }
1033
1034 #[rstest]
1035 fn calculate_investment_limits_for_candidates_commissioned_assets_filtered(
1036 process: Process,
1037 region_id: RegionID,
1038 agent_id: AgentID,
1039 ) {
1040 let process_rc = Rc::new(process);
1042 let capacity = Capacity(10.0);
1043
1044 let commissioned_asset = Asset::new_commissioned(
1046 agent_id.clone(),
1047 process_rc.clone(),
1048 region_id.clone(),
1049 capacity,
1050 2015,
1051 )
1052 .unwrap();
1053
1054 let candidate_asset =
1056 Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2015).unwrap();
1057
1058 let candidate_asset_ref = AssetRef::from(candidate_asset);
1059 let opt_assets = vec![
1060 AssetRef::from(commissioned_asset),
1061 candidate_asset_ref.clone(),
1062 ];
1063 let commodity_portion = Dimensionless(1.0);
1064
1065 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1066
1067 assert_eq!(result.len(), 1);
1069 assert!(result.contains_key(&candidate_asset_ref));
1070 }
1071
1072 #[rstest]
1073 fn calculate_investment_limits_for_candidates_no_investment_constraints(
1074 process: Process,
1075 region_id: RegionID,
1076 ) {
1077 let process_rc = Rc::new(process);
1079 let capacity = Capacity(15.0);
1080
1081 let candidate_asset = Asset::new_candidate(process_rc, region_id, capacity, 2015).unwrap();
1082
1083 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1084 let commodity_portion = Dimensionless(0.8);
1085
1086 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1087
1088 assert_eq!(result.len(), 1);
1090 let asset_ref = AssetRef::from(candidate_asset);
1091 assert_eq!(result[&asset_ref], AssetCapacity::Continuous(capacity));
1092 }
1093
1094 #[rstest]
1095 #[case(Capacity(15.0), Capacity(10.0))]
1097 #[case(Capacity(5.0), Capacity(5.0))]
1099 fn calculate_investment_limits_for_candidates_with_constraints(
1100 region_id: RegionID,
1101 process_activity_limits_map: ProcessActivityLimitsMap,
1102 process_flows_map: ProcessFlowsMap,
1103 process_parameter_map: ProcessParameterMap,
1104 #[case] asset_capacity: Capacity,
1105 #[case] expected_limit: Capacity,
1106 ) {
1107 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1108
1109 let constraint = ProcessInvestmentConstraint {
1111 addition_limit: Some(Capacity(10.0)),
1112 };
1113 let mut constraints = ProcessInvestmentConstraintsMap::new();
1114 constraints.insert((region_id.clone(), 2015), Rc::new(constraint));
1115
1116 let process = Process {
1117 id: "constrained_process".into(),
1118 description: "Process with constraints".into(),
1119 years: 2010..=2020,
1120 activity_limits: process_activity_limits_map,
1121 flows: process_flows_map,
1122 parameters: process_parameter_map,
1123 regions: region_ids,
1124 primary_output: None,
1125 capacity_to_activity: ActivityPerCapacity(1.0),
1126 investment_constraints: constraints,
1127 unit_size: None,
1128 };
1129
1130 let process_rc = Rc::new(process);
1131
1132 let candidate_asset =
1133 Asset::new_candidate(process_rc, region_id, asset_capacity, 2015).unwrap();
1134
1135 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1136 let commodity_portion = Dimensionless(1.0);
1137
1138 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1139
1140 assert_eq!(result.len(), 1);
1142 let asset_ref = AssetRef::from(candidate_asset);
1143 assert_eq!(
1144 result[&asset_ref],
1145 AssetCapacity::Continuous(expected_limit)
1146 );
1147 }
1148
1149 #[rstest]
1150 fn calculate_investment_limits_for_candidates_multiple_assets(
1151 region_id: RegionID,
1152 process_activity_limits_map: ProcessActivityLimitsMap,
1153 process_flows_map: ProcessFlowsMap,
1154 process_parameter_map: ProcessParameterMap,
1155 ) {
1156 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1157
1158 let constraint1 = ProcessInvestmentConstraint {
1160 addition_limit: Some(Capacity(12.0)),
1161 };
1162 let mut constraints1 = ProcessInvestmentConstraintsMap::new();
1163 constraints1.insert((region_id.clone(), 2015), Rc::new(constraint1));
1164
1165 let process1 = Process {
1166 id: "process1".into(),
1167 description: "First process".into(),
1168 years: 2010..=2020,
1169 activity_limits: process_activity_limits_map.clone(),
1170 flows: process_flows_map.clone(),
1171 parameters: process_parameter_map.clone(),
1172 regions: region_ids.clone(),
1173 primary_output: None,
1174 capacity_to_activity: ActivityPerCapacity(1.0),
1175 investment_constraints: constraints1,
1176 unit_size: None,
1177 };
1178
1179 let process2 = Process {
1181 id: "process2".into(),
1182 description: "Second process".into(),
1183 years: 2010..=2020,
1184 activity_limits: process_activity_limits_map,
1185 flows: process_flows_map,
1186 parameters: process_parameter_map,
1187 regions: region_ids,
1188 primary_output: None,
1189 capacity_to_activity: ActivityPerCapacity(1.0),
1190 investment_constraints: process_investment_constraints(),
1191 unit_size: None,
1192 };
1193
1194 let process1_rc = Rc::new(process1);
1195 let process2_rc = Rc::new(process2);
1196
1197 let candidate1 =
1198 Asset::new_candidate(process1_rc, region_id.clone(), Capacity(20.0), 2015).unwrap();
1199
1200 let candidate2 = Asset::new_candidate(process2_rc, region_id, Capacity(8.0), 2015).unwrap();
1201
1202 let opt_assets = vec![
1203 AssetRef::from(candidate1.clone()),
1204 AssetRef::from(candidate2.clone()),
1205 ];
1206 let commodity_portion = Dimensionless(0.75);
1207
1208 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1209
1210 assert_eq!(result.len(), 2);
1212
1213 let asset1_ref = AssetRef::from(candidate1);
1215 assert_eq!(
1216 result[&asset1_ref],
1217 AssetCapacity::Continuous(Capacity(9.0))
1218 );
1219
1220 let asset2_ref = AssetRef::from(candidate2);
1222 assert_eq!(
1223 result[&asset2_ref],
1224 AssetCapacity::Continuous(Capacity(8.0))
1225 );
1226 }
1227
1228 #[rstest]
1229 fn calculate_investment_limits_for_candidates_discrete_capacity(
1230 region_id: RegionID,
1231 process_activity_limits_map: crate::process::ProcessActivityLimitsMap,
1232 process_flows_map: crate::process::ProcessFlowsMap,
1233 process_parameter_map: crate::process::ProcessParameterMap,
1234 ) {
1235 let region_ids: IndexSet<RegionID> = [region_id.clone()].into();
1236
1237 let constraint = ProcessInvestmentConstraint {
1239 addition_limit: Some(Capacity(35.0)), };
1241 let mut constraints = ProcessInvestmentConstraintsMap::new();
1242 constraints.insert((region_id.clone(), 2015), Rc::new(constraint));
1243
1244 let process = Process {
1245 id: "discrete_process".into(),
1246 description: "Process with discrete units".into(),
1247 years: 2010..=2020,
1248 activity_limits: process_activity_limits_map,
1249 flows: process_flows_map,
1250 parameters: process_parameter_map,
1251 regions: region_ids,
1252 primary_output: None,
1253 capacity_to_activity: ActivityPerCapacity(1.0),
1254 investment_constraints: constraints,
1255 unit_size: Some(Capacity(10.0)), };
1257
1258 let process_rc = Rc::new(process);
1259 let capacity = Capacity(50.0); let candidate_asset = Asset::new_candidate(process_rc, region_id, capacity, 2015).unwrap();
1262
1263 let opt_assets = vec![AssetRef::from(candidate_asset.clone())];
1264 let commodity_portion = Dimensionless(1.0);
1265
1266 let result = calculate_investment_limits_for_candidates(&opt_assets, commodity_portion);
1267
1268 assert_eq!(result.len(), 1);
1271 let asset_ref = AssetRef::from(candidate_asset);
1272 assert_eq!(
1273 result[&asset_ref],
1274 AssetCapacity::Discrete(3, Capacity(10.0))
1275 );
1276 assert_eq!(result[&asset_ref].total_capacity(), Capacity(30.0));
1277 }
1278}