@@ -157,33 +157,66 @@ fn validate_commodities(
157157 milestone_years : & [ u32 ] ,
158158 time_slice_info : & TimeSliceInfo ,
159159) -> Result < ( ) > {
160- for ( commodity, region_id, year) in iproduct ! (
161- commodities. values( ) ,
162- region_ids. iter( ) ,
163- milestone_years. iter( ) . copied( ) ,
164- ) {
165- match commodity. kind {
166- CommodityType :: SupplyEqualsDemand => {
167- validate_sed_commodity ( & commodity. id , flows, region_id, year) ?;
168- }
169- CommodityType :: ServiceDemand => {
170- for ts_selection in
171- time_slice_info. iter_selections_for_level ( commodity. time_slice_level )
172- {
173- validate_svd_commodity (
174- time_slice_info,
175- commodity,
176- flows,
177- availabilities,
178- region_id,
179- year,
180- & ts_selection,
181- ) ?;
160+ for commodity in commodities. values ( ) {
161+ if commodity. kind == CommodityType :: Other {
162+ validate_other_commodity ( & commodity. id , flows) ?;
163+ continue ;
164+ }
165+
166+ for ( region_id, year) in iproduct ! ( region_ids. iter( ) , milestone_years. iter( ) . copied( ) ) {
167+ match commodity. kind {
168+ CommodityType :: SupplyEqualsDemand => {
169+ validate_sed_commodity ( & commodity. id , flows, region_id, year) ?;
182170 }
171+ CommodityType :: ServiceDemand => {
172+ for ts_selection in
173+ time_slice_info. iter_selections_for_level ( commodity. time_slice_level )
174+ {
175+ validate_svd_commodity (
176+ time_slice_info,
177+ commodity,
178+ flows,
179+ availabilities,
180+ region_id,
181+ year,
182+ & ts_selection,
183+ ) ?;
184+ }
185+ }
186+ _ => unreachable ! ( ) ,
183187 }
184- _ => { }
185188 }
186189 }
190+
191+ Ok ( ( ) )
192+ }
193+
194+ /// Check that commodities of type other are either produced or consumed but not both
195+ fn validate_other_commodity (
196+ commodity_id : & CommodityID ,
197+ flows : & HashMap < ProcessID , ProcessFlowsMap > ,
198+ ) -> Result < ( ) > {
199+ let mut is_producer = None ;
200+ for flows in flows. values ( ) . flat_map ( |flows| flows. values ( ) ) {
201+ if let Some ( flow) = flows. get ( commodity_id) {
202+ let cur_is_producer = flow. coeff > 0.0 ;
203+ if let Some ( is_producer) = is_producer {
204+ ensure ! (
205+ is_producer == cur_is_producer,
206+ "{commodity_id} is both a producer and consumer. \
207+ Commodities of type 'other' must only be consumed or produced."
208+ ) ;
209+ } else {
210+ is_producer = Some ( cur_is_producer) ;
211+ }
212+ }
213+ }
214+
215+ ensure ! (
216+ is_producer. is_some( ) ,
217+ "Commodity {commodity_id} is neither produced or consumed."
218+ ) ;
219+
187220 Ok ( ( ) )
188221}
189222
@@ -443,4 +476,90 @@ mod tests {
443476 for region GBR in year 2010 and time slice(s) winter.day"
444477 ) ;
445478 }
479+
480+ #[ fixture]
481+ fn commodity_other ( ) -> Commodity {
482+ Commodity {
483+ id : "commodity_other" . into ( ) ,
484+ description : "Other commodity" . into ( ) ,
485+ kind : CommodityType :: Other ,
486+ time_slice_level : TimeSliceLevel :: Annual ,
487+ levies : CommodityLevyMap :: new ( ) ,
488+ demand : DemandMap :: new ( ) ,
489+ }
490+ }
491+
492+ #[ fixture]
493+ fn producer_flows ( commodity_other : Commodity ) -> ProcessFlowsMap {
494+ ProcessFlowsMap :: from_iter ( vec ! [ (
495+ ( "GBR" . into( ) , 2010 ) ,
496+ indexmap! { commodity_other. id. clone( ) => ProcessFlow {
497+ commodity: commodity_other. into( ) ,
498+ coeff: 10.0 ,
499+ kind: FlowType :: Fixed ,
500+ cost: 1.0
501+ } } ,
502+ ) ] )
503+ }
504+
505+ #[ fixture]
506+ fn consumer_flows ( commodity_other : Commodity ) -> ProcessFlowsMap {
507+ ProcessFlowsMap :: from_iter ( vec ! [ (
508+ ( "GBR" . into( ) , 2010 ) ,
509+ indexmap! { commodity_other. id. clone( ) => ProcessFlow {
510+ commodity: commodity_other. into( ) ,
511+ coeff: -10.0 ,
512+ kind: FlowType :: Fixed ,
513+ cost: 1.0
514+ } } ,
515+ ) ] )
516+ }
517+
518+ #[ rstest]
519+ fn test_validate_other_commodity_valid_producer (
520+ commodity_other : Commodity ,
521+ producer_flows : ProcessFlowsMap ,
522+ ) {
523+ // Valid scenario: commodity is only produced
524+ let flows = HashMap :: from_iter ( vec ! [ ( "process1" . into( ) , producer_flows) ] ) ;
525+ assert ! ( validate_other_commodity( & commodity_other. id, & flows) . is_ok( ) ) ;
526+ }
527+
528+ #[ rstest]
529+ fn test_validate_other_commodity_valid_consumer (
530+ commodity_other : Commodity ,
531+ consumer_flows : ProcessFlowsMap ,
532+ ) {
533+ // Valid scenario: commodity is only consumed
534+ let flows = HashMap :: from_iter ( vec ! [ ( "process1" . into( ) , consumer_flows) ] ) ;
535+ assert ! ( validate_other_commodity( & commodity_other. id, & flows) . is_ok( ) ) ;
536+ }
537+
538+ #[ rstest]
539+ fn test_validate_other_commodity_invalid_both (
540+ commodity_other : Commodity ,
541+ producer_flows : ProcessFlowsMap ,
542+ consumer_flows : ProcessFlowsMap ,
543+ ) {
544+ // Invalid scenario: commodity is both produced and consumed
545+ let flows = HashMap :: from_iter ( vec ! [
546+ ( "process1" . into( ) , producer_flows) ,
547+ ( "process2" . into( ) , consumer_flows) ,
548+ ] ) ;
549+ assert_error ! (
550+ validate_other_commodity( & commodity_other. id, & flows) ,
551+ "commodity_other is both a producer and consumer. \
552+ Commodities of type 'other' must only be consumed or produced."
553+ ) ;
554+ }
555+
556+ #[ rstest]
557+ fn test_validate_other_commodity_invalid_neither ( commodity_other : Commodity ) {
558+ // Invalid scenario: commodity is neither produced nor consumed
559+ let flows = HashMap :: new ( ) ;
560+ assert_error ! (
561+ validate_other_commodity( & commodity_other. id, & flows) ,
562+ "Commodity commodity_other is neither produced or consumed."
563+ ) ;
564+ }
446565}
0 commit comments