@@ -11,13 +11,15 @@ use crate::region::RegionID;
1111use crate :: simulation:: CommodityPrices ;
1212use crate :: time_slice:: { TimeSliceID , TimeSliceInfo , TimeSliceSelection } ;
1313use crate :: units:: {
14- Activity , Capacity , Flow , Money , MoneyPerActivity , MoneyPerCapacity , MoneyPerFlow , Year ,
14+ Activity , Capacity , Dimensionless , Flow , Money , MoneyPerActivity , MoneyPerCapacity ,
15+ MoneyPerFlow , Year ,
1516} ;
1617use anyhow:: { Result , bail, ensure} ;
1718use highs:: { HighsModelStatus , HighsStatus , RowProblem as Problem , Sense } ;
1819use indexmap:: { IndexMap , IndexSet } ;
19- use itertools:: { chain, iproduct} ;
20- use std:: collections:: HashMap ;
20+ use itertools:: { Itertools , chain, iproduct} ;
21+ use std:: cell:: Cell ;
22+ use std:: collections:: { HashMap , HashSet } ;
2123use std:: error:: Error ;
2224use std:: fmt;
2325use std:: ops:: Range ;
@@ -179,13 +181,56 @@ impl VariableMap {
179181 }
180182}
181183
184+ /// Create a map of commodity flows for each asset's coeffs at every time slice.
185+ ///
186+ /// Note that this only includes commodity flows which relate to existing assets, so not every
187+ /// commodity in the simulation will necessarily be represented.
188+ fn create_flow_map < ' a > (
189+ existing_assets : & [ AssetRef ] ,
190+ time_slice_info : & TimeSliceInfo ,
191+ activity : impl IntoIterator < Item = ( & ' a AssetRef , & ' a TimeSliceID , Activity ) > ,
192+ ) -> FlowMap {
193+ // The decision variables represent assets' activity levels, not commodity flows. We
194+ // multiply this value by the flow coeffs to get commodity flows.
195+ let mut flows = FlowMap :: new ( ) ;
196+ for ( asset, time_slice, activity) in activity {
197+ let n_units = Dimensionless ( asset. num_children ( ) . unwrap_or ( 1 ) as f64 ) ;
198+ for flow in asset. iter_flows ( ) {
199+ let flow_key = ( asset. clone ( ) , flow. commodity . id . clone ( ) , time_slice. clone ( ) ) ;
200+ let flow_value = activity * flow. coeff / n_units;
201+ flows. insert ( flow_key, flow_value) ;
202+ }
203+ }
204+
205+ // Copy flows for each child asset
206+ for asset in existing_assets {
207+ if let Some ( parent) = asset. parent ( ) {
208+ for commodity_id in asset. iter_flows ( ) . map ( |flow| & flow. commodity . id ) {
209+ for time_slice in time_slice_info. iter_ids ( ) {
210+ let flow = flows[ & ( parent. clone ( ) , commodity_id. clone ( ) , time_slice. clone ( ) ) ] ;
211+ flows. insert (
212+ ( asset. clone ( ) , commodity_id. clone ( ) , time_slice. clone ( ) ) ,
213+ flow,
214+ ) ;
215+ }
216+ }
217+ }
218+ }
219+
220+ // Remove all the parent assets
221+ flows. retain ( |( asset, _, _) , _| !asset. is_parent ( ) ) ;
222+
223+ flows
224+ }
225+
182226/// The solution to the dispatch optimisation problem
183227#[ allow( clippy:: struct_field_names) ]
184228pub struct Solution < ' a > {
185229 solution : highs:: Solution ,
186230 variables : VariableMap ,
187231 time_slice_info : & ' a TimeSliceInfo ,
188232 constraint_keys : ConstraintKeys ,
233+ flow_map : Cell < Option < FlowMap > > ,
189234 /// The objective value for the solution
190235 pub objective_value : Money ,
191236}
@@ -195,19 +240,13 @@ impl Solution<'_> {
195240 ///
196241 /// Note that this only includes commodity flows which relate to existing assets, so not every
197242 /// commodity in the simulation will necessarily be represented.
243+ ///
244+ /// Note: The flow map is actually already created and is taken from `self` when this method is
245+ /// called (hence it can only be called once). The reason for this is because we need to convert
246+ /// back from parent assets to child assets. We can remove this hack once we have updated all
247+ /// the users of this interface to be able to handle parent assets correctly.
198248 pub fn create_flow_map ( & self ) -> FlowMap {
199- // The decision variables represent assets' activity levels, not commodity flows. We
200- // multiply this value by the flow coeffs to get commodity flows.
201- let mut flows = FlowMap :: new ( ) ;
202- for ( asset, time_slice, activity) in self . iter_activity_for_existing ( ) {
203- for flow in asset. iter_flows ( ) {
204- let flow_key = ( asset. clone ( ) , flow. commodity . id . clone ( ) , time_slice. clone ( ) ) ;
205- let flow_value = activity * flow. coeff ;
206- flows. insert ( flow_key, flow_value) ;
207- }
208- }
209-
210- flows
249+ self . flow_map . take ( ) . expect ( "Flow map already created" )
211250 }
212251
213252 /// Activity for all assets (existing and candidate, if present)
@@ -381,6 +420,21 @@ fn filter_input_prices(
381420 . collect ( )
382421}
383422
423+ /// Get the parent for each asset.
424+ ///
425+ /// Child assets are converted to their parents and non-divisible assets are returned as is. Each
426+ /// parent asset is returned only once.
427+ fn convert_assets_to_parents ( assets : & [ AssetRef ] ) -> impl Iterator < Item = AssetRef > {
428+ let mut parents = HashSet :: new ( ) ;
429+ assets
430+ . iter ( )
431+ . filter_map ( move |asset| match asset. parent ( ) {
432+ Some ( parent) => parents. insert ( parent. clone ( ) ) . then_some ( parent) ,
433+ None => Some ( asset) ,
434+ } )
435+ . cloned ( )
436+ }
437+
384438/// Provides the interface for running the dispatch optimisation.
385439///
386440/// The run will attempt to meet unmet demand: if the solver reports infeasibility
@@ -557,13 +611,15 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
557611 allow_unmet_demand : bool ,
558612 input_prices : Option < & CommodityPrices > ,
559613 ) -> Result < Solution < ' model > , ModelError > {
614+ let parent_assets = convert_assets_to_parents ( self . existing_assets ) . collect_vec ( ) ;
615+
560616 // Set up problem
561617 let mut problem = Problem :: default ( ) ;
562618 let mut variables = VariableMap :: new_with_activity_vars (
563619 & mut problem,
564620 self . model ,
565621 input_prices,
566- self . existing_assets ,
622+ & parent_assets ,
567623 self . candidate_assets ,
568624 self . year ,
569625 ) ;
@@ -577,7 +633,7 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
577633 // Check flexible capacity assets is a subset of existing assets
578634 for asset in self . flexible_capacity_assets {
579635 assert ! (
580- self . existing_assets . contains( asset) ,
636+ parent_assets . contains( asset) ,
581637 "Flexible capacity assets must be a subset of existing assets. Offending asset: {asset:?}"
582638 ) ;
583639 }
@@ -594,7 +650,7 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
594650 }
595651
596652 // Add constraints
597- let all_assets = chain ( self . existing_assets . iter ( ) , self . candidate_assets . iter ( ) ) ;
653+ let all_assets = chain ( parent_assets . iter ( ) , self . candidate_assets . iter ( ) ) ;
598654 let constraint_keys = add_model_constraints (
599655 & mut problem,
600656 & variables,
@@ -607,13 +663,20 @@ impl<'model, 'run> DispatchRun<'model, 'run> {
607663 // Solve model
608664 let solution = solve_optimal ( problem. optimise ( Sense :: Minimise ) ) ?;
609665
610- Ok ( Solution {
666+ let solution = Solution {
611667 solution : solution. get_solution ( ) ,
612668 variables,
613669 time_slice_info : & self . model . time_slice_info ,
614670 constraint_keys,
671+ flow_map : Cell :: default ( ) ,
615672 objective_value : Money ( solution. objective_value ( ) ) ,
616- } )
673+ } ;
674+ solution. flow_map . set ( Some ( create_flow_map (
675+ self . existing_assets ,
676+ & self . model . time_slice_info ,
677+ solution. iter_activity ( ) ,
678+ ) ) ) ;
679+ Ok ( solution)
617680 }
618681}
619682
0 commit comments