@@ -9,11 +9,11 @@ use crate::region::RegionID;
99use crate :: simulation:: CommodityPrices ;
1010use crate :: time_slice:: { TimeSliceID , TimeSliceInfo } ;
1111use crate :: units:: { Capacity , Dimensionless , Flow , FlowPerCapacity } ;
12- use anyhow:: { Result , bail , ensure} ;
12+ use anyhow:: { Result , ensure} ;
1313use indexmap:: IndexMap ;
1414use itertools:: { Itertools , chain, iproduct} ;
15- use log:: debug;
16- use std:: collections:: HashMap ;
15+ use log:: { debug, warn } ;
16+ use std:: collections:: { HashMap , VecDeque } ;
1717use std:: fmt:: Display ;
1818
1919pub mod appraisal;
@@ -55,6 +55,8 @@ impl InvestmentSet {
5555 demand : & AllDemandMap ,
5656 existing_assets : & [ AssetRef ] ,
5757 prices : & CommodityPrices ,
58+ seen_markets : & [ MarketID ] ,
59+ previously_selected_assets : & [ AssetRef ] ,
5860 writer : & mut DataWriter ,
5961 ) -> Result < Vec < AssetRef > > {
6062 match self {
@@ -67,15 +69,20 @@ impl InvestmentSet {
6769 prices,
6870 writer,
6971 ) ,
70- InvestmentSet :: Cycle ( markets) => select_assets_for_cycle (
71- model,
72- markets,
73- year,
74- demand,
75- existing_assets,
76- prices,
77- writer,
78- ) ,
72+ InvestmentSet :: Cycle ( markets) => {
73+ debug ! ( "Starting investment for cycle '{self}'" ) ;
74+ select_assets_for_cycle (
75+ model,
76+ markets,
77+ year,
78+ demand,
79+ existing_assets,
80+ prices,
81+ seen_markets,
82+ previously_selected_assets,
83+ writer,
84+ )
85+ }
7986 InvestmentSet :: Layer ( investment_sets) => {
8087 debug ! ( "Starting investment for layer '{self}'" ) ;
8188 let mut all_assets = Vec :: new ( ) ;
@@ -86,6 +93,8 @@ impl InvestmentSet {
8693 demand,
8794 existing_assets,
8895 prices,
96+ seen_markets,
97+ previously_selected_assets,
8998 writer,
9099 ) ?;
91100 all_assets. extend ( assets) ;
@@ -157,7 +166,9 @@ pub fn perform_agent_investment(
157166 year,
158167 & net_demand,
159168 existing_assets,
160- prices,
169+ & external_prices,
170+ & seen_markets,
171+ & all_selected_assets,
161172 writer,
162173 ) ?;
163174
@@ -277,16 +288,114 @@ fn select_assets_for_market(
277288 Ok ( selected_assets)
278289}
279290
291+ #[ allow( clippy:: too_many_arguments) ]
280292fn select_assets_for_cycle (
281- _model : & Model ,
293+ model : & Model ,
282294 markets : & [ MarketID ] ,
283- _year : u32 ,
284- _demand : & AllDemandMap ,
285- _existing_assets : & [ AssetRef ] ,
286- _prices : & CommodityPrices ,
287- _writer : & mut DataWriter ,
295+ year : u32 ,
296+ demand : & AllDemandMap ,
297+ existing_assets : & [ AssetRef ] ,
298+ prices : & CommodityPrices ,
299+ seen_markets : & [ MarketID ] ,
300+ previously_selected_assets : & [ AssetRef ] ,
301+ writer : & mut DataWriter ,
288302) -> Result < Vec < AssetRef > > {
289- bail ! ( "Investment cycles are not yet supported. Found cycle for commodities: {markets:?}" ) ;
303+ // Get markets to balance: all seen so far plus all in loop
304+ let mut markets_to_balance = seen_markets. to_vec ( ) ;
305+ markets_to_balance. extend_from_slice ( markets) ;
306+
307+ // Initialise list of unbalanced markets: start with all markets in the cycle
308+ let mut unbalanced_markets = VecDeque :: from ( markets. to_vec ( ) ) ;
309+
310+ // Iterate while there are markets in the unbalanced list
311+ let mut current_demand = demand. clone ( ) ;
312+ let mut selected_assets = HashMap :: new ( ) ;
313+ let mut loop_iter = 0 ;
314+ loop {
315+ // If max iterations reached, break with a warning
316+ if loop_iter == 10 {
317+ warn ! (
318+ "Max cycle investment iterations reached. Unable to find stable solution for the following markets: {}" ,
319+ unbalanced_markets. iter( ) . join( ", " )
320+ ) ;
321+ break ;
322+ }
323+
324+ // Pop off the first market
325+ // If there are no markets with unmet demand, we're done
326+ let Some ( current_market) = unbalanced_markets. pop_front ( ) else {
327+ debug ! (
328+ "Cycle investment for '{}' converged after {} iterations" ,
329+ markets. iter( ) . join( ", " ) ,
330+ loop_iter
331+ ) ;
332+ break ;
333+ } ;
334+
335+ // Select assets for this market
336+ // This will replace any previously selected assets for this market
337+ let assets = select_assets_for_market (
338+ model,
339+ & current_market,
340+ year,
341+ & current_demand,
342+ existing_assets,
343+ prices,
344+ writer,
345+ ) ?;
346+ selected_assets. insert ( current_market. clone ( ) , assets) ;
347+
348+ // Assemble full list of assets for dispatch
349+ let mut all_assets = previously_selected_assets. to_vec ( ) ;
350+ let flat_selected: Vec < AssetRef > = selected_assets
351+ . values ( )
352+ . flat_map ( |v| v. iter ( ) . cloned ( ) )
353+ . collect ( ) ;
354+ all_assets. extend_from_slice ( & flat_selected) ;
355+
356+ // Run dispatch
357+ // We allow unmet demand for all markets except the one we're currently balancing
358+ let markets_with_unmet_demand: Vec < MarketID > = markets
359+ . iter ( )
360+ . filter ( |m| * m != & current_market)
361+ . cloned ( )
362+ . collect ( ) ;
363+ let solution = DispatchRun :: new ( model, & all_assets, year)
364+ . with_market_balance_subset ( & markets_to_balance)
365+ . with_unmet_demand_vars ( & markets_with_unmet_demand)
366+ . run (
367+ & format ! (
368+ "cycle ({}) iteration {}" ,
369+ markets. iter( ) . join( ", " ) ,
370+ loop_iter
371+ ) ,
372+ writer,
373+ ) ?;
374+
375+ // Find markets with unmet demand and add these to the unbalanced list if not already present
376+ let coms = solution. get_markets_with_unmet_demand ( ) ;
377+ for market in coms {
378+ if !unbalanced_markets. contains ( & market) {
379+ unbalanced_markets. push_back ( market) ;
380+ }
381+ }
382+ println ! (
383+ "Unbalanced markets: {}" ,
384+ unbalanced_markets. iter( ) . join( ", " )
385+ ) ;
386+
387+ // Calculate a new demand map for the next iteration
388+ current_demand. clone_from ( demand) ;
389+ update_demand_map_with_inputs (
390+ & mut current_demand,
391+ & solution. create_flow_map ( ) ,
392+ & flat_selected,
393+ ) ;
394+
395+ loop_iter += 1 ;
396+ }
397+
398+ Ok ( selected_assets. into_values ( ) . flatten ( ) . collect ( ) )
290399}
291400
292401/// Flatten the preset commodity demands for a given year into a map of commodity, region and
@@ -362,6 +471,28 @@ fn update_net_demand_map(demand: &mut AllDemandMap, flows: &FlowMap, assets: &[A
362471 }
363472}
364473
474+ /// Update demand map with input flows from a set of assets
475+ fn update_demand_map_with_inputs ( demand : & mut AllDemandMap , flows : & FlowMap , assets : & [ AssetRef ] ) {
476+ for ( ( asset, commodity_id, time_slice) , flow) in flows {
477+ if assets. contains ( asset) {
478+ let key = (
479+ commodity_id. clone ( ) ,
480+ asset. region_id ( ) . clone ( ) ,
481+ time_slice. clone ( ) ,
482+ ) ;
483+
484+ // Only consider input flows
485+ if flow < & Flow ( 0.0 ) {
486+ // Note: we use the negative of the flow as input flows are negative in the flow map.
487+ demand
488+ . entry ( key)
489+ . and_modify ( |value| * value -= * flow)
490+ . or_insert ( -* flow) ;
491+ }
492+ }
493+ }
494+ }
495+
365496/// Get a portion of the demand profile for this commodity and region
366497fn get_demand_portion_for_commodity (
367498 time_slice_info : & TimeSliceInfo ,
0 commit comments