Skip to content

Commit 1b4c6f6

Browse files
EliEli
authored andcommitted
Changed OH4 estimate to consider SJR flow to improve estimates in flooding. Tidied up some code.
1 parent 3641aae commit 1b4c6f6

1 file changed

Lines changed: 85 additions & 51 deletions

File tree

bdschism/bdschism/ccf_gate_height.py

Lines changed: 85 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
import datetime as dtm
1010
import pandas as pd
1111
from vtools.functions.unit_conversions import M2FT, FT2M, CMS2CFS
12-
from vtools import days, hours,minutes
12+
from vtools import days, hours ,minutes, divide_interval
1313
from dms_datastore.read_ts import read_ts
1414
from dms_datastore.read_multi import read_ts_repo
1515
from vtools.functions.filter import cosine_lanczos
1616
from vtools.functions.merge import ts_merge
1717
from vtools.functions.coarsen import ts_coarsen
18+
1819
import schimpy.param as parms
1920
from schimpy.th_io import read_th
2021
import numpy as np
@@ -23,13 +24,26 @@
2324
import math
2425
import matplotlib.pyplot as plt
2526
import click
27+
import glob
28+
29+
30+
Q_NORM = 5000.0
31+
32+
OH4_SF_COEF = 1.057768
33+
OH4_SJR_EXCESS_COEF = 0.237763
34+
OH4_SJR_THRESHOLD = 2500.0
2635

36+
# Fixed intercept from the fit. This replaces per-window mean removal.
37+
OH4_SUB_INTERCEPT = -3.716825
2738

2839
ccf_A = 91868000 # area of ccf forbay above 0 navd 88 in ft^2
2940
ccf_reference_level = 2.0 # navd 88 in ft
30-
# this is the phase shift to match shiftted sffpx tide used for
31-
# gate opening priority system used in DSM2
32-
#sffpx_level_shift_h = hours(8)+minutes(30)
41+
42+
# NOTE:
43+
# SFFPX is shifted by ~8.5 hours here to align tidal phase for priority logic.
44+
# The same shifted series is passed into OH4 prediction, where an additional
45+
# ~150-minute lag is applied to the subtidal component. Total effective shift
46+
# for OH4 subtidal is therefore ~11 hours.
3347
sffpx_level_shift_h = minutes(30+8*60)
3448

3549
def tlmax(arr):
@@ -127,15 +141,16 @@ def make_priorities(input_tide, stime, etime, save_intermediate=False):
127141
sframe = s.to_frame()
128142
else:
129143
sframe = s
130-
144+
131145
# Find minimum and maximums
132146
sh, sl = tidalhl.get_tidal_hl(sframe) # Get Tidal highs and lows
133147
sh = pd.concat([sh, get_tidal_hh_lh(sh)], axis=1)
134148
sh.columns = ["max", "max_name"]
135149
sl = pd.concat([sl, get_tidal_ll_hl(sl)], axis=1)
136150
sl.columns = ["min", "min_name"]
137151

138-
# -------- flagg open and close portions - OG Priority 3
152+
153+
# -------- flag open and close portions - OG Priority 3
139154
# when it opens
140155
idx1 = sl[sl["min_name"] == "LL"].index + pd.Timedelta("1h")
141156
idx2 = sh[sh["max_name"] == "HH"].index - pd.Timedelta("1h")
@@ -145,6 +160,7 @@ def make_priorities(input_tide, stime, etime, save_intermediate=False):
145160
ci = idx1.union(idx2).union(idx3)
146161
opens = pd.DataFrame(data=np.ones(len(ci)), index=ci)
147162

163+
148164
# when it closes
149165
idx1 = sl[sl["min_name"] == "HL"].index + pd.Timedelta("2h")
150166
idx2 = sl[sl["min_name"] == "LL"].index - pd.Timedelta("2h")
@@ -159,7 +175,7 @@ def make_priorities(input_tide, stime, etime, save_intermediate=False):
159175
prio3_ts = prio3_ts[prio3_ts[3] != prio3_ts[3].shift()]
160176
prio3_ts.head()
161177

162-
# ------- flagg open and close portions - Priority 2
178+
# ------- flag open and close portions - Priority 2
163179

164180
# when it opens
165181
idx1 = sl[sl["min_name"] == "LL"].index + pd.Timedelta("1h")
@@ -184,7 +200,7 @@ def make_priorities(input_tide, stime, etime, save_intermediate=False):
184200
prio2_ts = prio2_ts[prio2_ts[2] != prio2_ts[2].shift()]
185201
prio2_ts.head()
186202

