99import datetime as dtm
1010import pandas as pd
1111from vtools .functions .unit_conversions import M2FT , FT2M , CMS2CFS
12- from vtools import days , hours ,minutes
12+ from vtools import days , hours ,minutes , divide_interval
1313from dms_datastore .read_ts import read_ts
1414from dms_datastore .read_multi import read_ts_repo
1515from vtools .functions .filter import cosine_lanczos
1616from vtools .functions .merge import ts_merge
1717from vtools .functions .coarsen import ts_coarsen
18+
1819import schimpy .param as parms
1920from schimpy .th_io import read_th
2021import numpy as np
2324import math
2425import matplotlib .pyplot as plt
2526import 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
2839ccf_A = 91868000 # area of ccf forbay above 0 navd 88 in ft^2
2940ccf_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.
3347sffpx_level_shift_h = minutes (30 + 8 * 60 )
3448
3549def 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
335336def 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
389410def 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