def test_get_layer_thicknesses(all_layers): # docstring examples top = np.reshape([[10]] * 4, (2, 2)) botm = np.reshape([[np.nan, 8., np.nan, np.nan, np.nan, 2., np.nan]] * 4, (2, 2, 7)).transpose(2, 0, 1) result = get_layer_thicknesses(top, botm) np.testing.assert_almost_equal( result[:, 0, 0], [np.nan, 2., np.nan, np.nan, np.nan, 6., np.nan]) top = np.reshape([[10]] * 4, (2, 2)) botm = np.reshape([[9, 8., 8, 6, 3, 2., -10]] * 4, (2, 2, 7)).transpose(2, 0, 1) result = get_layer_thicknesses(top, botm) assert np.allclose(result[:, 0, 0], [1., 1., 0., 2., 3., 1., 12.]) all_layers2 = np.stack([top] + [b for b in botm]) assert np.allclose(np.abs(np.diff(all_layers2, axis=0)), result) top = all_layers[0].copy() botm = all_layers[1:].copy() thicknesses = get_layer_thicknesses(top, botm) assert thicknesses[-1, 0, 0] == 7 b = thicknesses[:, 0, 0] assert np.array_equal(b[~np.isnan(b)], np.array([7.])) expected = np.zeros(botm.shape[0]) * np.nan expected[1] = 2 expected[4] = 3 expected[-1] = 4 assert np.allclose(thicknesses[:, 2, 2].copy(), expected, equal_nan=True)
def test_wel_setup(shellmound_model_with_dis): m = shellmound_model_with_dis # deepcopy(model) m.cfg['wel']['external_files'] = False wel = m.setup_wel() wel.write() assert os.path.exists(os.path.join(m.model_ws, wel.filename)) assert isinstance(wel, mf6.ModflowGwfwel) assert wel.stress_period_data is not None # verify that periodata blocks were written output = read_mf6_block(wel.filename, 'period') for per, ra in wel.stress_period_data.data.items(): assert len(output[per + 1]) == len(ra) # check the stress_period_data against source data sums = [ ra['q'].sum() if ra is not None else 0 for ra in wel.stress_period_data.array ] cellids = set() cellids2d = set() for per, ra in wel.stress_period_data.data.items(): cellids.update(set(ra['cellid'])) cellids2d.update(set([c[1:] for c in ra['cellid']])) # sum the rates from the source files min_thickness = m.cfg['wel']['source_data']['csvfiles'][ 'vertical_flux_distribution']['minimum_layer_thickness'] dfs = [] for f in m.cfg['wel']['source_data']['csvfiles']['filenames']: dfs.append(pd.read_csv(f)) df = pd.concat(dfs) # cull wells to within model area l, b, r, t = m.modelgrid.bounds outside = (df.x.values > r) | (df.x.values < l) | (df.y.values < b) | (df.y.values > t) df['outside'] = outside df = df.loc[~outside] df['start_datetime'] = pd.to_datetime(df.start_datetime) df['end_datetime'] = pd.to_datetime(df.end_datetime) from mfsetup.grid import get_ij i, j = get_ij(m.modelgrid, df.x.values, df.y.values) df['i'] = i df['j'] = j thicknesses = get_layer_thicknesses(m.dis.top.array, m.dis.botm.array, m.idomain) b = thicknesses[:, i, j] b[np.isnan(b)] = 0 df['k'] = np.argmax(b, axis=0) df['laythick'] = b[df['k'].values, range(b.shape[1])] df['idomain'] = m.idomain[df['k'], i, j] valid_ij = (df['idomain'] == 1) & ( df['laythick'] > min_thickness ) # nwell array of valid i, j locations (with at least one valid layer) culled = df.loc[~valid_ij].copy() # wells in invalid i, j locations df = df.loc[valid_ij].copy() # remaining wells cellids_2d_2 = set(list(zip(df['i'], df['j']))) df.index = df.start_datetime sums2 = [] for i, r in m.perioddata.iterrows(): end_datetime = r.end_datetime - pd.Timedelta(1, unit='d') welldata_overlaps_period = (df.start_datetime < end_datetime) & \ (df.end_datetime > r.start_datetime) q = df.loc[welldata_overlaps_period, 'flux_m3'].sum() sums2.append(q) sums = np.array(sums) sums2 = np.array(sums2) # if this doesn't match # may be due to wells with invalid open intervals getting removed assert np.allclose(sums, sums2, rtol=0.01)
def test_dis_setup(shellmound_model_with_grid): m = shellmound_model_with_grid #deepcopy(model_with_grid) # test intermediate array creation m.cfg['dis']['remake_top'] = True m.cfg['dis']['source_data']['top']['resample_method'] = 'nearest' m.cfg['dis']['source_data']['botm']['resample_method'] = 'nearest' dis = m.setup_dis() botm = m.dis.botm.array.copy() assert isinstance(dis, mf6.ModflowGwfdis) assert 'DIS' in m.get_package_list() # verify that units got conveted correctly assert m.dis.top.array.mean() < 100 assert m.dis.length_units.array == 'meters' # verify that modelgrid was reset after building DIS mg = m.modelgrid assert (mg.nlay, mg.nrow, mg.ncol) == m.dis.botm.array.shape assert np.array_equal(mg.top, m.dis.top.array) assert np.array_equal(mg.botm, m.dis.botm.array) arrayfiles = m.cfg['intermediate_data']['top'] + \ m.cfg['intermediate_data']['botm'] + \ m.cfg['intermediate_data']['idomain'] for f in arrayfiles: assert os.path.exists(f) fname = os.path.splitext(os.path.split(f)[1])[0] k = ''.join([s for s in fname if s.isdigit()]) var = fname.strip(k) data = np.loadtxt(f) model_array = getattr(m.dis, var).array if len(k) > 0: k = int(k) model_array = model_array[k] assert np.array_equal(model_array, data) # test that written idomain array reflects supplied shapefile of active area active_area = rasterize(m.cfg['dis']['source_data']['idomain']['filename'], m.modelgrid) isactive = active_area == 1 written_idomain = load_array(m.cfg['dis']['griddata']['idomain']) assert np.all(written_idomain[:, ~isactive] <= 0) # test idomain from just layer elevations del m.cfg['dis']['griddata']['idomain'] dis = m.setup_dis() top = dis.top.array.copy() top[top == m._nodata_value] = np.nan botm = dis.botm.array.copy() botm[botm == m._nodata_value] = np.nan thickness = get_layer_thicknesses(top, botm) invalid_botms = np.ones_like(botm) invalid_botms[np.isnan(botm)] = 0 invalid_botms[thickness < 1.0001] = 0 # these two arrays are not equal # because isolated cells haven't been removed from the second one # this verifies that _set_idomain is removing them assert not np.array_equal(m.idomain[:, isactive].sum(axis=1), invalid_botms[:, isactive].sum(axis=1)) invalid_botms = find_remove_isolated_cells(invalid_botms, minimum_cluster_size=20) active_cells = m.idomain[:, isactive].copy() active_cells[active_cells < 0] = 0 # need to do this because some idomain cells are -1 assert np.array_equal(active_cells.sum(axis=1), invalid_botms[:, isactive].sum(axis=1)) # test recreating package from external arrays m.remove_package('dis') assert m.cfg['dis']['griddata']['top'] is not None assert m.cfg['dis']['griddata']['botm'] is not None dis = m.setup_dis() assert np.array_equal(m.dis.botm.array[m.dis.idomain.array == 1], botm[m.dis.idomain.array == 1]) # test recreating just the top from the external array m.remove_package('dis') m.cfg['dis']['remake_top'] = False m.cfg['dis']['griddata']['botm'] = None dis = m.setup_dis() dis.write() assert np.array_equal(m.dis.botm.array[m.dis.idomain.array == 1], botm[m.dis.idomain.array == 1]) arrayfiles = m.cfg['dis']['griddata']['top'] for f in arrayfiles: assert os.path.exists(f['filename']) assert os.path.exists(os.path.join(m.model_ws, dis.filename)) # dis package idomain should be consistent with model property updated_idomain = m.idomain assert np.array_equal(m.dis.idomain.array, updated_idomain) # check that units were converted (or not) assert np.allclose(dis.top.array[dis.idomain.array[0] == 1].mean(), 40, atol=10) mcaq = m.cfg['dis']['source_data']['botm']['filenames'][3] assert 'mcaq' in mcaq with rasterio.open(mcaq) as src: mcaq_data = src.read(1) mcaq_data[mcaq_data == src.meta['nodata']] = np.nan assert np.allclose(m.dis.botm.array[3][dis.idomain.array[3] == 1].mean() / .3048, np.nanmean(mcaq_data), atol=5)
def read_wdnr_monthly_water_use(wu_file, wu_points, model, active_area=None, drop_ids=None, minimum_layer_thickness=2 ): """Read water use data from a master file generated from WDNR_wu_data.ipynb. Cull data to area of model. Reshape to one month-year-site value per row. Parameters ---------- wu_file : csv file Water use data ouput from the WDNR_wu_data.ipynb. wu_points : point shapefile Water use locations, generated in the WDNR_wu_data.ipynb Must be in same CRS as sr. model : flopy.modflow.Modflow instance Must have a valid attached .sr attribute defining the model grid. Only wells within the bounds of the sr will be retained. Sr is also used for row/column lookup. Must be in same CRS as wu_points. active_area : str (shapefile path) or shapely.geometry.Polygon Polygon denoting active area of the model. If specified, wells are culled to this area instead of the model bounding box. (default None) minimum_layer_thickness : scalar Minimum layer thickness to have pumping. Returns ------- monthly_data : DataFrame """ col_fmt = '{}_wdrl_gpm_amt' data_renames = {'site_seq_no': 'site_no', 'wdrl_year': 'year'} df = pd.read_csv(wu_file) drop_cols = [c for c in df.columns if 'unnamed' in c.lower()] drop_cols += ['objectid'] df.drop(drop_cols, axis=1, inplace=True, errors='ignore') df.rename(columns=data_renames, inplace=True) if drop_ids is not None: df = df.loc[~df.site_no.isin(drop_ids)].copy() # implement automatic reprojection in gis-utils # maintaining backwards compatibility kwargs = {'dest_crs': model.modelgrid.crs} kwargs = get_input_arguments(kwargs, shp2df) locs = shp2df(wu_points, **kwargs) site_seq_col = [c for c in locs if 'site_se' in c.lower()] locs_renames = {c: 'site_no' for c in site_seq_col} locs.rename(columns=locs_renames, inplace=True) if drop_ids is not None: locs = locs.loc[~locs.site_no.isin(drop_ids)].copy() if active_area is None: # cull the data to the model bounds features = model.modelgrid.bbox txt = "No wells are inside the model bounds of {}"\ .format(model.modelgrid.extent) elif isinstance(active_area, str): # implement automatic reprojection in gis-utils # maintaining backwards compatibility kwargs = {'dest_crs': model.modelgrid.crs} kwargs = get_input_arguments(kwargs, shp2df) features = shp2df(active_area, **kwargs).geometry.tolist() if len(features) > 1: features = MultiPolygon(features) else: features = Polygon(features[0]) txt = "No wells are inside the area of {}"\ .format(active_area) elif isinstance(active_area, Polygon): features = active_area within = [g.within(features) for g in locs.geometry] assert len(within) > 0, txt locs = locs.loc[within].copy() if len(locs) == 0: print('No wells within model area:\n{}\n{}'.format(wu_file, wu_points)) return None, None df = df.loc[df.site_no.isin(locs.site_no)] df.sort_values(by=['site_no', 'year'], inplace=True) # create seperate dataframe with well info well_info = df[['site_no', 'well_radius_mm', 'borehole_radius_mm', 'well_depth_m', 'elev_open_int_top_m', 'elev_open_int_bot_m', 'screen_length_m', 'screen_midpoint_elev_m']].copy() # groupby site number to cull duplicate information well_info = well_info.groupby('site_no').first() well_info['site_no'] = well_info.index # add top elevation, screen midpoint elev, row, column and layer points = dict(zip(locs['site_no'], locs.geometry)) well_info['x'] = [points[sn].x for sn in well_info.site_no] well_info['y'] = [points[sn].y for sn in well_info.site_no] # have to do a loop because modelgrid.rasterize currently only works with scalars print('intersecting wells with model grid...') t0 = time.time() #i, j = [], [] #for x, y in zip(well_info.x.values, well_info.y.values): # iy, jx = model.modelgrid.rasterize(x, y) # i.append(iy) # j.append(jx) i, j = get_ij(model.modelgrid, well_info.x.values, well_info.y.values) print("took {:.2f}s\n".format(time.time() - t0)) top = model.dis.top.array botm = model.dis.botm.array thickness = get_layer_thicknesses(top, botm) well_info['i'] = i well_info['j'] = j well_info['elv_m'] = top[i, j] well_info['elv_top_m'] = well_info.elev_open_int_top_m well_info['elv_botm_m'] = well_info.elev_open_int_bot_m well_info['elv_mdpt_m'] = well_info.screen_midpoint_elev_m well_info['k'] = get_layer(botm, i, j, elev=well_info['elv_mdpt_m'].values) well_info['laythick'] = thickness[well_info.k.values, i, j] well_info['ktop'] = get_layer(botm, i, j, elev=well_info['elv_top_m'].values) well_info['kbotm'] = get_layer(botm, i, j, elev=well_info['elv_botm_m'].values) # for wells in a layer below minimum thickness # move to layer with screen top, then screen botm, # put remainder in layer 1 and hope for the best well_info = wells.assign_layers_from_screen_top_botm(well_info, model, flux_col='q', screen_top_col='elv_top_m', screen_botm_col='elv_botm_m', across_layers=False, distribute_by='transmissivity', minimum_layer_thickness=2.) #isthin = well_info.laythick < minimum_layer_thickness #well_info.loc[isthin, 'k'] = well_info.loc[isthin, 'ktop'].values #well_info.loc[isthin, 'laythick'] = model.dis.thickness.array[well_info.k[isthin].values, # well_info.i[isthin].values, # well_info.j[isthin].values] #isthin = well_info.laythick < minimum_layer_thickness #well_info.loc[isthin, 'k'] = well_info.loc[isthin, 'kbotm'].values #well_info.loc[isthin, 'laythick'] = model.dis.thickness.array[well_info.k[isthin].values, # well_info.i[isthin].values, # well_info.j[isthin].values] #isthin = well_info.laythick < minimum_layer_thickness #well_info.loc[isthin, 'k'] = 1 #well_info.loc[isthin, 'laythick'] = model.dis.thickness.array[well_info.k[isthin].values, # well_info.i[isthin].values, # well_info.j[isthin].values] isthin = well_info.laythick < minimum_layer_thickness assert not np.any(isthin) # make a datetime column monthlyQ_cols = [col_fmt.format(calendar.month_abbr[i]).lower() for i in range(1, 13)] monthly_data = df[['site_no', 'year'] + monthlyQ_cols] monthly_data.columns = ['site_no', 'year'] + np.arange(1, 13).tolist() # stack the data # so that each row is a site number, year, month # reset the index to move multi-index levels back out to columns stacked = monthly_data.set_index(['site_no', 'year']).stack().reset_index() stacked.columns = ['site_no', 'year', 'month', 'gallons'] stacked['datetime'] = pd.to_datetime(['{}-{:02d}'.format(y, m) for y, m in zip(stacked.year, stacked.month)]) monthly_data = stacked return well_info, monthly_data