187-
# ------ flagg open and close portions - Priority 1
203+
# ------ flag open and close portions - Priority 1
188204

189205
# when it opens
190206
idx1 = sh[sh["max_name"] == "LH"].index + pd.Timedelta("1h")
@@ -295,41 +311,26 @@ def gen_prio_for_varying_exports(input_tide, export_df):
295311
return priority_df, max_gate_height
296312

297313

298-
def get_export_ts_cfs(s1, s2, flux):
314+
def get_flux_ts_cfs(s1, s2, flux):
299315
"""
300-
retrieve swp and cvp pumping rate from a SCHISM flux.th
301-
302-
Parameters
303-
----------
304-
s1 : :py:class:'datetime <datetime.datetime>'
305-
start time.
306-
s2 : :py:class:'datetime <datetime.datetime>'
307-
end time.
308-
flux : str
309-
path to the SCHISM flux th file.
310-
311-
Returns
312-
-------
313-
swp_ts : :py:class:'DataFrame <pandas.DataFrame>'
314-
swp pump rate in cfs.
315-
cvp_ts : :py:class:'DataFrame <pandas.DataFrame>'
316-
cvp pump rate in cfs.
316+
Retrieve SWP, CVP, and SJR flows from a SCHISM flux.th file in cfs.
317317
318+
SJR is sign-adjusted so positive values represent San Joaquin River inflow.
318319
"""
319-
# flux = "//cnrastore-bdo/SCHISM/studies/itp202411/th_files/repo/flux_20241213.th"
320-
flux_ts = read_th(flux)
321-
# flux_ts = pd.read_csv(flux, parse_dates=True, index_col=0, sep=r"\s+")
322-
swp_ts = flux_ts["swp"][s1:s2] * CMS2CFS
323-
cvp_ts = flux_ts["cvp"][s1:s2] * CMS2CFS
324-
return swp_ts, cvp_ts
320+
flux_ts = read_th(flux).loc[s1:s2]
321+
322+
required = ["swp", "cvp", "sjr"]
323+
missing = [c for c in required if c not in flux_ts.columns]
324+
if missing:
325+
raise ValueError(
326+
f"Missing columns in {flux!r}: {missing}. "
327+
f"Found: {flux_ts.columns.tolist()}"
328+
)
325329

330+
out = flux_ts[required].copy() * CMS2CFS
331+
out["sjr"] = -out["sjr"]
332+
return out
326333

327-
import glob
328-
import os
329-
import pandas as pd
330-
from dms_datastore.read_ts import read_ts
331-
from dms_datastore.read_multi import read_ts_repo
332-
from vtools import days
333334

334335

335336
def sffpx_level(sdate, edate, sffpx_datasrc):
@@ -373,18 +374,38 @@ def sffpx_level(sdate, edate, sffpx_datasrc):
373374
return sf
374375

375376

376-
def predict_oh4_level(s1, s2, oh4_astro_ts, sffpx_elev_ts):
377+
def predict_oh4_level(s1, s2, oh4_astro_ts, sffpx_elev_ts, sjr_ts):
378+
"""Predict total OH4 stage using harmonic tide, SFFPX subtidal, and SJR flow."""
379+
sffpx = sffpx_elev_ts
377380

378-
sffpx = sffpx_elev_ts
379-
sffpx_subtide = cosine_lanczos(sffpx, cutoff_period="40h")
381+
# SFFPX input is meters in the processed repo; the OH4 fit is in feet.
382+
sffpx_subtide = cosine_lanczos(sffpx * M2FT, cutoff_period="40h")
380383
sffpx_subtide = sffpx_subtide.resample("15min").ffill()
381-
## linear regression of sffpx sub tide to oh4 sub tide
382-
oh4_sub_predicted = sffpx_subtide * 0.9620 + 1.1513
383-
best_shift = -10
384-
oh4_sub_predicted = oh4_sub_predicted.shift(-best_shift).squeeze()
385-
oh4_predicted = oh4_sub_predicted + oh4_astro_ts - oh4_sub_predicted.mean()
386-
return oh4_predicted[s1:s2]
387384

