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::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
12use crate::timeit::InvestmentTimer;
13use crate::units::{ActivityPerCapacity, Capacity, 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<TimeSliceSelection, Flow>;
32
33pub type AllDemandMap = IndexMap<(CommodityID, RegionID, TimeSliceSelection), 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 = collect_preset_demands_for_year(&model.commodities, year);
60
61 let mut all_selected_assets = Vec::new();
64
65 let investment_order = &model.investment_order[&year];
66 debug!(
67 "Investment order for year '{year}': {}",
68 investment_order.iter().join(" -> ")
69 );
70
71 let mut seen_markets = Vec::new();
75
76 for market_set in investment_order {
78 let selected_assets = market_set.select_assets(
80 model,
81 year,
82 &net_demand,
83 existing_assets,
84 prices,
85 &seen_markets,
86 &all_selected_assets,
87 writer,
88 )?;
89
90 for market in market_set.iter_markets() {
92 seen_markets.push(market.clone());
93 }
94
95 if selected_assets.is_empty() {
99 debug!("No assets selected for '{market_set}'");
100 continue;
101 }
102
103 all_selected_assets.extend(selected_assets.iter().cloned());
105
106 debug!("Running post-investment dispatch for '{market_set}'");
110
111 let solution = DispatchRun::new(model, &all_selected_assets, year)
114 .without_commodity_constraints()
115 .with_market_balance_subset(&seen_markets)
116 .with_input_prices(&prices.shadow)
117 .run(&format!("post {market_set} investment"), writer)?;
118
119 update_net_demand_map(
121 &mut net_demand,
122 &solution.create_flow_map(),
123 &selected_assets,
124 );
125 }
126
127 Ok(all_selected_assets)
128}
129
130pub fn collect_preset_demands_for_year(commodities: &CommodityMap, year: u32) -> AllDemandMap {
136 let mut demand_map = AllDemandMap::new();
137 for (commodity_id, commodity) in commodities {
138 for ((region_id, data_year, time_slice_selection), demand) in &commodity.demand {
139 if *data_year != year {
140 continue;
141 }
142 demand_map.insert(
143 (
144 commodity_id.clone(),
145 region_id.clone(),
146 time_slice_selection.clone(),
147 ),
148 *demand,
149 );
150 }
151 }
152 demand_map
153}
154
155pub fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[AssetRef]) {
165 for ((asset, commodity_id, time_slice), flow) in flows {
166 if assets.contains(asset) {
167 if (flow < &Flow(0.0))
170 || asset
171 .primary_output()
172 .is_some_and(|p| &p.commodity.id == commodity_id)
173 {
174 let level = asset
175 .get_flow(commodity_id)
176 .unwrap()
177 .commodity
178 .time_slice_level;
179 let selection = level.containing_selection(time_slice);
180 let key = (commodity_id.clone(), asset.region_id().clone(), selection);
181 demand
183 .entry(key)
184 .and_modify(|value| *value -= *flow)
185 .or_insert(-*flow);
186 }
187 }
188 }
189}
190
191pub fn calculate_candidate_asset_capacity_scale(
203 asset: &Asset,
204 commodity: &Commodity,
205 demand: &DemandMap,
206) -> Capacity {
207 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
208 let max_annual_supply_per_capacity = *asset
209 .get_activity_per_capacity_limits_for_selection(&TimeSliceSelection::Annual)
210 .end()
211 * coeff;
212 if max_annual_supply_per_capacity < FlowPerCapacity::EPSILON {
213 return Capacity(0.0);
214 }
215 let annual_demand = demand.values().copied().sum::<Flow>();
216 annual_demand / max_annual_supply_per_capacity
217}
218
219pub fn get_demand_limiting_capacity(
233 time_slice_info: &TimeSliceInfo,
234 asset: &Asset,
235 commodity: &Commodity,
236 demand: &DemandMap,
237) -> Capacity {
238 let coeff = asset.get_flow(&commodity.id).unwrap().coeff;
239 let mut capacity = Capacity(0.0);
240 let mut demand_cache: HashMap<_, Flow> = HashMap::new();
241
242 for level in TimeSliceLevel::iter() {
244 for selection in time_slice_info.iter_selections_at_level(level) {
245 let max_supply_for_selection = *asset
247 .get_activity_per_capacity_limits_for_selection(&selection)
248 .end()
249 * coeff;
250
251 if max_supply_for_selection == FlowPerCapacity(0.0) {
254 continue;
255 }
256
257 let demand_selection_level = level.max(commodity.time_slice_level);
269 let demand_selection = selection
270 .containing_selection_at_level(demand_selection_level)
271 .unwrap();
272 let serviceable_demand_for_selection = *demand_cache
273 .entry(demand_selection.clone())
274 .or_insert_with(|| {
275 demand_selection
276 .iter_at_level(time_slice_info, commodity.time_slice_level)
277 .unwrap()
278 .filter(|(bucket, _)| {
279 bucket.iter(time_slice_info).any(|(ts, _)| {
280 *asset.get_activity_per_capacity_limits(ts).end()
281 > ActivityPerCapacity(0.0)
282 })
283 })
284 .map(|(bucket, _)| demand[&bucket])
285 .sum()
286 });
287
288 capacity = capacity.max(serviceable_demand_for_selection / max_supply_for_selection);
291 }
292 }
293
294 capacity
295}
296
297fn log_on_equal_appraisal_outputs(
299 outputs: &[AppraisalOutput],
300 agent_id: &AgentID,
301 commodity_id: &CommodityID,
302 region_id: &RegionID,
303) {
304 if outputs.is_empty() {
305 return;
306 }
307
308 let num_identical = count_equal_and_best_appraisal_outputs(outputs);
309
310 if num_identical > 0 {
311 let asset_details = outputs[..=num_identical]
312 .iter()
313 .map(|output| {
314 let asset = &output.asset;
315 format!(
316 "Process ID: '{}' (State: {}{}, Commission year: {})",
317 asset.process_id(),
318 asset.state(),
319 asset
320 .id()
321 .map(|id| format!(", Asset ID: {id}"))
322 .unwrap_or_default(),
323 asset.commission_year()
324 )
325 })
326 .join(", ");
327 debug!(
328 "Found equally good appraisals for Agent ID: {agent_id}, Commodity: '{commodity_id}', \
329 Region: {region_id}. Options: [{asset_details}]. Selecting first option.",
330 );
331 }
332}
333
334#[allow(clippy::too_many_arguments)]
336pub fn select_best_assets(
337 model: &Model,
338 mut opt_assets: Vec<AssetRef>,
339 agent_addition_limits: HashMap<ProcessID, Capacity>,
340 agent_total_limits: HashMap<ProcessID, Capacity>,
341 commodity: &Commodity,
342 agent: &Agent,
343 region_id: &RegionID,
344 prices: &Prices,
345 mut demand: DemandMap,
346 year: u32,
347 writer: &mut DataWriter,
348) -> Result<Vec<AssetRef>> {
349 let objective_type = &agent.objectives[&year];
350
351 let mut remaining_agent_addition_limits = agent_addition_limits;
354
355 let mut remaining_agent_total_limits = agent_total_limits;
358
359 let mut available_retention_tranches =
361 prepare_commissioned_assets_for_retention(&mut opt_assets);
362
363 let coefficients =
365 calculate_coefficients_for_assets(model, objective_type, &opt_assets, prices, year);
366
367 let mut round = 0;
369 let mut best_assets: Vec<AssetRef> = Vec::new();
370 while is_any_remaining_demand(
371 &demand,
372 model.parameters.remaining_demand_absolute_tolerance,
373 ) {
374 remove_assets_exceeding_agent_limits(
377 &mut opt_assets,
378 &remaining_agent_addition_limits,
379 true,
380 );
381 remove_assets_exceeding_agent_limits(&mut opt_assets, &remaining_agent_total_limits, false);
382 ensure!(
383 !opt_assets.is_empty(),
384 "Failed to meet demand for commodity '{}' in region '{}' with provided investment \
385 options. This may be due to overly restrictive process investment constraints.",
386 commodity.id,
387 region_id
388 );
389 let mut outputs: Vec<AppraisalOutput> = opt_assets
394 .par_iter()
395 .map(|asset| -> Result<Option<AppraisalOutput>> {
396 if asset.total_capacity() <= Capacity(0.0) {
398 return Ok(None);
399 }
400
401 Ok(Some(appraise_investment(
402 model,
403 asset,
404 commodity,
405 objective_type,
406 &coefficients[asset],
407 &demand,
408 )?))
409 })
410 .collect::<Result<Vec<_>>>()? .into_iter()
412 .flatten()
413 .collect();
414
415 writer.write_appraisal_debug_info(
417 year,
418 &format!("{} {} round {}", commodity.id, agent.id, round),
419 &outputs,
420 &demand,
421 commodity.time_slice_level,
422 )?;
423
424 let num_nonfeasible = sort_and_filter_appraisal_outputs(&mut outputs);
426
427 if outputs.is_empty() {
431 warn!(
432 "Investment appraisal completed with unmet demand for commodity '{}', region '{}', \
433 year '{}', agent '{}'. {} non-feasible investments were not considered. \
434 This unmet demand may still be satisfied during the full system dispatch.",
435 commodity.id, region_id, year, agent.id, num_nonfeasible
436 );
437 break;
438 }
439
440 log_on_equal_appraisal_outputs(&outputs, &agent.id, &commodity.id, region_id);
442
443 let best_output = outputs.into_iter().next().unwrap();
444
445 debug!(
447 "Selected {} asset '{}' (capacity: {})",
448 best_output.asset.state(),
449 best_output.asset.process_id(),
450 best_output.asset.total_capacity()
451 );
452
453 update_selection_state(
455 &best_output.asset,
456 &mut opt_assets,
457 &mut remaining_agent_addition_limits,
458 &mut remaining_agent_total_limits,
459 &mut available_retention_tranches,
460 );
461
462 record_asset_selection(best_output.asset, &mut best_assets);
464
465 demand = best_output.unmet_demand;
466 round += 1;
467 }
468
469 for asset in &mut best_assets {
472 if asset.is_candidate() {
473 asset
474 .make_mut()
475 .select_candidate_for_investment(agent.id.clone());
476 }
477 }
478
479 Ok(best_assets)
480}
481
482fn prepare_commissioned_assets_for_retention(assets: &mut [AssetRef]) -> HashMap<AssetRef, u32> {
487 let mut available_retention_tranches = HashMap::new();
488
489 for asset in assets.iter_mut().filter(|asset| asset.is_commissioned()) {
490 let num_tranches = asset.num_tranches();
491
492 *asset = asset.clone().as_single_tranche();
494
495 available_retention_tranches.insert(asset.clone(), num_tranches);
497 }
498
499 available_retention_tranches
500}
501
502fn is_any_remaining_demand(demand: &DemandMap, absolute_tolerance: Flow) -> bool {
504 demand.values().any(|flow| *flow > absolute_tolerance)
505}
506
507fn remove_assets_exceeding_agent_limits(
509 opt_assets: &mut Vec<AssetRef>,
510 remaining_agent_limits: &HashMap<ProcessID, Capacity>,
511 only_candidates: bool,
512) {
513 opt_assets.retain(|asset| {
514 (only_candidates && !asset.is_candidate())
515 || remaining_agent_limits
516 .get(asset.process_id())
517 .is_none_or(|limit| *limit >= asset.total_capacity())
518 });
519}
520
521fn subtract_capacity_from_remaining_limit(
523 best_asset: &AssetRef,
524 remaining_agent_limits: &mut HashMap<ProcessID, Capacity>,
525) {
526 if let Some(remaining_capacity) = remaining_agent_limits.get_mut(best_asset.process_id()) {
527 *remaining_capacity -= best_asset.total_capacity();
528 assert!(
529 *remaining_capacity >= Capacity(0.0),
530 "Remaining Capacity has fallen below zero"
531 );
532 }
533}
534
535fn update_selection_state(
550 best_asset: &AssetRef,
551 opt_assets: &mut Vec<AssetRef>,
552 remaining_agent_addition_limits: &mut HashMap<ProcessID, Capacity>,
553 remaining_agent_total_limits: &mut HashMap<ProcessID, Capacity>,
554 available_retention_tranches: &mut HashMap<AssetRef, u32>,
555) {
556 subtract_capacity_from_remaining_limit(best_asset, remaining_agent_total_limits);
558
559 if best_asset.is_candidate() {
562 subtract_capacity_from_remaining_limit(best_asset, remaining_agent_addition_limits);
564 } else {
565 let remaining = available_retention_tranches.get_mut(best_asset).unwrap();
568 *remaining = remaining.saturating_sub(1);
569
570 if *remaining == 0 {
572 let old_idx = opt_assets
573 .iter()
574 .position(|asset| *asset == *best_asset)
575 .unwrap();
576 opt_assets.swap_remove(old_idx);
577 available_retention_tranches.remove(best_asset);
578 }
579 }
580}
581
582fn record_asset_selection(best_asset: AssetRef, best_assets: &mut Vec<AssetRef>) {
591 assert!(
592 best_asset.is_commissioned() || best_asset.is_candidate(),
593 "Invalid asset type"
594 );
595
596 if let Some(existing_asset) = best_assets.iter_mut().find(|asset| **asset == best_asset) {
598 existing_asset
600 .make_mut()
601 .increase_capacity(best_asset.capacity());
602 } else {
603 best_assets.push(best_asset.with_no_mothballed_tranches());
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use crate::commodity::Commodity;
612 use crate::fixture::{
613 agent_id, asset, process, process_activity_limits_map, process_flows_map, svd_commodity,
614 time_slice, time_slice_info, time_slice_info2,
615 };
616 use crate::process::{ActivityLimits, FlowType, Process, ProcessFlow};
617 use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
618 use crate::units::Dimensionless;
619 use crate::units::{Flow, FlowPerActivity, MoneyPerFlow};
620 use indexmap::indexmap;
621 use rstest::{fixture, rstest};
622
623 use std::sync::Arc;
624
625 #[rstest]
626 fn get_demand_limiting_capacity_works(
627 time_slice: TimeSliceID,
628 time_slice_info: TimeSliceInfo,
629 svd_commodity: Commodity,
630 mut process: Process,
631 ) {
632 let commodity_rc = Arc::new(svd_commodity);
634 let process_flow = ProcessFlow {
635 commodity: Arc::clone(&commodity_rc),
636 coeff: FlowPerActivity(2.0), kind: FlowType::Fixed,
638 cost: MoneyPerFlow(0.0),
639 };
640 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
641 let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows));
642 process.flows = process_flows_map;
643
644 let asset = asset(process);
646
647 let demand = indexmap! { TimeSliceSelection::Single(time_slice.clone()) => Flow(10.0)};
649
650 let result = get_demand_limiting_capacity(&time_slice_info, &asset, &commodity_rc, &demand);
652
653 assert_eq!(result, Capacity(5.0));
657 }
658
659 #[rstest]
660 fn get_demand_limiting_capacity_multiple_time_slices(
661 time_slice_info2: TimeSliceInfo,
662 svd_commodity: Commodity,
663 mut process: Process,
664 ) {
665 let (time_slice1, time_slice2) =
666 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
667
668 let commodity_rc = Arc::new(svd_commodity);
670 let process_flow = ProcessFlow {
671 commodity: Arc::clone(&commodity_rc),
672 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
674 cost: MoneyPerFlow(0.0),
675 };
676 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
677 let process_flows_map = process_flows_map(process.regions.clone(), Arc::new(process_flows));
678 process.flows = process_flows_map;
679
680 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
682 limits.add_time_slice_limit(time_slice1.clone(), Dimensionless(0.0)..=Dimensionless(0.2));
683 limits.add_time_slice_limit(time_slice2.clone(), Dimensionless(0.0)..=Dimensionless(0.0));
684 let limits_map = process_activity_limits_map(process.regions.clone(), limits);
685 process.activity_limits = limits_map;
686
687 let asset = asset(process);
689
690 let demand = indexmap! {
692 TimeSliceSelection::Single(time_slice1.clone()) => Flow(4.0), TimeSliceSelection::Single(time_slice2.clone()) => Flow(3.0), };
695
696 let result =
698 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
699
700 assert_eq!(result, Capacity(20.0));
705 }
706
707 #[rstest]
708 fn get_demand_limiting_capacity_uses_coarser_limits(
709 time_slice_info2: TimeSliceInfo,
710 svd_commodity: Commodity,
711 mut process: Process,
712 ) {
713 let (time_slice1, time_slice2) =
714 time_slice_info2.time_slices.keys().collect_tuple().unwrap();
715
716 let commodity_rc = Arc::new(svd_commodity);
718 let process_flow = ProcessFlow {
719 commodity: Arc::clone(&commodity_rc),
720 coeff: FlowPerActivity(1.0),
721 kind: FlowType::Fixed,
722 cost: MoneyPerFlow(0.0),
723 };
724
725 let process_flows = indexmap! { commodity_rc.id.clone() => process_flow.clone() };
726 process.flows = process_flows_map(process.regions.clone(), Arc::new(process_flows));
727
728 let limits = HashMap::from([
737 (
738 TimeSliceSelection::Single(time_slice1.clone()),
739 Dimensionless(0.0)..=Dimensionless(1.0),
740 ),
741 (
742 TimeSliceSelection::Single(time_slice2.clone()),
743 Dimensionless(0.0)..=Dimensionless(1.0),
744 ),
745 (
746 TimeSliceSelection::Annual,
747 Dimensionless(0.0)..=Dimensionless(0.5),
748 ),
749 ]);
750
751 process.activity_limits = process_activity_limits_map(
752 process.regions.clone(),
753 ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap(),
754 );
755
756 let asset = asset(process);
757
758 let demand = indexmap! {
759 TimeSliceSelection::Single(time_slice1.clone()) => Flow(5.0),
760 TimeSliceSelection::Single(time_slice2.clone()) => Flow(5.0),
761 };
762
763 let result =
764 get_demand_limiting_capacity(&time_slice_info2, &asset, &commodity_rc, &demand);
765
766 assert_eq!(result, Capacity(20.0));
767 }
768
769 #[rstest]
770 #[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(
775 time_slice: TimeSliceID,
776 time_slice_info: TimeSliceInfo,
777 svd_commodity: Commodity,
778 mut process: Process,
779 #[case] demand_value: Flow,
780 #[case] activity_limit: Dimensionless,
781 #[case] expected: Capacity,
782 ) {
783 let commodity_rc = Arc::new(svd_commodity);
784 let process_flow = ProcessFlow {
785 commodity: Arc::clone(&commodity_rc),
786 coeff: FlowPerActivity(1.0),
787 kind: FlowType::Fixed,
788 cost: MoneyPerFlow(0.0),
789 };
790 process.flows = process_flows_map(
791 process.regions.clone(),
792 Arc::new(indexmap! { commodity_rc.id.clone() => process_flow }),
793 );
794
795 let mut limits = ActivityLimits::new_with_full_availability(&time_slice_info);
796 limits.add_time_slice_limit(time_slice.clone(), Dimensionless(0.0)..=activity_limit);
797 process.activity_limits = process_activity_limits_map(process.regions.clone(), limits);
798
799 let asset = asset(process);
800 let demand = indexmap! { TimeSliceSelection::Single(time_slice)=> demand_value };
801 assert_eq!(
802 calculate_candidate_asset_capacity_scale(&asset, &commodity_rc, &demand),
803 expected
804 );
805 }
806
807 #[rstest]
808 fn subtract_capacity_from_remaining_limit_works(asset: Asset) {
809 let mut remaining_agent_limits =
810 HashMap::from([(asset.process_id().clone(), Capacity(10.0))]);
811 subtract_capacity_from_remaining_limit(
812 &AssetRef::from(asset.clone()),
813 &mut remaining_agent_limits,
814 );
815
816 assert_eq!(
817 remaining_agent_limits[asset.process_id()].clone(),
818 Capacity(8.0)
819 );
820 }
821
822 #[fixture]
823 fn commissioned_asset(agent_id: AgentID, asset: Asset) -> Asset {
824 Asset::new_commissioned(
825 agent_id,
826 Arc::new(asset.process().clone()),
827 asset.region_id().clone(),
828 asset.capacity(),
829 asset.commission_year(),
830 )
831 .unwrap()
832 }
833
834 #[fixture]
835 fn candidate_asset(asset: Asset) -> Asset {
836 Asset::new_candidate(
837 Arc::new(asset.process().clone()),
838 asset.region_id().clone(),
839 asset.capacity().tranche_size(),
840 asset.commission_year(),
841 )
842 .unwrap()
843 }
844
845 #[rstest]
846 fn remove_assets_exceeding_agent_limits_works(
847 commissioned_asset: Asset,
848 candidate_asset: Asset,
849 ) {
850 let mut opt_assets = vec![
851 commissioned_asset.clone().into(),
852 candidate_asset.clone().into(),
853 ];
854 let limits = HashMap::from([(commissioned_asset.process_id().clone(), Capacity(1.0))]);
855
856 remove_assets_exceeding_agent_limits(&mut opt_assets, &limits, true);
858 assert_eq!(opt_assets.len(), 1);
859 assert_eq!(opt_assets[0], commissioned_asset.clone().into());
860
861 opt_assets.push(candidate_asset.clone().into());
863 remove_assets_exceeding_agent_limits(&mut opt_assets, &limits, false);
864 assert_eq!(opt_assets, []);
865 }
866
867 #[rstest]
868 #[case(false)]
869 #[case(true)]
870 fn update_selection_state_works(
871 commissioned_asset: Asset,
872 candidate_asset: Asset,
873 #[case] commissioned: bool,
874 ) {
875 let best_asset: AssetRef = if commissioned {
876 commissioned_asset.clone().into()
877 } else {
878 candidate_asset.clone().into()
879 };
880 let mut opt_assets = vec![best_asset.clone()];
881 let mut addition_limits = HashMap::from([(best_asset.process_id().clone(), Capacity(2.0))]);
882 let mut total_limits = HashMap::from([(best_asset.process_id().clone(), Capacity(2.0))]);
883 let mut retention_tranches = HashMap::from([(best_asset.clone(), 1)]);
884
885 update_selection_state(
886 &best_asset,
887 &mut opt_assets,
888 &mut addition_limits,
889 &mut total_limits,
890 &mut retention_tranches,
891 );
892
893 if commissioned {
894 assert_eq!(total_limits[best_asset.process_id()], Capacity(0.0));
896 assert!(!retention_tranches.contains_key(&best_asset));
897
898 assert_eq!(addition_limits[best_asset.process_id()], Capacity(2.0));
900 } else {
901 assert_eq!(total_limits[best_asset.process_id()], Capacity(0.0));
903 assert_eq!(addition_limits[best_asset.process_id()], Capacity(0.0));
904
905 assert_eq!(retention_tranches[&best_asset], 1);
907 }
908 }
909}