Skip to content

Commit af1d6ce

Browse files
authored
Merge pull request #33 from RaidoYAGAMI/main
Remove neobam from validation and add missing runtime dependencies
2 parents 95628e5 + a726a32 commit af1d6ce

2 files changed

Lines changed: 99 additions & 125 deletions

File tree

‎Singularity.def‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,11 @@ From: ghcr.io/swot-confluence/validation:latest
66
sos_read/. /app/sos_read/
77
validation_confluence.py /app/validation_confluence.py
88

9+
%post
10+
/app/env/bin/python3 -m pip install --no-cache-dir --upgrade pip
11+
/app/env/bin/python3 -m pip install --no-cache-dir \
12+
numpy scipy netCDF4 matplotlib seaborn \
13+
boto3 botocore s3transfer jmespath
14+
915
%runscript
10-
exec /app/env/bin/python3 /app/validation_confluence.py "$@"
16+
exec /app/env/bin/python3 /app/validation_confluence.py "$@"

‎validation_confluence.py‎

Lines changed: 92 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@
5555
SVS_FILE = Path("/mnt/data/input/svs/SVS_v1_0.5_trans_v17.nc")
5656
SVS_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+
5878
class 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

Comments
 (0)