5555SVS_FILE = Path ("/mnt/data/input/svs/SVS_v1_0.5_trans_v17.nc" )
5656SVS_EXCLUDE_JSON = Path ("/mnt/data/input/svs/reachids_svs_gages_used_to_train.json" )
5757
58+ FLPE_MOI_ALGOS = [
59+ "metroman" ,
60+ "busboi" ,
61+ "hivdi" ,
62+ "momma" ,
63+ "sad" ,
64+ "sic4dvar" ,
65+ "consensus" ,
66+ ]
67+
68+ MOI_BASE_ALGOS = [
69+ "metroman" ,
70+ "busboi" ,
71+ "hivdi" ,
72+ "momma" ,
73+ "sad" ,
74+ "sic4dvar" ,
75+ ]
76+
77+
5878class ValidationConfluence :
5979 """Class that runs validation operations for Confluence workflow.
6080
@@ -102,7 +122,7 @@ class ValidationConfluence:
102122 """
103123
104124 INT_FILL = - 999
105- NUM_ALGOS = 8 # flpe/moi: metroman, neobam , busboi, hivdi, momma, sad, sic4dvar, consensus
125+ NUM_ALGOS = len ( FLPE_MOI_ALGOS ) # flpe/moi: metroman, busboi, hivdi, momma, sad, sic4dvar, consensus
106126 NUM_ALGOS_OFFLINE = 16
107127
108128 def __init__ (self , reach_data , flpe_dir , moi_dir , offline_dir , input_dir , output_dir , run_type , gage_dir , svs_file = None , exclude_json = None ):
@@ -379,31 +399,38 @@ def read_moi_data(self, moi_dir):
379399 moi_file = f"{ moi_dir } /{ self .reach_id } _integrator.nc"
380400 moi = Dataset (moi_file , 'r' )
381401 moi_data = {}
382- moi_data ["metroman" ] = moi ["metroman/q" ][:].filled (np .nan )
383- moi_data ["neobam" ] = moi ["neobam/q" ][:].filled (np .nan )
384- moi_data ["busboi" ] = moi ["busboi/q" ][:].filled (np .nan )
385- moi_data ["hivdi" ] = moi ["hivdi/q" ][:].filled (np .nan )
386- moi_data ["momma" ] = moi ["momma/q" ][:].filled (np .nan )
387- moi_data ["sad" ] = moi ["sad/q" ][:].filled (np .nan )
388- moi_data ["sic4dvar" ] = moi ["sic4dvar/q" ][:].filled (np .nan )
389- moi_data ["consensus" ] = moi ["consensus/q" ][:].filled (np .nan )
402+
403+ def safe_read_q (group_name ):
404+ if group_name in moi .groups and "q" in moi [group_name ].variables :
405+ return moi [f"{ group_name } /q" ][:].filled (np .nan )
406+ return - 9999
407+
408+ for algo in MOI_BASE_ALGOS :
409+ moi_data [algo ] = safe_read_q (algo )
410+
390411 moi .close ()
391-
392- #create pre-offline consensus
393- ALLQ = np .full ((len (moi_data .keys ()), len (moi_data ["metroman" ])), np .nan )
394- for row in range (len (moi_data .keys ())):
395- ALGv = moi_data [list (moi_data .keys ())[row ]]
396- ALGv [ALGv < 0 ] = np .nan
397- ALLQ [row , :] = ALGv
398-
399- consensus = np .nanmedian (ALLQ , axis = 0 )
400- moi_data ["consensus" ] = consensus
401-
412+
413+ # MOI output does not write a consensus group, so compute it here
414+ # from the available basin-scale algorithm discharge series.
415+ ref_key = next ((k for k in MOI_BASE_ALGOS if not np .isscalar (moi_data [k ])), None )
416+ if ref_key is not None :
417+ allq = np .full ((len (MOI_BASE_ALGOS ), len (moi_data [ref_key ])), np .nan )
418+ for row , algo in enumerate (MOI_BASE_ALGOS ):
419+ algv = moi_data [algo ]
420+ if np .isscalar (algv ) and algv == - 9999 :
421+ continue
422+ algv = algv .copy ()
423+ algv [algv < 0 ] = np .nan
424+ allq [row , :] = algv
425+ moi_data ["consensus" ] = np .nanmedian (allq , axis = 0 )
426+ else :
427+ moi_data ["consensus" ] = - 9999
428+
402429 if self .is_moi_valid (moi_data ):
403430 return moi_data
404- else :
431+ else :
405432 return {}
406-
433+
407434 def is_moi_valid (self , moi_data ):
408435 """Check if moi data is only comprised of NaN values.
409436
@@ -437,7 +464,6 @@ def read_flpe_data(self, flpe_dir):
437464 """
438465 convention_dict = {
439466 "metroman" : "average/allq" ,
440- "neobam" : "q/q" ,
441467 "busboi" : "q/q" ,
442468 "hivdi" : "reach/Q" ,
443469 "momma" : "Q" ,
@@ -446,107 +472,39 @@ def read_flpe_data(self, flpe_dir):
446472 "consensus" : "consensus_q" ,
447473 }
448474
449- flpe_file_metroman = f"{ flpe_dir } /{ 'metroman' } /{ self .reach_id } _metroman.nc"
450- flpe_file_neobam = f"{ flpe_dir } /{ 'geobam' } /{ self .reach_id } _geobam.nc"
451- flpe_file_busboi = f"{ flpe_dir } /{ 'busboi' } /{ self .reach_id } _busboi.nc"
452- flpe_file_momma = f"{ flpe_dir } /{ 'momma' } /{ self .reach_id } _momma.nc"
453- flpe_file_sad = f"{ flpe_dir } /{ 'sad' } /{ self .reach_id } _sad.nc"
454- flpe_file_sic4dvar = f"{ flpe_dir } /{ 'sic4dvar' } /{ self .reach_id } _sic4dvar.nc"
455- flpe_file_hivdi = f"{ flpe_dir } /{ 'hivdi' } /{ self .reach_id } _hivdi.nc"
456- flpe_file_consensus = f"{ flpe_dir } /{ 'consensus' } /{ self .reach_id } _consensus.nc"
457- try :
458- flpe_mm = Dataset (flpe_file_metroman , 'r' )
459- except :
460- flpe_mm = - 9999
461- try :
462- flpe_nb = Dataset (flpe_file_neobam , 'r' )
463- except :
464- flpe_nb = - 9999
465- try :
466- flpe_bb = Dataset (flpe_file_busboi , 'r' )
467- except :
468- flpe_bb = - 9999
469- try :
470- flpe_hi = Dataset (flpe_file_hivdi , 'r' )
471- except :
472- flpe_hi = - 9999
473- try :
474- flpe_mo = Dataset (flpe_file_momma , 'r' )
475- except :
476- flpe_mo = - 9999
477- try :
478- flpe_sa = Dataset (flpe_file_sad , 'r' )
479- except :
480- flpe_sa = - 9999
481- try :
482- flpe_si = Dataset (flpe_file_sic4dvar , 'r' )
483- except :
484- flpe_si = - 9999
485- try :
486- flpe_cons = Dataset (flpe_file_consensus , 'r' )
487- except :
488- flpe_cons = - 9999
489-
475+ flpe_file_map = {
476+ "metroman" : f"{ flpe_dir } /metroman/{ self .reach_id } _metroman.nc" ,
477+ "busboi" : f"{ flpe_dir } /busboi/{ self .reach_id } _busboi.nc" ,
478+ "hivdi" : f"{ flpe_dir } /hivdi/{ self .reach_id } _hivdi.nc" ,
479+ "momma" : f"{ flpe_dir } /momma/{ self .reach_id } _momma.nc" ,
480+ "sad" : f"{ flpe_dir } /sad/{ self .reach_id } _sad.nc" ,
481+ "sic4dvar" : f"{ flpe_dir } /sic4dvar/{ self .reach_id } _sic4dvar.nc" ,
482+ "consensus" : f"{ flpe_dir } /consensus/{ self .reach_id } _consensus.nc" ,
483+ }
484+
490485 flpe_data = {}
491486 conlen = 0
492-
493- if flpe_mm == - 9999 :
494- flpe_data ["metroman" ] = - 9999
495- else :
496- flpe_data ["metroman" ] = flpe_mm [convention_dict ["metroman" ]][:].filled (np .nan )
497- conlen = len (flpe_data ["metroman" ])
498- flpe_mm .close ()
499- if flpe_nb == - 9999 :
500- flpe_data ["neobam" ] = - 9999
501- else :
502- flpe_data ["neobam" ] = flpe_nb [convention_dict ["neobam" ]][:].filled (np .nan )
503- conlen = len (flpe_data ["neobam" ])
504- flpe_nb .close ()
505- if flpe_bb == - 9999 :
506- flpe_data ["busboi" ] = - 9999
507- else :
508- flpe_data ["busboi" ] = flpe_bb [convention_dict ["busboi" ]][:].filled (np .nan )
509- conlen = len (flpe_data ["busboi" ])
510- flpe_bb .close ()
511- if flpe_hi == - 9999 :
512- flpe_data ["hivdi" ] = - 9999
513- else :
514- flpe_data ["hivdi" ] = flpe_hi [convention_dict ["hivdi" ]][:].filled (np .nan )
515- conlen = len (flpe_data ["hivdi" ])
516- flpe_hi .close ()
517- if flpe_mo == - 9999 :
518- flpe_data ["momma" ] = - 9999
519- else :
520- flpe_data ["momma" ] = flpe_mo [convention_dict ["momma" ]][:].filled (np .nan )
521- conlen = len (flpe_data ["momma" ])
522- flpe_mo .close ()
523- if flpe_sa == - 9999 :
524- flpe_data ["sad" ] = - 9999
525- else :
526- flpe_data ["sad" ] = flpe_sa [convention_dict ["sad" ]][:].filled (np .nan )
527- conlen = len (flpe_data ["sad" ])
528- flpe_sa .close ()
529- if flpe_si == - 9999 :
530- flpe_data ["sic4dvar" ] = - 9999
531- else :
532- flpe_data ["sic4dvar" ] = flpe_si [convention_dict ["sic4dvar" ]][:].filled (np .nan )
533- conlen = len (flpe_data ["sic4dvar" ])
534- flpe_si .close ()
535- if flpe_cons == - 9999 :
536- flpe_data ["consensus" ] = - 9999
537- else :
538- flpe_data ["consensus" ] = flpe_cons [convention_dict ["consensus" ]][:].filled (np .nan )
539- conlen = len (flpe_data ["consensus" ])
540- flpe_cons .close ()
487+
488+ for algo in FLPE_MOI_ALGOS :
489+ flpe_file = flpe_file_map [algo ]
490+ try :
491+ flpe_ds = Dataset (flpe_file , 'r' )
492+ except Exception :
493+ flpe_data [algo ] = - 9999
494+ continue
495+
496+ flpe_data [algo ] = flpe_ds [convention_dict [algo ]][:].filled (np .nan )
497+ conlen = len (flpe_data [algo ])
498+ flpe_ds .close ()
541499
542500 if conlen > 0 :
543501 if self .is_flpe_valid (flpe_data ):
544502 return flpe_data
545- else :
503+ else :
546504 return {}
547505 else :
548506 return {}
549-
507+
550508 def is_flpe_valid (self , flpe_data ):
551509 """Check if flpe data is only comprised of NaN values.
552510
@@ -689,7 +647,7 @@ def validate(self):
689647 "n" : np .full (algo_dim , fill_value = - 9999 ),
690648 "nRMSE" : np .full (algo_dim , fill_value = - 9999 ),
691649 "nBIAS" : np .full (algo_dim , fill_value = - 9999 ),
692- "t" : np .full (( self . NUM_ALGOS ) , fill_value = - 9999 ),
650+ "t" : np .full (Tdim , fill_value = - 9999 ),
693651 "consensus" : np .full (Tdim , fill_value = - 9999 ),
694652 }
695653
@@ -722,7 +680,7 @@ def validate(self):
722680 "n" : np .full (algo_dim , fill_value = - 9999 ),
723681 "nRMSE" : np .full (algo_dim , fill_value = - 9999 ),
724682 "nBIAS" : np .full (algo_dim , fill_value = - 9999 ),
725- "t" : np .full (( self . NUM_ALGOS ) , fill_value = - 9999 ),
683+ "t" : np .full (Tdim , fill_value = - 9999 ),
726684 "consensus" : np .full (Tdim , fill_value = - 9999 ),
727685 }
728686
@@ -751,7 +709,7 @@ def validate(self):
751709 "n" : np .full ((self .NUM_ALGOS_OFFLINE ), fill_value = - 9999 ),
752710 "nRMSE" : np .full ((self .NUM_ALGOS_OFFLINE ), fill_value = - 9999 ),
753711 "nBIAS" : np .full ((self .NUM_ALGOS_OFFLINE ), fill_value = - 9999 ),
754- "t" : np .full (( self . NUM_ALGOS_OFFLINE ) , fill_value = - 9999 ),
712+ "t" : np .full (Tdim , fill_value = - 9999 ),
755713 "consensus" : np .full (Tdim , fill_value = - 9999 ),
756714 }
757715
@@ -819,15 +777,25 @@ def safe_gid(gid_array):
819777 out .has_validation_o = 0 if np .where (stats_O ["algorithm" ] == "" )[0 ].size == self .NUM_ALGOS_OFFLINE else 1
820778 out .gage_type = gage_type .upper ()
821779
822- # Separate fixed dimensions for flpe/moi (8) and offline (16)
823- out .createDimension ("num_algos_flpe" , self .NUM_ALGOS ) # 8
824- out .createDimension ("num_algos_offline" , self .NUM_ALGOS_OFFLINE ) # 16
780+ # Separate fixed dimensions for flpe/moi and offline
781+ out .createDimension ("num_algos_flpe" , self .NUM_ALGOS )
782+ out .createDimension ("num_algos_offline" , self .NUM_ALGOS_OFFLINE )
825783 c_dim_flpe = out .createDimension ("nchar_flpe" , None )
826784 c_dim_gage = out .createDimension ("nchar_gage" , None )
827- t_dim = out .createDimension ("time" , len (stats_flpe ["t" ]))
785+
786+ time_values = None
787+ for stats_dict in (stats_flpe , stats_moi , stats_O ):
788+ candidate = np .asarray (stats_dict ["t" ])
789+ if candidate .size > 0 and not np .all (np .isclose (candidate , empty )):
790+ time_values = candidate
791+ break
792+ if time_values is None :
793+ time_values = np .array ([], dtype = int )
794+
795+ t_dim = out .createDimension ("time" , len (time_values ))
828796 t_v_flpe = out .createVariable ("time" , "i4" , ("time" ,))
829- t_v_flpe .units = "days since Jan 1 Year 1"
830- t_v_flpe [:] = stats_flpe [ "t" ]
797+ t_v_flpe .units = "days since Jan 1 Year 1"
798+ t_v_flpe [:] = time_values
831799
832800 # --- FLPE variables (use num_algos_flpe) ---
833801 if FLPEno == False :
0 commit comments