|
| 1 | +"""Separation of tidal data into species |
| 2 | +The key function in this module is separate_species, which decomposes |
| 3 | +tides into subtidal, diurnal, semidiurnal and noise components. |
| 4 | +
|
| 5 | +The fileters are long, so the time resolution of the amplitude may be limited. |
| 6 | +A demo function is also provided that reads tide series (6min intervl) |
| 7 | +from input files, seperates the species, writes results and optionally plots |
| 8 | +an example |
| 9 | +""" |
| 10 | + |
| 11 | +import argparse |
| 12 | +from vtools.datastore.read_ts import read_ts |
| 13 | +import datetime as dtm |
| 14 | +from vtools.functions.filter import cosine_lanczos |
| 15 | +from vtools.data.vtime import hours, minutes |
| 16 | + |
| 17 | + |
| 18 | +def separate_species(ts, noise_thresh_min=40): |
| 19 | + """Separate species into subtidal, diurnal, semidiurnal and noise components |
| 20 | +
|
| 21 | + Input: |
| 22 | + ts: timeseries to be decomposed into species, assumed to be |
| 23 | + at six minute intervals. The filters used |
| 24 | + have long lenghts, so avoid missing data and allow for four extra |
| 25 | + days worth of data on each end. |
| 26 | +
|
| 27 | + Output: |
| 28 | + four regular time series, representing subtidal, diurnal, semi-diurnal and noise |
| 29 | + """ |
| 30 | + |
| 31 | + # the first filter eliminates noise |
| 32 | + ts_denoise = cosine_lanczos(ts, cutoff_period=minutes(noise_thresh_min)) |
| 33 | + ts_noise = ts - ts_denoise # this is the residual, the part that IS noise |
| 34 | + |
| 35 | + # the filter length assumes 6 minute data. The resulting filter is 90 hours |
| 36 | + # long which is MUCH longer than the default because this filter has to be |
| 37 | + # really sharp |
| 38 | + assert ts.index.freq == minutes(6) |
| 39 | + # 14.5 hours = 870min |
| 40 | + ts_diurnal_and_low = cosine_lanczos( |
| 41 | + ts_denoise, cutoff_period=minutes(870), filter_len=900 |
| 42 | + ) |
| 43 | + ts_semidiurnal_and_high = ts_denoise - ts_diurnal_and_low |
| 44 | + |
| 45 | + # The resulting filter is again 90 hours |
| 46 | + # long which is still a bit longer than the default. Again, |
| 47 | + # we want this filter to be pretty sharp. |
| 48 | + # ts_sub_tide=cosine_lanczos(ts_diurnal_and_low,cutoff_period=hours(40), |
| 49 | + # filter_len=900) |
| 50 | + ts_sub_tide = cosine_lanczos(ts_denoise, cutoff_period=hours(40), filter_len=900) |
| 51 | + ts_diurnal = ts_diurnal_and_low - ts_sub_tide |
| 52 | + return ts_sub_tide, ts_diurnal, ts_semidiurnal_and_high, ts_noise |
| 53 | + |
| 54 | + |
| 55 | +def write_th(filename, ts_output): |
| 56 | + """This works fine for fairly big series""" |
| 57 | + fout = open(filename, "w") |
| 58 | + st = ts_output.ticks[0] |
| 59 | + for el in ts_output: |
| 60 | + tck = float(el.ticks - st) |
| 61 | + fout.write("%-12.1f %6.4f\n" % (tck, el.value)) |
| 62 | + fout.close() |
| 63 | + |
| 64 | + |
| 65 | +def plot_result(ts, ts_semi, ts_diurnal, ts_sub_tide, station): |
| 66 | + import matplotlib.pyplot as plt |
| 67 | + |
| 68 | + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True) |
| 69 | + |
| 70 | + ax1.plot(ts.times, ts.data, color="black", linewidth=1, label="Original data") |
| 71 | + ax1.legend(loc="lower right") # ,prop=legend_font) |
| 72 | + ax1.set_ylabel("Elev (m)") |
| 73 | + ax1.grid(b=True, which="both", color="0.9", linestyle="-", linewidth=0.5) |
| 74 | + |
| 75 | + ax2.plot( |
| 76 | + ts_semi.times, ts_semi.data, color="black", linewidth=1, label="Semidiurnal" |
| 77 | + ) |
| 78 | + ax2.legend(loc="lower right") |
| 79 | + ax2.set_ylabel("Elev (m)") |
| 80 | + ax2.grid(b=True, which="both", color="0.9", linestyle="-", linewidth=0.5) |
| 81 | + |
| 82 | + ax3.plot( |
| 83 | + ts_diurnal.times, ts_diurnal.data, color="black", linewidth=1, label="Diurnal" |
| 84 | + ) |
| 85 | + ax3.legend(loc="lower right") |
| 86 | + ax3.set_ylabel("Elev (m)") |
| 87 | + ax3.grid(b=True, which="both", color="0.9", linestyle="-", linewidth=0.5) |
| 88 | + |
| 89 | + ax4.plot( |
| 90 | + ts_sub_tide.times, |
| 91 | + ts_sub_tide.data, |
| 92 | + color="black", |
| 93 | + linewidth=1, |
| 94 | + label="Subtidal", |
| 95 | + ) |
| 96 | + ax4.legend(loc="lower right") |
| 97 | + ax4.set_ylabel("Elev (m)") |
| 98 | + ax4.grid(b=True, which="both", color="0.9", linestyle="-", linewidth=0.5) |
| 99 | + |
| 100 | + fig.tight_layout() |
| 101 | + fig.autofmt_xdate() |
| 102 | + plt.show() |
| 103 | + # plt.savefig('tidal_species_%s.png'%station,bbox_inches=0) |
| 104 | + plt.close(fig) |
| 105 | + |
| 106 | + |
| 107 | +################# command line application ##################### |
| 108 | +def create_arg_parser(): |
| 109 | + import textwrap |
| 110 | + |
| 111 | + parser = argparse.ArgumentParser( |
| 112 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 113 | + epilog=textwrap.dedent( |
| 114 | + """ |
| 115 | + ============== Example ================== |
| 116 | + > separate_species.py --stime=2009-03-12 --etime=2010-01-01 --label="San Francisco" --outprefix=sf --plot 9414290_gageheight.csv |
| 117 | + """ |
| 118 | + ), |
| 119 | + description="""Script to filter a tide into its subtidal, diurnal and semidiurnal components (and residual noise""", |
| 120 | + ) |
| 121 | + parser.add_argument( |
| 122 | + "--stime", |
| 123 | + default=None, |
| 124 | + required=False, |
| 125 | + help="Start time in ISO-like format 2009-03-12T00:00:00. Time part and 'T' are optional.", |
| 126 | + ) |
| 127 | + parser.add_argument("--etime", default=None, required=False, help="End time.") |
| 128 | + parser.add_argument( |
| 129 | + "--plot", |
| 130 | + default=False, |
| 131 | + required=False, |
| 132 | + action="store_true", |
| 133 | + help="Plot time series in matplotlib", |
| 134 | + ) |
| 135 | + parser.add_argument( |
| 136 | + "--label", default="Tide", required=False, help="Label for station in plots" |
| 137 | + ) |
| 138 | + parser.add_argument( |
| 139 | + "--outprefix", |
| 140 | + default=None, |
| 141 | + help="Output file prefix (species name will be added). If omitted, the label will be used", |
| 142 | + ) |
| 143 | + parser.add_argument("infile", help="Input file.") |
| 144 | + |
| 145 | + return parser |
| 146 | + |
| 147 | + |
| 148 | +def main(): |
| 149 | + parser = create_arg_parser() |
| 150 | + args = parser.parse_args() |
| 151 | + if args.stime: |
| 152 | + sdate = dtm.datetime( |
| 153 | + *list(map(int, re.split(r"[^\d]", args.stime))) |
| 154 | + ) # convert start time string input to datetime |
| 155 | + else: |
| 156 | + sdate = None |
| 157 | + if args.etime: |
| 158 | + edate = dtm.datetime( |
| 159 | + *list(map(int, re.split(r"[^\d]", args.etime))) |
| 160 | + ) # convert start time string input to datetime |
| 161 | + else: |
| 162 | + edate = None |
| 163 | + data_file = args.infile |
| 164 | + station_name = args.label |
| 165 | + outprefix = args.outprefix |
| 166 | + if not outprefix: |
| 167 | + outprefix = station_name.replace(" ", "_") |
| 168 | + do_plot = args.plot |
| 169 | + |
| 170 | + ts = read_ts(data_file, None, None) |
| 171 | + astart = max(ts.start, sdate - days(5)) if sdate else ts.start |
| 172 | + aend = min(ts.end, edate + days(5)) if edate else ts.end |
| 173 | + ts_sub, ts_diurnal, ts_semi, ts_noise = separate_species(ts.window(astart, aend)) |
| 174 | + |
| 175 | + comps = [ |
| 176 | + (ts_noise, "noise"), |
| 177 | + (ts_semi, "semi_over"), |
| 178 | + (ts_diurnal, "diurnal"), |
| 179 | + (ts_sub, "subtidal"), |
| 180 | + ] |
| 181 | + |
| 182 | + for comp in comps: |
| 183 | + ts_output = comp[0].window(sdate, edate) |
| 184 | + output_file = outprefix + "_" + comp[1] + ".th" |
| 185 | + write_th(output_file, ts_output) |
| 186 | + |
| 187 | + if do_plot: |
| 188 | + plot_result( |
| 189 | + ts.window(sdate, edate), |
| 190 | + ts_semi.window(sdate, edate), |
| 191 | + ts_diurnal.window(sdate, edate), |
| 192 | + ts_sub.window(sdate, edate), |
| 193 | + station_name, |
| 194 | + ) |
| 195 | + |
| 196 | + |
| 197 | +def run_example(): |
| 198 | + """This is the data for the example. |
| 199 | + Note that you want the data to |
| 200 | + be at least 4 days longer than the desired output |
| 201 | + """ |
| 202 | + start = dtm.datetime(2009, 2, 18) |
| 203 | + end = dtm.datetime(2010, 11, 24) |
| 204 | + |
| 205 | + out_st = dtm.datetime(2009, 3, 12) |
| 206 | + out_end = dtm.datetime(2010, 11, 2) |
| 207 | + |
| 208 | + sf_path = "../9415020_gageheight.csv" |
| 209 | + ts = read_ts(sf_path, start, end) |
| 210 | + |
| 211 | + print("separating reys...") |
| 212 | + separate_species(ts, "rey", out_st, out_end, do_plot=True) |
| 213 | + |
| 214 | + print("all done") |
| 215 | + |
| 216 | + |
| 217 | +if __name__ == "__main__": |
| 218 | + main() |
| 219 | + # run_example() |
0 commit comments