def process_quad_patch(name, M=None): center_idx = np.nonzero(centerlines['name'] == name)[0][0] centerline = centerlines['geom'][center_idx] if M is None: M = centerlines['rows'][center_idx] bound = bounds['geom'][bounds['name'] == name][0] center = linestring_utils.resample_linearring(np.array(centerline), scale, closed_ring=0) g = unstructured_grid.UnstructuredGrid(max_sides=6) # Smooth the exterior ext_points = np.array(bound.exterior) ext_points = linestring_utils.resample_linearring(ext_points, scale, closed_ring=True) from stompy import filters ext_points[:, 0] = filters.lowpass_fir(ext_points[:, 0], 3) ext_points[:, 1] = filters.lowpass_fir(ext_points[:, 1], 3) smooth_bound = geometry.Polygon(ext_points) L = smooth_bound.exterior.length def profile(x, s, perp): probe_left = geometry.LineString([x, x + L * perp]) probe_right = geometry.LineString([x, x - L * perp]) left_cross = smooth_bound.exterior.intersection(probe_left) right_cross = smooth_bound.exterior.intersection(probe_right) assert left_cross.type == 'Point', "Fix this for multiple intersections" assert right_cross.type == 'Point', "Fix this for multiple intersections" pnt_left = np.array(left_cross) pnt_right = np.array(right_cross) d_left = utils.dist(x, pnt_left) d_right = utils.dist(x, pnt_right) return np.interp(np.linspace(-1, 1, M), [-1, 0, 1], [-d_right, 0, d_left]) g.add_rectilinear_on_line(center, profile) g.renumber() return g
def nudge_by_gage(ds, usgs_station, station, decorr_days, period_start=None, period_end=None): # This slicing may be stopping one sample shy, shouldn't be a problem. if period_start is None: period_start = ds.time.values[0] if period_end is None: period_end = ds.time.values[-1] usgs_gage = usgs_nwis.nwis_dataset(usgs_station, products=[60], start_date=period_start, end_date=period_end, days_per_request='M', cache_dir=common.cache_dir) # Downsample to daily df = usgs_gage['stream_flow_mean_daily'].to_dataframe() df_daily = df.resample('D').mean() # Get the subset of BAHM data which overlaps this gage data time_slc = slice(np.searchsorted(ds.time, df_daily.index.values[0]), 1 + np.searchsorted(ds.time, df_daily.index.values[-1])) bahm_subset = ds.sel(station=station).isel(time=time_slc) assert len(bahm_subset.time) == len( df_daily), "Maybe BAHM data doesn't cover entire period" errors = bahm_subset.flow_cfs - df_daily.stream_flow_mean_daily # Easiest: interpolate errors over nans, apply to bahm data array. # the decorrelation time is tricky, though. # Specify a decorrelation time scale then relax from error to zero # over that period valid = np.isfinite(errors.values) errors_interp = np.interp(utils.to_dnum(ds.time), utils.to_dnum(df_daily.index[valid]), errors[valid]) all_valid = np.zeros(len(ds.time), 'f8') all_valid[time_slc] = 1 * valid weights = (2 * filters.lowpass_fir(all_valid, decorr_days)).clip(0, 1) weighted_errors = weights * errors_interp # Does this work? subset = dict(station=station) cfs_vals = ds.flow_cfs.loc[subset] - weighted_errors ds.flow_cfs.loc[subset] = cfs_vals.clip(0, np.inf) ds.flow_cms.loc[subset] = 0.028316847 * ds.flow_cfs.loc[subset] # user feedback cfs_shifts = weighted_errors[time_slc] print("Nudge: %s => %s, shift in CFS: %.2f +- %.2f" % (usgs_station, station, np.mean(cfs_shifts), np.std(cfs_shifts)))
def Hsig_adjustment(da): Hsig=cdip_mop.hindcast_dataset(station='SM141', # Pescadero State Beach start_date=da.time.values[0], end_date=da.time.values[-1], clip='inclusive', cache_dir=cache_dir, variables=['waveHs']) Hsig_colo=np.interp(utils.to_dnum(da.time.values), utils.to_dnum(Hsig.time.values), Hsig['waveHs'].values ) offset=(0.351*Hsig_colo - 0.230).clip(0) # TODO: see if a hanning window lowpass makes the "optimal" window size # something shorter, which would seem more physical. offset=filters.lowpass_fir(offset,winsize=7*24,window='boxcar') return da+offset
## # Sample datasets if 1: import bathy dem = bathy.dem() # fake, sparser tracks. adcp_shp = wkb2shp.shp2geom('sparse_fake_bathy_trackline.shp') xys = [] for feat in adcp_shp['geom']: feat_xy = np.array(feat) feat_xy = linestring_utils.resample_linearring(feat_xy, 1.0, closed_ring=0) feat_xy = filters.lowpass_fir(feat_xy, winsize=6, axis=0) xys.append(feat_xy) adcp_xy = np.concatenate(xys) source_ds = xr.Dataset() source_ds['x'] = ('sample', 'xy'), adcp_xy source_ds['z'] = ('sample', ), dem(adcp_xy) ## def steady_streamline_oneway(g, Uc, x0, max_t=3600, max_dist=np.inf): # trace some streamlines x0 = np.asarray(x0) t0 = 0.0 c = g.select_cells_nearest(x0, inside=True)
def low_high(d, winsize): high = percentile_filter(d, 95, winsize) low = percentile_filter(d, 5, winsize) high = filters.lowpass_fir(high, winsize) low = filters.lowpass_fir(low, winsize) return low, high
def figure_usgs_salinity_time_series(station, station_name): mod_lp_win = usgs_lp_win = 40 # 40h lowpass # Gather USGS data: ds = usgs_salinity_time_series(station) usgs_dt_s = np.median(np.diff(ds.time)) / np.timedelta64(1, 's') usgs_stride = slice(None, None, max(1, int(3600. / usgs_dt_s))) if 'salinity_01' in ds: obs_salt_davg = np.c_[ds.salinity.values[usgs_stride], ds.salinity_01.values[usgs_stride]] obs_salt_davg = np.nanmean(obs_salt_davg, axis=1) else: obs_salt_davg = ds.salinity.values[usgs_stride] dists = utils.dist(his_xy, [ds.x, ds.y]) station_idx = np.argmin(dists) print("Nearest model station is %.0f m away from observation" % (dists[station_idx])) print(station_idx) def low_high(d, winsize): high = percentile_filter(d, 95, winsize) low = percentile_filter(d, 5, winsize) high = filters.lowpass_fir(high, winsize) low = filters.lowpass_fir(low, winsize) return low, high obs_salt_range = low_high(obs_salt_davg, usgs_lp_win) mod_salt_davg = his.salinity.isel(stations=station_idx).mean(dim='laydim') mod_salt_range = low_high(mod_salt_davg, mod_lp_win) # Try picking out a reasonable depth in the model surf_label = "Surface" bed_label = "Bed" if ds.salinity.attrs['elev_mab'] is not None: z_mab = ds.salinity.attrs['elev_mab'] surf_label = "%.1f mab" % z_mab mod_salt_surf = extract_at_zab(his, "salinity", z_mab, stations=station_idx) else: mod_salt_surf = his.salinity.isel(stations=station_idx, laydim=-1) if ('salinity_01' in ds) and (ds.salinity_01.attrs['elev_mab'] is not None): z_mab = ds.salinity_01.attrs['elev_mab'] bed_label = "%.1f mab" % z_mab mod_salt_bed = extract_at_zab(his, "salinity", z_mab, stations=station_idx) else: mod_salt_bed = his.salinity.isel(stations=station_idx, laydim=0) mod_deltaS = mod_salt_bed - mod_salt_surf if 'salinity_01' in ds: if ds.site_no == '375607122264701': ds = ds.rename({ "salinity": "salinity_01", "salinity_01": "salinity" }) if 'salinity_01' in ds: obs_deltaS = ds.salinity_01.values - ds.salinity.values else: obs_deltaS = None if 1: # plotting time series plt.figure(1).clf() fig, ax = plt.subplots(num=1) fig.set_size_inches([10, 4.75], forward=True) # These roughly mimic the style of water level plots in the validation report. obs_color = 'cornflowerblue' obs_lw = 1.5 mod_color = 'k' mod_lw = 0.8 if 'salinity_01' in ds: ax.plot(utils.to_dnum(ds.time)[usgs_stride], filters.lowpass_fir(ds.salinity[usgs_stride], usgs_lp_win), label='Obs. Upper', lw=obs_lw, color=obs_color) ax.plot(utils.to_dnum(ds.time)[usgs_stride], filters.lowpass_fir(ds.salinity_01[usgs_stride], usgs_lp_win), label='Obs. Lower', lw=obs_lw, color=obs_color, ls='--') else: ax.plot(utils.to_dnum(ds.time)[usgs_stride], filters.lowpass_fir(ds.salinity[usgs_stride], usgs_lp_win), label='Obs.', lw=obs_lw, color=obs_color) ax.plot(utils.to_dnum(his.time), filters.lowpass_fir(mod_salt_surf, 40), label='Model %s' % surf_label, lw=mod_lw, color=mod_color) ax.plot(utils.to_dnum(his.time), filters.lowpass_fir(mod_salt_bed, 40), label='Model %s' % bed_label, lw=mod_lw, color=mod_color, ls='--') if 1: # is it worth showing tidal variability? ax.fill_between(utils.to_dnum(ds.time)[usgs_stride], obs_salt_range[0], obs_salt_range[1], color=obs_color, alpha=0.3, zorder=-1, lw=0) ax.fill_between(utils.to_dnum(his.time), mod_salt_range[0], mod_salt_range[1], color='0.3', alpha=0.3, zorder=-1, lw=0) ax.set_title(station_name) ax.xaxis.axis_date() fig.autofmt_xdate() ax.set_ylabel('Salinity (ppt)') ax.legend(fontsize=10, loc='lower left') ax.axis(xmin=utils.to_dnum(t_spunup), xmax=utils.to_dnum(t_stop)) fig.tight_layout() safe_station = station_name.replace(' ', '_') fig.savefig(os.path.join(savepath, "%s.png" % safe_station), dpi=100) fig.savefig(os.path.join(savepath, "%s.pdf" % safe_station)) if tex_fp is not None: # metrics target_time_dnum = utils.to_dnum(his.time.values) obs_time_dnum = utils.to_dnum(ds.time.values) obs_salt_davg_intp = utils.interp_near(target_time_dnum, obs_time_dnum[usgs_stride], obs_salt_davg, 1.5 / 24) valid = np.isfinite(mod_salt_davg * obs_salt_davg_intp).values valid = (valid & (target_time_dnum >= utils.to_dnum(t_spunup)) & (target_time_dnum <= utils.to_dnum(t_stop))) dnum = target_time_dnum[valid] mod_values = mod_salt_davg[valid].values obs_values = obs_salt_davg_intp[valid] bias = np.mean(mod_values - obs_values) ms = utils.model_skill(mod_values, obs_values) r2 = np.corrcoef(mod_values, obs_values)[0, 1] rmse = utils.rms(mod_values - obs_values) tex_fp.write(( "%-16s " # station name " & %7.3f" # skill " & %11.2f" # bias " & %7.3f" # r2 " & %10.2f" # rmse " \\\ \\hline \n") % (station_name, ms, bias, r2, rmse))
def smooth(x): return filters.lowpass_fir(x, winsize=lp_win)
fields=['tnum','swim_x','swim_y','x','y', 'model_u_surf','model_v_surf'], fill='interp') t=expand['tnum'].values swim_x=expand['swim_x'].values swim_y=expand['swim_y'].values geo_x=expand['x'].values geo_y=expand['y'].values hyd_u=expand['model_u_surf'].values hyd_v=expand['model_v_surf'].values winsize=7 # Hydro values hyd_hdg=np.arctan2(filters.lowpass_fir(hyd_v,winsize), filters.lowpass_fir(hyd_u,winsize))[1:-1] # Swim-based values: x_lp=filters.lowpass_fir(swim_x,winsize) y_lp=filters.lowpass_fir(swim_y,winsize) u=np.diff(x_lp)/np.diff(swim_t) v=np.diff(y_lp)/np.diff(swim_t) hdg=np.arctan2(v,u) swim_turn=(np.diff(hdg) -np.pi)%(2*np.pi) + np.pi swim_spd=np.sqrt(u**2+v**2) swim_spd_ctr=0.5*(spd[1:]+spd[:-1]) swim_hdg_ctr=0.5*(hdg[1:]+hdg[:-1]) # this is already relative to the flow # Geo-based values geo_x_lp=filters.lowpass_fir(geo_x,winsize)
# get the data into a monthly time series before trying to fit seasonal cycle valid = np.isfinite(fld_in.values) absmonth_mean=bin_mean(absmonth[valid],fld_in.values[valid]) month_mean=bin_mean(month[valid],fld_in.values[valid]) if np.sum(np.isfinite(month_mean)) < 12: print("Insufficient data for seasonal trends - will fill with sample mean") trend_and_season=np.nanmean(month_mean) * np.ones(len(dns)) t_and_s_flag=FLAG_MEAN else: # fit long-term trend and a stationary seasonal cycle # this removes both the seasonal cycle and the long-term mean, # leaving just the trend trend_hf=fld_in.values - month_mean[month] lp = filters.lowpass_fir(trend_hf,lowpass_days,nan_weight_threshold=0.01) trend = utils.fill_invalid(lp) # recombine with the long-term mean and monthly trend # to get the fill values. trend_and_season = trend + month_mean[month] t_and_s_flag=FLAG_SEASONAL_TREND # long gaps are mostly filled by trend and season gaps=mark_gaps(dns,valid,shortgap_days,include_ends=True) fld_in.values[gaps] = trend_and_season[gaps] fld_flag.values[gaps] = t_and_s_flag still_missing=np.isnan(fld_in.values) fld_in.values[still_missing] = utils.fill_invalid(fld_in.values)[still_missing] fld_flag.values[still_missing] = FLAG_INTERP
lamb = 4000 # wave-length of meanders width = 500 # mean channel width noise_w = 50 # amplitude of noise to add to the channel banks noise_l = 1500 # length-scale of noise centerline = np.c_[s, amp * np.cos(2 * np.pi * s / lamb)] pline = geometry.LineString(centerline) channel = pline.buffer(width / 2) ring = np.array(channel.exterior) ring_norm = linestring_utils.left_normals(ring) noise = (np.random.random(len(ring_norm)) - 0.5) winsize = int(noise_l / (channel.exterior.length / len(ring_norm))) noise[:winsize] = 0 # so the ends still match up noise[-winsize:] = 0 noise_lp = filters.lowpass_fir(noise, winsize) noise_lp *= noise_w / np.sqrt(np.mean(noise_lp**2)) # domain boundary including the random noise ring_noise = ring + noise_lp[:, None] * ring_norm # Create the curvilinear section thalweg = centerline[50:110] plt.figure(1).clf() plt.plot(centerline[:, 0], centerline[:, 1], 'k-', zorder=2) plt.axis('equal') plot_wkb.plot_wkb(channel, zorder=-2) plt.plot(ring_noise[:, 0], ring_noise[:, 1], 'm-')
def disp_array(self): self.hydro.infer_2d_elements() self.hydro.infer_2d_links() # first calculate all time steps, just in 2D. Q = np.zeros((len(self.hydro.t_secs), self.hydro.n_2d_links), np.float64) A = np.zeros((len(self.hydro.t_secs), self.hydro.n_2d_links), np.float64) for ti in utils.progress(range(len(self.hydro.t_secs))): t_sec = self.hydro.t_secs[ti] flows = [ hydro.flows(t_sec) for hydro in [self.hydro_tidal, self.hydro] ] flow_hp = flows[0] - flows[1] # depth-integrate flow_hor = flow_hp[:self.hydro_tidal.n_exch_x] link_flows = np.bincount( self.hydro.exch_to_2d_link['link'], self.hydro.exch_to_2d_link['sgn'] * flow_hor) Q[ti, :] = link_flows**2 A[ti, :] = np.bincount( self.hydro.exch_to_2d_link['link'], self.hydro.areas(t_sec)[:self.hydro.n_exch_x]) dt_s = np.median(np.diff(self.hydro.t_secs)) winsize = int(self.lowpass_days * 86400 / dt_s) # These are a little slow. 10s? # could streamline this some since we later only use a fraction of the values. # clip here is because in some cases the values are very low and # and some roundoff is creating negatives. Qlp = filters.lowpass_fir(Q, winsize=winsize, axis=0).clip(0) Alp = filters.lowpass_fir(A, winsize=winsize, axis=0).clip(0) rms_flows = np.sqrt(Qlp) mean_A = Alp Lexch = self.hydro.exchange_lengths.sum(axis=1)[:self.hydro.n_exch_x] L = [ Lexch[exchs[0]] for l, exchs in utils.enumerate_groups( self.hydro.exch_to_2d_link['link']) ] # This is just a placeholder. A proper scaling needs to account for # cell size. rms_flows has units of m3/s. probably that should be normalized # by dividing by average flux area, and possibly multiplying by the distance # between cell centers. that doesn't seem quite right. link_K = self.K_scale * rms_flows * L / mean_A # this is computed for every time step, but we can trim that down # it's lowpassed at winsize. Try stride of half winsize. # That was used for the first round of tests, but it looks a bit # sparse. K_stride = winsize // 4 K2D = link_K[::K_stride, :] K_t_secs = self.hydro.t_secs[::K_stride] if self.amp_factor != 1.0: Kbar = K2D.mean(axis=0) K2D = (Kbar[None, :] + self.amp_factor * (K2D - Kbar[None, :])).clip(0) K = np.zeros((len(K_t_secs), self.hydro.n_exch), np.float64) # and then project to 3D K[:, :self.hydro.n_exch_x] = K2D[:, self.hydro.exch_to_2d_link['link']] if 0: # DEBUGGING # verify that I can get back to the previous, constant in time # run. log.warning("Debugging K") Kconst = super(KautoUnsteady, self).disp_array() K[:, :] = Kconst[None, :] log.info("Median dispersion coefficient: %g" % (np.median(K))) return K_t_secs, K
# more explicitly: # time with a headwind minus the unimpaired time # distance / reduced speed - unimpaired time # (1500 * 0.174) / (1500-0.25) - 0.174 = 29us # So my signal is much smaller -- but the assumption of 0.75m/s and 0.25 m/s is # probably an exaggeration, and some of the distance traveled is not parallel to the # flow # Still need to properly work through the whole formula if 0: # Any chance that filtering out the spikes gives something reasonable? lp_skew = net_skew.copy() lp_skew[np.abs(lp_skew) > 6000] = np.nan lp_skew = filters.lowpass_fir(lp_skew, winsize=5000) axs[0].plot(t_complete, lp_skew, label='LP Net skew') axs[0].legend(loc='upper left') # plot skew and travel in original time coordinate for # each def demean(x): return x - np.nanmean(x) for i, leg in enumerate(legs): axs[1].plot(leg.transits.time, leg.transits.clock_skew_us, label=leg.label) axs[2].plot(leg.transits.time, demean(leg.transits.mean_travel_us),
def lp(x): return filters.lowpass_fir(x, winsize=50)
def fill_and_flag(ds,fld,site, lowpass_days=3*365, shortgap_days=45 # okay to interpolate a little over a month? ): """ Update a single field for a single site in ds, by extracting long-term trends, seasonal cycle, and interpolating between these and measured data """ # first, create mapping from time index to absolute month dts=utils.to_datetime(dns) absmonth = [12*dt.year + (dt.month-1) for dt in dts] absmonth = np.array(absmonth) - dts[0].year*12 month=absmonth%12 fld_in=ds[fld].sel(site=site) orig_values=fld_in.values fld_flag=ds[fld+'_flag'].sel(site=site) prefilled=fld_flag.values & (FLAG_SEASONAL_TREND | FLAG_INTERP | FLAG_MEAN) fld_in.values[prefilled]=np.nan # resets the work of this loop in case it's run multiple times n_valid=np.sum(~fld_in.isnull()) if n_valid==0: msg=" --SKIPPING--" else: msg="" print(" field: %s %d/%d valid input points %s"%(fld,n_valid,len(fld_in),msg)) if n_valid==0: return # get the data into a monthly time series before trying to fit seasonal cycle valid = np.isfinite(fld_in.values) absmonth_mean=bin_mean(absmonth[valid],fld_in.values[valid]) month_mean=bin_mean(month[valid],fld_in.values[valid]) if np.sum(np.isfinite(month_mean)) < 12: print("Insufficient data for seasonal trends - will fill with sample mean") trend_and_season=np.nanmean(month_mean) * np.ones(len(dns)) t_and_s_flag=FLAG_MEAN else: # fit long-term trend and a stationary seasonal cycle # this removes both the seasonal cycle and the long-term mean, # leaving just the trend trend_hf=fld_in.values - month_mean[month] lp = filters.lowpass_fir(trend_hf,lowpass_days,nan_weight_threshold=0.01) trend = utils.fill_invalid(lp) # recombine with the long-term mean and monthly trend # to get the fill values. trend_and_season = trend + month_mean[month] t_and_s_flag=FLAG_SEASONAL_TREND # long gaps are mostly filled by trend and season gaps=mark_gaps(dns,valid,shortgap_days,include_ends=True) fld_in.values[gaps] = trend_and_season[gaps] fld_flag.values[gaps] = t_and_s_flag still_missing=np.isnan(fld_in.values) fld_in.values[still_missing] = utils.fill_invalid(fld_in.values)[still_missing] fld_flag.values[still_missing] = FLAG_INTERP # Make sure all flows are nonnegative negative=fld_in.values<0.0 fld_in.values[negative]=0.0 fld_flag.values[negative] |= FLAG_CLIPPED if 0: # illustrative(?) plots fig,ax=plt.subplots() ax.plot(dns,orig_values,'m-o',label='Measured %s'%fld) ax.plot(dns,fld_in,'k-',label='Final %s'%fld,zorder=5) # ax.plot(dns,month_mean[month],'r-',label='Monthly Clim.') # ax.plot(dns,trend_hf,'b-',label='Trend w/HF') ax.plot(dns,trend,'g-',lw=3,label='Trend') ax.plot(dns,trend_and_season,color='orange',label='Trend and season')
def fill_tidal_data(da,fill_time=True): """ Extract tidal harmonics from an incomplete xarray DataArray, use those to fill in the gaps and return a complete DataArray. Uses all 37 of the standard NOAA harmonics, may not be stable with short time series. A 5-day lowpass is removed from the harmonic decomposition, and added back in afterwards. Assumes that the DataArray has a 'time' coordinate with datetime64 values. The time dimension must be dense enough to extract an exact time step If fill_time is True, holes in the time coordinate will be filled, too. """ diffs=np.diff(da.time) dt=np.median(diffs) if fill_time: gaps=np.nonzero(diffs>1.5*dt)[0] pieces=[] last=0 for gap_i in gaps: # gap_i=10 means that the 10th diff was too big # that means the jump from 10 to 11 was too big # the preceding piece should go through 9, so # exclusive of gap_i pieces.append(da.time.values[last:gap_i]) pieces.append(np.arange( da.time.values[gap_i], da.time.values[gap_i+1], dt)) last=gap_i+1 pieces.append(da.time.values[last:]) dense_times=np.concatenate(pieces) dense_values=np.nan*np.zeros(len(dense_times),np.float64) dense_values[ np.searchsorted(dense_times,da.time.values) ] = da.values da=xr.DataArray(dense_values, dims=['time'],coords=[dense_times]) else: pass dnums=utils.to_dnum(da.time) data=da.values # lowpass at about 5 days, splitting out low/high components winsize=int( np.timedelta64(5,'D') / dt ) data_lp=filters.lowpass_fir(data,winsize) data_hp=data - data_lp valid=np.isfinite(data_hp) omegas=harm_decomp.noaa_37_omegas() # as rad/sec harmonics=harm_decomp.decompose(dnums[valid]*86400,data_hp[valid],omegas) dense=harm_decomp.recompose(dnums*86400,harmonics,omegas) data_recon=utils.fill_invalid(data_lp) + dense data_filled=data.copy() missing=np.isnan(data_filled) data_filled[missing] = data_recon[missing] fda=xr.DataArray(data_filled,coords=[da.time],dims=['time']) return fda
# units: precip: kg/m2, monthly average. Based on prior scripts, this is maybe a per day number # https://www.esrl.noaa.gov/psd/data/gridded/data.narr.monolevel.html#plot # mentions that this file is "Monthly average of Daily Accumulation" # units: evap: kg/m2 monthly accumulated average. Based on prior scripts, maybe a per 3h number?? # From the file metadata, "should be" mm/month. ax.plot(utils.to_dnum(precip.time.values), precip.apcp.isel(x=lon_i, y=lat_i), '--', label='NARR precip') ax.plot(utils.to_dnum(evap.time.values), 8 * evap.pevap.isel(x=lon_i, y=lat_i), label='NARR p-evap') # data in mm, originally at hourly time scale. ax.plot(utils.to_dnum(union_city.time), filters.lowpass_fir(24 * union_city.HlyPrecip, 10 * 24), '--', label='CIMIS precip') ax.plot(utils.to_dnum(union_city.time), filters.lowpass_fir(24 * union_city.HlyEto, 10 * 24), label='CIMIS ETO') ax.plot(utils.to_dnum(union_city.time), filters.lowpass_fir(24 * union_city.HlyEvap, 10 * 24), label='CIMIS Evap') ax.plot(utils.to_dnum(burl_ds.time), burl_ds.evap, label='Burlingame') # -- ax_cumul.plot(utils.to_dnum(precip.time.values),