1use super::optimisation::{DispatchRun, FlowMap};
3use crate::agent::{Agent, AgentID};
4use crate::asset::{Asset, AssetRef};
5use crate::commodity::{Commodity, CommodityID, CommodityMap};
6use crate::model::Model;
7use crate::output::DataWriter;
8use crate::process::ProcessID;
9use crate::region::RegionID;
10use crate::simulation::prices::Prices;
11use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
12use crate::timeit::InvestmentTimer;
13use crate::units::{ActivityPerCapacity, Capacity, Dimensionless, Flow, FlowPerCapacity};
14use anyhow::{Result, ensure};
15use context_manager;
16use indexmap::IndexMap;
17use itertools::Itertools;
18use log::{debug, warn};
19use rayon::prelude::*;
20use std::collections::HashMap;
21use strum::IntoEnumIterator;
22
23pub mod appraisal;
24use appraisal::coefficients::calculate_coefficients_for_assets;
25use appraisal::{
26 AppraisalOutput, appraise_investment, count_equal_and_best_appraisal_outputs,
27 sort_and_filter_appraisal_outputs,
28};
29
30pub type DemandMap = IndexMap<TimeSliceID, Flow>;
32
33pub type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceID), Flow>;
35
36#[context_manager::wrap(InvestmentTimer)]
51pub fn perform_agent_investment(
52 model: &Model,
53 year: u32,
54 existing_assets: &[AssetRef],
55 prices: &Prices,
56 writer: &mut DataWriter,
57) -> Result<Vec<AssetRef>> {
58 let mut net_demand =
60 flatten_preset_demands_for_year(&model.commodities, &model.time_slice_info, year);
61
62 let mut all_selected_assets = Vec::new();
65
66 let investment_order = &model.investment_order[&year];
67 debug!(
68 "Investment order for year '{year}': {}",
69 investment_order.iter().join(" -> ")
70 );
71
72 let mut seen_markets = Vec::new();
76
77 for market_set in investment_order {
79 let selected_assets = market_set.select_assets(
81 model,
82 year,
83 &net_demand,
84 existing_assets,
85 prices,
86 &seen_markets,
87 &all_selected_assets,
88 writer,
89 )?;
90
91 for market in market_set.iter_markets() {
93 seen_markets.push(market.clone());
94 }
95
96 if selected_assets.is_empty() {
100 debug!("No assets selected for '{market_set}'");
101 continue;
102 }
103
104 all_selected_assets.extend(selected_assets.iter().cloned());
106
107 debug!("Running post-investment dispatch for '{market_set}'");
111
112 let solution = DispatchRun::new(model, &all_selected_assets, year)
115 .without_commodity_constraints()
116 .with_market_balance_subset(&seen_markets)
117 .with_input_prices(&prices.shadow)
118 .run(&format!("post {market_set} investment"), writer)?;
119
120 update_net_demand_map(
122 &mut net_demand,
123 &solution.create_flow_map(),
124 &selected_assets,
125 );
126 }
127
128 Ok(all_selected_assets)
129}
130
131pub fn flatten_preset_demands_for_year(
141 commodities: &CommodityMap,
142 time_slice_info: &TimeSliceInfo,
143 year: u32,
144) -> AllDemandMap {
145 let mut demand_map = AllDemandMap::new();
146 for (commodity_id, commodity) in commodities {
147 for ((region_id, data_year, time_slice_selection), demand) in &commodity.demand {
148 if *data_year != year {
149 continue;
150 }
151
152 #[allow(clippy::cast_precision_loss)]
156 let n_time_slices = time_slice_selection.iter(time_slice_info).count() as f64;
157 let demand_per_slice = *demand / Dimensionless(n_time_slices);
158 for (time_slice, _) in time_slice_selection.iter(time_slice_info) {
159 demand_map.insert(
160 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
161 demand_per_slice,
162 );
163 }
164 }
165 }
166 demand_map
167}
168
169pub fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
179 for ((asset, commodity_id, time_slice), flow) in flows {
180 if assets.contains(asset) {
181 let key = (
182 commodity_id.clone(),
183 asset.region_id().clone(),
184 time_slice.clone(),
185 );
186
187 if (flow < &Flow(0.0))
190 || asset
191 .primary_output()
192 .is_some_and(|p| &p.commodity.id == commodity_id)
193 {
194 demand
196 .entry(key)
197 .and_modify(|value| *value -= *flow)
198 .or_insert(-*flow);
199 }
200 }
201 }
202}
203
204pub fn calculate_candidate_asset_capacity_scale(
216 asset: &Asset,
217 commodity: &Commodity,
218 demand: &DemandMap,
219) -> Capacity {
220 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
221 let max_annual_supply_per_capacity = *asset
222 .get_activity_per_capacity_limits_for_selection(&TimeSliceSelection::Annual)
223 .end()
224 * coeff;
225 if max_annual_supply_per_capacity < FlowPerCapacity::EPSILON {
226 return Capacity(0.0);
227 }
228 let annual_demand = demand.values().copied().sum::<Flow>();
229 annual_demand / max_annual_supply_per_capacity
230}
231
232pub fn get_demand_limiting_capacity(
246 time_slice_info: &TimeSliceInfo,
247 asset: &Asset,
248 commodity: &Commodity,
249 demand: &DemandMap,
250) -> Capacity {
251 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
252 let mut capacity = Capacity(0.0);
253 let mut demand_cache: HashMap<_, Flow> = HashMap::new();
254
255 for level in TimeSliceLevel::iter() {
257 for selection in time_slice_info.iter_selections_at_level(level) {
258 let max_supply_for_selection = *asset
260 .get_activity_per_capacity_limits_for_selection(&selection)
261 .end()
262 * coeff;
263
264 if max_supply_for_selection == FlowPerCapacity(0.0) {
267 continue;
268 }
269
270 let demand_selection_level = level.max(commodity.time_slice_level);
282 let demand_selection = selection
283 .containing_selection_at_level(demand_selection_level)
284 .unwrap();
285 let serviceable_demand_for_selection = *demand_cache
286 .entry(demand_selection.clone())
287 .or_insert_with(|| {
288 demand_selection
289 .iter_at_level(time_slice_info, commodity.time_slice_level)
290 .unwrap()
291 .filter(|(bucket, _)| {
292 bucket.iter(time_slice_info).any(|(ts, _)| {
293 *asset.get_activity_per_capacity_limits(ts).end()
294 > ActivityPerCapacity(0.0)
295 })
296 })
297 .map(|(bucket, _)| {
298 bucket
299 .iter(time_slice_info)
300 .map(|(ts, _)| demand[ts])
301 .sum::<Flow>()
302 })
303 .sum()
304 });
305
306 capacity = capacity.max(serviceable_demand_for_selection / max_supply_for_selection);
309 }
310 }
311
312 capacity
313}
314
315fn log_on_equal_appraisal_outputs(
317 outputs: &[AppraisalOutput],
318 agent_id: &AgentID,
319 commodity_id: &CommodityID,
320 region_id: &RegionID,
321) {
322 if outputs.is_empty() {
323 return;
324 }
325
326 let num_identical = count_equal_and_best_appraisal_outputs(outputs);
327
328 if num_identical > 0 {
329 let asset_details = outputs[..=num_identical]
330 .iter()
331 .map(|output| {
332 let asset = &output.asset;
333 format!(
334 "Process ID: '{}' (State: {}{}, Commission year: {})",
335 asset.process_id(),
336 asset.state(),
337 asset
338 .id()
339 .map(|id| format!(", Asset ID: {id}"))
340 .unwrap_or_default(),
341 asset.commission_year()
342 )
343 })
344 .join(", ");
345 debug!(
346 "Found equally good appraisals for Agent ID: {agent_id}, Commodity: '{commodity_id}', \
347 Region: {region_id}. Options: [{asset_details}]. Selecting first option.",
348 );
349 }
350}
351
352#[allow(clippy::too_many_arguments)]
354pub fn select_best_assets(
355 model: &Model,
356 mut opt_assets: Vec<AssetRef>,
357 agent_addition_limits: HashMap<ProcessID, Capacity>,
358 agent_total_limits: HashMap<ProcessID, Capacity>,
359 commodity: &Commodity,
360 agent: &Agent,
361 region_id: &RegionID,
362 prices: &Prices,
363 mut demand: DemandMap,
364 year: u32,
365 writer: &mut DataWriter,
366) -> Result<Vec<AssetRef>> {
367 let objective_type = &agent.objectives[&year];
368
369 let mut remaining_agent_addition_limits = agent_addition_limits;
372
373 let mut remaining_agent_total_limits = agent_total_limits;
376
377 let mut available_retention_tranches =
379 prepare_commissioned_assets_for_retention(&mut opt_assets);
380
381 let coefficients =
383 calculate_coefficients_for_assets(model, objective_type, &opt_assets, prices, year);
384
385 let mut round = 0;
387 let mut best_assets: Vec<AssetRef> = Vec::new();
388 while is_any_remaining_demand(
389 &demand,
390 model.parameters.remaining_demand_absolute_tolerance,
391 ) {
392 remove_assets_exceeding_agent_limits(
395 &mut opt_assets,
396 &remaining_agent_addition_limits,
397 true,
398 );
399 remove_assets_exceeding_agent_limits(&mut opt_assets, &remaining_agent_total_limits, false);
400 ensure!(
401 !opt_assets.is_empty(),
402 "Failed to meet demand for commodity '{}' in region '{}' with provided investment \
403 options. This may be due to overly restrictive process investment constraints.",
404 commodity.id,
405 region_id
406 );
407 let mut outputs: Vec<AppraisalOutput> = opt_assets
412 .par_iter()
413 .map(|asset| -> Result<Option<AppraisalOutput>> {
414 if asset.total_capacity() <= Capacity(0.0) {
416 return Ok(None);
417 }
418
419 Ok(Some(appraise_investment(
420 model,
421 asset,
422 commodity,
423 objective_type,
424 &coefficients[asset],
425 &demand,
426 )?))
427 })
428 .collect::<Result<Vec<_>>>()? .into_iter()
430 .flatten()
431 .collect();
432
433 writer.write_appraisal_debug_info(
435 year,
436 &format!("{} {} round {}", commodity.id, agent.id, round),
437 &outputs,
438 &demand,
439 )?;
440
441 let num_nonfeasible = sort_and_filter_appraisal_outputs(&mut outputs);
443
444 if outputs.is_empty() {
448 warn!(
449 "Investment appraisal completed with unmet demand for commodity '{}', region '{}', \
450 year '{}', agent '{}'. {} non-feasible investments were not considered. \
451 This unmet demand may still be satisfied during the full system dispatch.",
452 commodity.id, region_id, year, agent.id, num_nonfeasible
453 );
454 break;
455 }
456
457 log_on_equal_appraisal_outputs(&outputs, &agent.id, &commodity.id, region_id);
459
460 let best_output = outputs.into_iter().next().unwrap();
461
462 debug!(
464 "Selected {} asset '{}' (capacity: {})",
465 best_output.asset.state(),
466 best_output.asset.process_id(),
467 best_output.asset.total_capacity()
468 );
469
470 update_selection_state(
472 &best_output.asset,
473 &mut opt_assets,
474 &mut remaining_agent_addition_limits,
475 &mut remaining_agent_total_limits,
476 &mut available_retention_tranches,
477 );
478
479 record_asset_selection(best_output.asset, &mut best_assets);
481
482 demand = best_output.unmet_demand;
483 round += 1;
484 }
485
486 for asset in &mut best_assets {
489 if asset.is_candidate() {
490 asset
491 .make_mut()
492 .select_candidate_for_investment(agent.id.clone());
493 }
494 }
495
496 Ok(best_assets)
497}
498
499fn prepare_commissioned_assets_for_retention(assets: &mut [AssetRef]) -> HashMap<AssetRef, u32> {
504 let mut available_retention_tranches = HashMap::new();
505
506 for asset in assets.iter_mut().filter(|asset| asset.is_commissioned()) {
507 let num_tranches = asset.num_tranches();
508
509 *asset = asset.clone().as_single_tranche();
511
512 available_retention_tranches.insert(asset.clone(), num_tranches);
514 }
515
516 available_retention_tranches
517}
518
519fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: Flow) -> bool {
521 demand.values().any(|flow| *flow > absolute_tolerance)
522}
523
524fn remove_assets_exceeding_agent_limits(
526 opt_assets: &mut Vec<AssetRef>,
527 remaining_agent_limits: &HashMap<ProcessID, Capacity>,
528 only_candidates: bool,
529) {
530 opt_assets.retain(|asset| {
531 (only_candidates && !asset.is_candidate())
532 || remaining_agent_limits
533 .get(asset.process_id())
534 .is_none_or(|limit| *limit >= asset.total_capacity())
535 });
536}
537
538fn subtract_capacity_from_remaining_limit(
540 best_asset: &AssetRef,
541 remaining_agent_limits: &mut HashMap<ProcessID, Capacity>,
542) {
543 if let Some(remaining_capacity) = remaining_agent_limits.get_mut(best_asset.process_id()) {
544 *remaining_capacity -= best_asset.total_capacity();
545 assert!(
546 *remaining_capacity >= Capacity(0.0),
547 "Remaining Capacity has fallen below zero"
548 );
549 }
550}
551
552fn update_selection_state(
567 best_asset: &AssetRef,
568 opt_assets: &mut Vec<AssetRef>,
569 remaining_agent_addition_limits: &mut HashMap<ProcessID, Capacity>,
570 remaining_agent_total_limits: &mut HashMap<ProcessID, Capacity>,
571 available_retention_tranches: &mut HashMap<AssetRef, u32>,
572) {
573 subtract_capacity_from_remaining_limit(best_asset, remaining_agent_total_limits);
575
576 if best_asset.is_candidate() {
579 subtract_capacity_from_remaining_limit(best_asset, remaining_agent_addition_limits);
581 } else {
582 let remaining = available_retention_tranches.get_mut(best_asset).unwrap();
585 *remaining = remaining.saturating_sub(1);
586
587 if *remaining == 0 {
589 let old_idx = opt_assets
590 .iter()
591 .position(|asset| *asset == *best_asset)
592 .unwrap();
593 opt_assets.swap_remove(old_idx);
594 available_retention_tranches.remove(best_asset);
595 }
596 }
597}
598
599fn record_asset_selection(best_asset: AssetRef, best_assets: &mut Vec<AssetRef>) {
608 assert!(
609 best_asset.is_commissioned() || best_asset.is_candidate(),
610 "Invalid asset type"
611 );
612
613 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset) {
615 existing_asset
617 .make_mut()
618 .increase_capacity(best_asset.capacity());
619 } else {
620 best_assets.push(best_asset.with_no_mothballed_tranches());
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use crate::commodity::Commodity;
629 use crate::fixture::{
630 agent_id, asset, process, process_activity_limits_map, process_flows_map, svd_commodity,
631 time_slice, time_slice_info, time_slice_info2,
632 };
633 use crate::process::{ActivityLimits, FlowType, Process, ProcessFlow};
634 use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
635 use crate::units::Dimensionless;
636 use crate::units::{Flow, FlowPerActivity, MoneyPerFlow};
637 use indexmap::indexmap;
638 use rstest::{fixture, rstest};
639
640 use std::sync::Arc;
641
642 #[rstest]
643 fn get_demand_limiting_capacity_works(
644 time_slice: TimeSliceID,
645 time_slice_info: TimeSliceInfo,
646 svd_commodity: Commodity,
647 mut process: Process,
648 ) {
649 let commodity_rc = Arc::new(svd_commodity);
651 let process_flow = ProcessFlow {
652 commodity: Arc::clone(&commodity_rc),
653 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
655 cost: MoneyPerFlow(0.0),
656 };
657 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
658 let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows));
659 process.flows = process_flows_map;
660
661 let asset = asset(process);
663
664 let demand = indexmap! { time_slice.clone() => Flow(10.0)};
666
667 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
669
670 assert_eq!(result, Capacity(5.0));
674 }
675
676 #[rstest]
677 fn get_demand_limiting_capacity_multiple_time_slices(
678 time_slice_info2: TimeSliceInfo,
679 svd_commodity: Commodity,
680 mut process: Process,
681 ) {
682 let (time_slice1, time_slice2) =
683 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
684
685 let commodity_rc = Arc::new(svd_commodity);
687 let process_flow = ProcessFlow {
688 commodity: Arc::clone(&commodity_rc),
689 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
691 cost: MoneyPerFlow(0.0),
692 };
693 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
694 let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows));
695 process.flows = process_flows_map;
696
697 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
699 limits.add_time_slice_limit(time_slice1.clone(), Dimensionless(0.0)..=Dimensionless(0.2));
700 limits.add_time_slice_limit(time_slice2.clone(), Dimensionless(0.0)..=Dimensionless(0.0));
701 let limits_map = process_activity_limits_map(process.regions.clone(), limits);
702 process.activity_limits = limits_map;
703
704 let asset = asset(process);
706
707 let demand = indexmap! {
709 time_slice1.clone() => Flow(4.0), time_slice2.clone() => Flow(3.0), };
712
713 let result =
715 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
716
717 assert_eq!(result, Capacity(20.0));
722 }
723
724 #[rstest]
725 fn get_demand_limiting_capacity_uses_coarser_limits(
726 time_slice_info2: TimeSliceInfo,
727 svd_commodity: Commodity,
728 mut process: Process,
729 ) {
730 let (time_slice1, time_slice2) =
731 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
732
733 let commodity_rc = Arc::new(svd_commodity);
735 let process_flow = ProcessFlow {
736 commodity: Arc::clone(&commodity_rc),
737 coeff: FlowPerActivity(1.0),
738 kind: FlowType::Fixed,
739 cost: MoneyPerFlow(0.0),
740 };
741
742 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
743 process.flows = process_flows_map(process.regions.clone(), Arc::new(process_flows));
744
745 let limits = HashMap::from([
754 (
755 TimeSliceSelection::Single(time_slice1.clone()),
756 Dimensionless(0.0)..=Dimensionless(1.0),
757 ),
758 (
759 TimeSliceSelection::Single(time_slice2.clone()),
760 Dimensionless(0.0)..=Dimensionless(1.0),
761 ),
762 (
763 TimeSliceSelection::Annual,
764 Dimensionless(0.0)..=Dimensionless(0.5),
765 ),
766 ]);
767
768 process.activity_limits = process_activity_limits_map(
769 process.regions.clone(),
770 ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap(),
771 );
772
773 let asset = asset(process);
774
775 let demand = indexmap! {
776 time_slice1.clone() => Flow(5.0),
777 time_slice2.clone() => Flow(5.0),
778 };
779
780 let result =
781 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
782
783 assert_eq!(result, Capacity(20.0));
784 }
785
786 #[rstest]
787 #[case(Flow(10.0), Dimensionless(1.0), Capacity(10.0))] #[case(Flow(0.0), Dimensionless(1.0), Capacity(0.0))] #[case(Flow(10.0), Dimensionless(0.5), Capacity(20.0))] #[case(Flow(10.0), Dimensionless(0.0), Capacity(0.0))] fn calculate_asset_capacity_scale_works(
792 time_slice: TimeSliceID,
793 time_slice_info: TimeSliceInfo,
794 svd_commodity: Commodity,
795 mut process: Process,
796 #[case] demand_value: Flow,
797 #[case] activity_limit: Dimensionless,
798 #[case] expected: Capacity,
799 ) {
800 let commodity_rc = Arc::new(svd_commodity);
801 let process_flow = ProcessFlow {
802 commodity: Arc::clone(&commodity_rc),
803 coeff: FlowPerActivity(1.0),
804 kind: FlowType::Fixed,
805 cost: MoneyPerFlow(0.0),
806 };
807 process.flows = process_flows_map(
808 process.regions.clone(),
809 Arc::new(indexmap! { commodity_rc.id.clone() => process_flow }),
810 );
811
812 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info);
813 limits.add_time_slice_limit(time_slice.clone(), Dimensionless(0.0)..=activity_limit);
814 process.activity_limits = process_activity_limits_map(process.regions.clone(), limits);
815
816 let asset = asset(process);
817 let demand = indexmap! { time_slice => demand_value };
818 assert_eq!(
819 calculate_candidate_asset_capacity_scale(&asset, &commodity_rc, &demand),
820 expected
821 );
822 }
823
824 #[rstest]
825 fn subtract_capacity_from_remaining_limit_works(asset: Asset) {
826 let mut remaining_agent_limits =
827 HashMap::from([(asset.process_id().clone(), Capacity(10.0))]);
828 subtract_capacity_from_remaining_limit(
829 &AssetRef::from(asset.clone()),
830 &mut remaining_agent_limits,
831 );
832
833 assert_eq!(
834 remaining_agent_limits[asset.process_id()].clone(),
835 Capacity(8.0)
836 );
837 }
838
839 #[fixture]
840 fn commissioned_asset(agent_id: AgentID, asset: Asset) -> Asset {
841 Asset::new_commissioned(
842 agent_id,
843 Arc::new(asset.process().clone()),
844 asset.region_id().clone(),
845 asset.capacity(),
846 asset.commission_year(),
847 )
848 .unwrap()
849 }
850
851 #[fixture]
852 fn candidate_asset(asset: Asset) -> Asset {
853 Asset::new_candidate(
854 Arc::new(asset.process().clone()),
855 asset.region_id().clone(),
856 asset.capacity().tranche_size(),
857 asset.commission_year(),
858 )
859 .unwrap()
860 }
861
862 #[rstest]
863 fn remove_assets_exceeding_agent_limits_works(
864 commissioned_asset: Asset,
865 candidate_asset: Asset,
866 ) {
867 let mut opt_assets = vec![
868 commissioned_asset.clone().into(),
869 candidate_asset.clone().into(),
870 ];
871 let limits = HashMap::from([(commissioned_asset.process_id().clone(), Capacity(1.0))]);
872
873 remove_assets_exceeding_agent_limits(&mut opt_assets, &limits, true);
875 assert_eq!(opt_assets.len(), 1);
876 assert_eq!(opt_assets[0], commissioned_asset.clone().into());
877
878 opt_assets.push(candidate_asset.clone().into());
880 remove_assets_exceeding_agent_limits(&mut opt_assets, &limits, false);
881 assert!(opt_assets.is_empty());
882 }
883
884 #[rstest]
885 #[case(false)]
886 #[case(true)]
887 fn update_selection_state_works(
888 commissioned_asset: Asset,
889 candidate_asset: Asset,
890 #[case] commissioned: bool,
891 ) {
892 let best_asset: AssetRef = if commissioned {
893 commissioned_asset.clone().into()
894 } else {
895 candidate_asset.clone().into()
896 };
897 let mut opt_assets = vec![best_asset.clone()];
898 let mut addition_limits = HashMap::from([(best_asset.process_id().clone(), Capacity(2.0))]);
899 let mut total_limits = HashMap::from([(best_asset.process_id().clone(), Capacity(2.0))]);
900 let mut retention_tranches = HashMap::from([(best_asset.clone(), 1)]);
901
902 update_selection_state(
903 &best_asset,
904 &mut opt_assets,
905 &mut addition_limits,
906 &mut total_limits,
907 &mut retention_tranches,
908 );
909
910 if commissioned {
911 assert_eq!(total_limits[best_asset.process_id()], Capacity(0.0));
913 assert!(!retention_tranches.contains_key(&best_asset));
914
915 assert_eq!(addition_limits[best_asset.process_id()], Capacity(2.0));
917 } else {
918 assert_eq!(total_limits[best_asset.process_id()], Capacity(0.0));
920 assert_eq!(addition_limits[best_asset.process_id()], Capacity(0.0));
921
922 assert_eq!(retention_tranches[&best_asset], 1);
924 }
925 }
926}