Skip to content

Commit 63bcf24

Browse files
authored
Offline patch for sonde timeOffset (NOAA-EMC#538)
In NOAA-EMC#496, it was shown that GSI applies special logic in `read_prepbufr.F90` that overrides the twindow specified in the convinfo table for obs with balloon drift information. Essentially, GSI will attempt a fallback time (-3600s) if the original time falls outside the allowed window (5400s), and rejects the observation if the fallback still fails the new window check. This doesn't usually impact the 00/12z cycles since those are launch times. The impact from this change is usually noted in 01/13z cycles where the timeOffset can be -7500s (outside the -5400s time window) in which case, those obs gets snapped to -3600s. This PR adds this capability to change the timeOffset of the data via the `offline_ioda_patch.py` utility. Here is an example call to use this new function:
1 parent 6e3dbe8 commit 63bcf24

1 file changed

Lines changed: 61 additions & 1 deletion

File tree

rrfs-test/IODA/offline_ioda_patch.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def toc(tic=tic, label=""):
3636

3737
parser = argparse.ArgumentParser()
3838
parser.add_argument('-o', '--obs', type=str, help='ioda observation file', required=True)
39+
parser.add_argument('--patch-timeoffset', action='store_true',
40+
help='Patch MetaData/timeOffset for soundings (ObsType 120/220)')
41+
parser.add_argument('--to-min', type=float, default=-5400.0)
42+
parser.add_argument('--to-max', type=float, default=5400.0)
43+
parser.add_argument('--to-set', type=float, default=3600.0,
44+
help='Magnitude to set timeOffset to (sign preserved)')
45+
3946
args = parser.parse_args()
4047

4148
# Assign filenames
@@ -83,7 +90,7 @@ def toc(tic=tic, label=""):
8390
g.createVariable(var, vartype, 'Location', fill_value=fill)
8491
except (AttributeError, KeyError): # String variables
8592
g.createVariable(var, 'str', 'Location')
86-
#
93+
8794
if qc_group and vartype == "int32":
8895
np_invar = np.array(invar)
8996
np_invar[(np_invar < 0) | (np_invar > 15)] = 15
@@ -93,6 +100,7 @@ def toc(tic=tic, label=""):
93100
g.variables[var][:] = invar[:][:].data
94101
else:
95102
g.variables[var][:] = invar[:][:]
103+
96104
# Copy attributes for this variable
97105
for attr in invar.ncattrs():
98106
if '_FillValue' in attr:
@@ -125,6 +133,58 @@ def toc(tic=tic, label=""):
125133
metadata_group.createVariable(f"{var}", 'f4', 'Location', fill_value=fill)
126134
metadata_group.variables[f"{var}"][:] = data
127135

136+
# patch MetaData/timeOffset for soundings only (ObsType 120 or 220), hard-coded
137+
if args.patch_timeoffset:
138+
md = fout.groups.get("MetaData", None)
139+
ot = fout.groups.get("ObsType", None)
140+
141+
if md is None or ot is None:
142+
print("patch-timeoffset requested but MetaData or ObsType group missing; skipping")
143+
elif "timeOffset" not in md.variables:
144+
print("patch-timeoffset requested but MetaData/timeOffset missing; skipping")
145+
else:
146+
to_var = md.variables["timeOffset"]
147+
to = to_var[:].astype(np.float64)
148+
149+
# Save original for debugging
150+
if "origTimeOffset" not in md.variables:
151+
md.createVariable("origTimeOffset", "f4", "Location", fill_value=np.float32(3.402823e38))
152+
md.variables["origTimeOffset"].long_name = "Original timeOffset before offline patch"
153+
md.variables["origTimeOffset"].units = getattr(to_var, "units", "s")
154+
md.variables["origTimeOffset"][:] = to.astype(np.float32)
155+
156+
# Build sounding mask: true if ANY ObsType/* is 120 or 220 at that Location
157+
sounding = np.zeros(to.shape, dtype=bool)
158+
for vname in ot.variables:
159+
v = ot.variables[vname]
160+
if getattr(v, "dimensions", ()) != ("Location",):
161+
continue
162+
otv = v[:]
163+
164+
# Exclude fill values if present
165+
try:
166+
fv = v.getncattr("_FillValue")
167+
good = (otv != fv)
168+
except Exception:
169+
good = np.ones(otv.shape, dtype=bool)
170+
171+
sounding |= (good & ((otv == 120) | (otv == 220)))
172+
173+
# Outside bounds?
174+
mask_lo = sounding & (to < float(args.to_min))
175+
mask_hi = sounding & (to > float(args.to_max))
176+
177+
# Patch: preserve sign, set magnitude to to_set
178+
to[mask_lo] = -abs(float(args.to_set))
179+
to[mask_hi] = abs(float(args.to_set))
180+
181+
to_var[:] = to.astype(to_var.dtype)
182+
183+
n_type = int(np.count_nonzero(np.ma.filled(sounding, False)))
184+
n_adj = int(np.count_nonzero(np.ma.filled(mask_lo, False)) + np.count_nonzero(np.ma.filled(mask_hi, False)))
185+
186+
print(f"Patched MetaData/timeOffset for soundings: matched={n_type}, adjusted={n_adj}")
187+
128188
# Close the datasets
129189
obs_ds.close()
130190
fout.close()

0 commit comments

Comments
 (0)