385+
# Existing workflow applies the additional 150-min subtidal lag here.
386+
lag = divide_interval(minutes(150), sffpx_subtide.index.freq)
387+
sffpx_subtide = sffpx_subtide.shift(lag).squeeze()
388+
389+
sjr = sjr_ts.resample("15min").ffill()
390+
sjr_excess_norm = np.maximum(sjr - OH4_SJR_THRESHOLD, 0.0) / Q_NORM
391+
392+
parts = pd.concat(
393+
[
394+
oh4_astro_ts.rename("oh4_astro"),
395+
sffpx_subtide.rename("sffpx_subtide"),
396+
sjr_excess_norm.rename("sjr_excess_norm"),
397+
],
398+
axis=1,
399+
).dropna()
400+
401+
oh4_sub_predicted = (
402+
OH4_SUB_INTERCEPT
403+
+ OH4_SF_COEF * parts["sffpx_subtide"]
404+
+ OH4_SJR_EXCESS_COEF * parts["sjr_excess_norm"]
405+
)
406+
407+
oh4_predicted = parts["oh4_astro"] + oh4_sub_predicted
408+
return oh4_predicted[s1:s2]
388409

389410
def radial_gate_flow_ts(zdown_ts, zup_ts, height_ts, s1, s2, dt):
390411

@@ -701,7 +722,7 @@ def gen_gate_height(
701722
return df, zin_df2
702723

703724

704-
def process_height(s1, s2, swp_ts,cvp_ts, oh4_astro_ts, sffpx_elev_ts, save_intermediate=False):
725+
def process_height(s1, s2, swp_ts,cvp_ts, sjr_ts, oh4_astro_ts, sffpx_elev_ts, save_intermediate=False):
705726
"""
706727
Create a ccfb radial gate height time series file
707728
@@ -749,7 +770,13 @@ def process_height(s1, s2, swp_ts,cvp_ts, oh4_astro_ts, sffpx_elev_ts, save_inte
749770
float_format="%.3f",
750771
date_format="%Y-%m-%dT%H:%M",
751772
)
752-
oh4_predict = predict_oh4_level(s1 - margin, s2 + margin, oh4_astro_ts, sffpx_elev_ts)
773+
oh4_predict = predict_oh4_level(
774+
s1 - margin,
775+
s2 + margin,
776+
oh4_astro_ts,
777+
sffpx_elev_ts,
778+
sjr_ts,
779+
)
753780

754781
sim_gate_height, zin_df = gen_gate_height(
755782
swp_ts, priority, max_height, oh4_predict, cvp_ts, inside_level0, s1, s2, dt
@@ -823,15 +850,21 @@ def ccf_gate_cli(
823850
s1 = dtm.datetime.strptime(sdate, "%Y-%m-%d")
824851
s2 = dtm.datetime.strptime(edate, "%Y-%m-%d")
825852
margin = days(3)
826-
swp_ts, cvp_ts = get_export_ts_cfs(s1 - margin, s2 + margin, export_datasrc)
853+
flux_ts = get_flux_ts_cfs(s1 - margin, s2 + margin, export_datasrc)
854+
swp_ts = flux_ts["swp"]
855+
cvp_ts = flux_ts["cvp"]
856+
sjr_ts = flux_ts["sjr"]
857+
827858
oh4_astro_ts = read_ts(oh4_astro_datasrc, force_regular=True).squeeze()
859+
828860
ccf_gate(
829861
s1,
830862
s2,
831863
output,
832864
oh4_astro_ts,
833865
swp_ts,
834866
cvp_ts,
867+
sjr_ts,
835868
sffpx_elev_ts,
836869
plot,
837870
save_intermediate,
@@ -845,6 +878,7 @@ def ccf_gate(
845878
astro_ts,
846879
swp_ts,
847880
cvp_ts,
881+
sjr_ts,
848882
sffpx_elev_ts,
849883
plot=False,
850884
save_intermediate=False,
@@ -890,7 +924,7 @@ def ccf_gate(
890924
position_shift = int(shift_h / sffpx_elev_ts.index.freq)
891925
sffpx_elev_ts = sffpx_elev_ts.shift(position_shift)
892926
oneday = days(1)
893-
height, zin = process_height(sdate, edate, swp_ts,cvp_ts, astro_ts, sffpx_elev_ts)
927+
height, zin = process_height(sdate, edate, swp_ts,cvp_ts, sjr_ts, astro_ts, sffpx_elev_ts)
894928
#height_t = remove_continuous_duplicates(height, height.columns.tolist()[0])
895929
height_t = ts_coarsen(height, grid="2min",preserve_vals=[0.0],qwidth=0.01,hyst=0.5,heartbeat_freq="60min")
896930
height_t = height_t * FT2M

0 commit comments

Comments
 (0)