def test_get_inset_boundary_heads(tmr, parent_heads): """Verify that inset model specified head boundary accurately reflects parent model head solution, including when cells are dry or missing (e.g. pinched out cells in MF6). """ bheads_df = tmr.get_inset_boundary_heads() groups = bheads_df.groupby('per') all_kstpkper = parent_heads.get_kstpkper() kstpkper_list = [all_kstpkper[0], all_kstpkper[-1]] for kstp, kper in kstpkper_list: hds = parent_heads.get_data(kstpkper=(kstp, kper)) df = groups.get_group(kper) df['cellid'] = list(zip(df.k, df.i, df.j)) # check for duplicate locations (esp. corners) # in mf2005, duplicate chd heads will be summed assert not df.cellid.duplicated().any() # x, y, z locations of inset model boundary cells ix = tmr.inset.modelgrid.xcellcenters[df.i, df.j] iy = tmr.inset.modelgrid.ycellcenters[df.i, df.j] iz = tmr.inset.modelgrid.zcellcenters[df.k, df.i, df.j] # parent model grid cells associated with inset boundary cells i, j = get_ij(tmr.parent.modelgrid, ix, iy) k = get_layer(tmr.parent.dis.botm.array, i, j, iz)
def test_upw_setup(pfl_nwt_with_dis, case): m = pfl_nwt_with_dis #deepcopy(pfl_nwt_with_dis) if case == 0: # test intermediate array creation m.cfg['upw']['remake_arrays'] = True upw = m.setup_upw() arrayfiles = m.cfg['intermediate_data']['hk'] + \ m.cfg['intermediate_data']['vka'] for f in arrayfiles: assert os.path.exists(f) # check that lakes were set up properly hiKlakes_value = {} hiKlakes_value['hk'] = float(m.cfg['parent']['hiKlakes_value']) hiKlakes_value['sy'] = 1.0 hiKlakes_value['ss'] = 1.0 for var in ['hk', 'sy', 'ss']: arr = upw.__dict__[var].array for k, kvar in enumerate(arr): if not np.any(m.isbc[k] == 2): assert kvar.max(axis=(0, 1)) < hiKlakes_value[var] else: assert np.diff(kvar[m.isbc[k] == 2]).sum() == 0 assert kvar[m.isbc[k] == 2][0] == hiKlakes_value[var] # compare values to parent model for var in ['hk', 'vka']: ix, iy = m.modelgrid.xcellcenters.ravel( ), m.modelgrid.ycellcenters.ravel() pi, pj = get_ij(m.parent.modelgrid, ix, iy) parent_layer = {0: 0, 1: 0, 2: 1, 3: 2, 4: 3} for k, pk in parent_layer.items(): parent_vals = m.parent.upw.__dict__[var].array[pk, pi, pj] inset_vals = upw.__dict__[var].array parent_max_val = m.parent.upw.__dict__[var].array.max() valid_parent = parent_vals != hiKlakes_value.get(var, -9999) valid_inset = inset_vals[k].ravel() != hiKlakes_value.get( var, -9999) parent_vals = parent_vals[valid_parent & valid_inset] inset_vals = inset_vals[k].ravel()[valid_parent & valid_inset] assert np.allclose(parent_vals, inset_vals, rtol=0.01) elif case == 1: # test changing vka to anisotropy m.cfg['upw']['layvka'] = [1, 1, 1, 1, 1] m.cfg['upw']['vka'] = [10, 10, 10, 10, 10] upw = m.setup_upw() assert np.array_equal(m.upw.layvka.array, np.array([1, 1, 1, 1, 1])) assert np.allclose(m.upw.vka.array.max(axis=(1, 2)), np.array([10, 10, 10, 10, 10]))
def compare_inset_parent_values(inset_array, parent_array, inset_modelgrid, parent_modelgrid, inset_parent_layer_mapping=None, nodata=-9999, **kwargs): """Compare values on different model grids (for example, parent and inset models that overlap), by getting the closes parent cell at each inset cell location. todo: compare_inset_parent_values: add interpolation for more precise comparison Parameters ---------- inset_array : inset model values (ndarray) parent_array : parent model values (ndarray) inset_modelgrid : flopy modelgrid for inset model parent_modelgrid : flopy modelgrid for parent model inset_parent_layer_mapping : dict Mapping between inset and parent model layers {inset model layer: parent model layer} nodata : float Exclude these values from comparison kwargs : kwargs to np.allclose Returns ------- AssertionError if np.allclose evaluates to False for any layer """ if len(inset_array.shape) < 3: inset_array = np.array([inset_array]) if inset_parent_layer_mapping is None: nlay = inset_array.shape[0] inset_parent_layer_mapping = dict( zip(list(range(nlay)), list(range(nlay)))) ix, iy = inset_modelgrid.xcellcenters.ravel( ), inset_modelgrid.ycellcenters.ravel() pi, pj = get_ij(parent_modelgrid, ix, iy) for k, pk in inset_parent_layer_mapping.items(): parent_vals = parent_array[pk, pi, pj] valid = (parent_vals != nodata) & (inset_array[k].ravel() != nodata) parent_vals = parent_vals[valid] inset_vals = inset_array[k].ravel()[valid] assert np.allclose(parent_vals, inset_vals, **kwargs)
def _source_grid_mask(self): """Boolean array indicating window in parent model grid (subset of cells) that encompass the pfl_nwt model domain. Used to speed up interpolation of parent grid values onto pfl_nwt grid.""" if self._source_mask is None: mask = np.zeros( (self.parent.modelgrid.nrow, self.parent.modelgrid.ncol), dtype=bool) if self.inset.parent_mask.shape == self.parent.modelgrid.xcellcenters.shape: mask = self.inset.parent_mask else: x, y = np.squeeze(self.inset.bbox.exterior.coords.xy) pi, pj = get_ij(self.parent.modelgrid, x, y) pad = 3 i0, i1 = pi.min() - pad, pi.max() + pad j0, j1 = pj.min() - pad, pj.max() + pad mask[i0:i1, j0:j1] = True self._source_mask = mask return self._source_mask
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 get_open_interval_thickness(m, heads=None, i=None, j=None, x=None, y=None, screen_top=None, screen_botm=None, nodata=-999): """ Gets the thicknesses of each model layer at specified locations and open intervals. If heads are supplied, a saturated thickness is determined for each row, column or x, y location; otherwise, total layer thickness is used. Returned thicknesses are limited to open intervals (screen_top, screen_botm) if included, otherwise the layer tops and bottoms and (optionally) the water table are used. Parameters ---------- m : mfsetup.MF6model or mfsetup.MFnwtModel instance Must have dis, and optionally, attached MFsetupGrid instance heads : 2D array OR 3D array (optional) numpy array of shape nlay by n locations (2D) OR complete heads array of the model for one time (3D). i : 1D array-like of ints, of length n locations zero-based row indices (optional; alternately specify x, y) j : 1D array-like of ints, of length n locations zero-based column indices (optional; alternately specify x, y) x : 1D array-like of floats, of length n locations x locations in real world coordinates (optional) y : 1D array-like of floats, of length n locations y locations in real world coordinates (optional) screen_top : 1D array-like of floats, of length n locations open interval tops (optional; default is model top) screen_botm : 1D array-like of floats, of length n locations open interval bottoms (optional; default is model bottom) nodata : numeric optional; locations where heads=nodata will be assigned T=0 Returns ------- T : 2D array of same shape as heads (nlay x n locations) Transmissivities in each layer at each location """ if i is not None and j is not None: pass elif x is not None and y is not None: # get row, col for observation locations i, j = get_ij(m.modelgrid, x, y) else: raise ValueError('Must specify row, column or x, y locations.') botm = m.dis.botm.array[:, i, j] if heads is None: # use model top elevations; expand to nlay x n locations heads = np.repeat(m.dis.top.array[np.newaxis, i, j], m.nlay, axis=0) if heads.shape == (m.nlay, m.nrow, m.ncol): heads = heads[:, i, j] msg = 'Shape of heads array must be nlay x n locations' assert heads.shape == botm.shape, msg # set open interval tops/bottoms to model top/bottom if None if screen_top is None: screen_top = m.dis.top.array[i, j] if screen_botm is None: screen_botm = m.dis.botm.array[-1, i, j] # make an array of layer tops tops = np.empty_like(botm, dtype=float) tops[0, :] = m.dis.top.array[i, j] tops[1:, :] = botm[:-1] # expand top and bottom arrays to be same shape as botm, thickness, etc. # (so we have an open interval value for each layer) sctoparr = np.zeros(botm.shape) sctoparr[:] = screen_top scbotarr = np.zeros(botm.shape) scbotarr[:] = screen_botm # start with layer tops # set tops above heads to heads # set tops above screen top to screen top # (we only care about the saturated open interval) openinvtop = tops.copy() openinvtop[openinvtop > heads] = heads[openinvtop > heads] openinvtop[openinvtop > sctoparr] = sctoparr[openinvtop > screen_top] # start with layer bottoms # set bottoms below screened interval to screened interval bottom # set screen bottoms below bottoms to layer bottoms openinvbotm = botm.copy() openinvbotm[openinvbotm < scbotarr] = scbotarr[openinvbotm < screen_botm] openinvbotm[scbotarr < botm] = botm[scbotarr < botm] # compute thickness of open interval in each layer thick = openinvtop - openinvbotm # assign open intervals above or below model to closest cell in column not_in_layer = np.sum(thick < 0, axis=0) not_in_any_layer = not_in_layer == thick.shape[0] for i, n in enumerate(not_in_any_layer): if n: closest = np.argmax(thick[:, i]) thick[closest, i] = 1. thick[thick < 0] = 0 thick[heads == nodata] = 0 # exclude nodata cells return thick
def setup_wel_data(model, for_external_files=True): """Performs the part of well package setup that is independent of MODFLOW version. Returns a DataFrame with the information needed to set up stress_period_data. """ # default options for distributing fluxes vertically vfd_defaults = { 'across_layers': False, 'distribute_by': 'thickness', 'screen_top_col': 'screen_top', 'screen_botm_col': 'screen_botm', 'minimum_layer_thickness': model.cfg['wel'].get('minimum_layer_thickness', 2.) } # master dataframe for stress period data columns = ['per', 'k', 'i', 'j', 'q', 'boundname'] df = pd.DataFrame(columns=columns) # check for source data datasets = model.cfg['wel'].get('source_data') # delete the dropped wells file if it exists, to avoid confusion dropped_wells_file = model.cfg['wel']['output_files'][ 'dropped_wells_file'].format(model.name) if os.path.exists(dropped_wells_file): os.remove(dropped_wells_file) # get well package input from source (parent) model in lieu of source data # todo: fetching correct well package from mf6 parent model if datasets is None and model.cfg['parent'].get('default_source_data') \ and hasattr(model.parent, 'wel'): # get well stress period data from mfnwt or mf6 model parent = model.parent spd = get_package_stress_period_data(parent, package_name='wel') # map the parent stress period data to inset stress periods periods = spd.groupby('per') dfs = [] for inset_per, parent_per in model.parent_stress_periods.items(): if parent_per in periods.groups: period = periods.get_group(parent_per) if len(dfs) > 0 and period.drop('per', axis=1).equals( dfs[-1].drop('per', axis=1)): continue else: dfs.append(period) spd = pd.concat(dfs) parent_well_i = spd.i.copy() parent_well_j = spd.j.copy() parent_well_k = spd.k.copy() # set boundnames based on well locations in parent model parent_name = parent.name spd['boundname'] = [ '{}_({},{},{})'.format(parent_name, pk, pi, pj) for pk, pi, pj in zip(parent_well_k, parent_well_i, parent_well_j) ] parent_well_x = parent.modelgrid.xcellcenters[parent_well_i, parent_well_j] parent_well_y = parent.modelgrid.ycellcenters[parent_well_i, parent_well_j] coords = project((parent_well_x, parent_well_y), model.modelgrid.proj_str, parent.modelgrid.proj_str) geoms = [Point(x, y) for x, y in zip(*coords)] bounds = model.modelgrid.bbox within = [g.within(bounds) for g in geoms] i, j = get_ij(model.modelgrid, parent_well_x[within], parent_well_y[within]) spd = spd.loc[within].copy() spd['i'] = i spd['j'] = j df = df.append(spd) # read source data and map onto model space and time discretization # multiple types of source data can be submitted elif datasets is not None: for k, v in datasets.items(): # determine the format if 'csvfile' in k.lower(): # generic csv # read csv file and aggregate flow rates to model stress periods # sum well fluxes co-located in a cell sd = TransientTabularSourceData.from_config( v, resolve_duplicates_with='sum', dest_model=model) csvdata = sd.get_data() csvdata.rename(columns={ v['data_column']: 'q', v['id_column']: 'boundname' }, inplace=True) if 'k' not in csvdata.columns: if model.nlay > 1: vfd = vfd_defaults.copy() vfd.update(v.get('vertical_flux_distribution', {})) csvdata = assign_layers_from_screen_top_botm( csvdata, model, **vfd) else: csvdata['k'] = 0 df = df.append(csvdata[columns]) elif k.lower() == 'wells': # generic dict added_wells = {k: v for k, v in v.items() if v is not None} if len(added_wells) > 0: aw = pd.DataFrame(added_wells).T aw['boundname'] = aw.index else: aw = None if aw is not None: if 'x' in aw.columns and 'y' in aw.columns: aw['i'], aw['j'] = get_ij(model.modelgrid, aw['x'].values, aw['y'].values) aw['per'] = aw['per'].astype(int) aw['k'] = aw['k'].astype(int) df = df.append(aw) elif k.lower() == 'wdnr_dataset': # custom input format for WI DNR # Get steady-state pumping rates check_source_files([v['water_use'], v['water_use_points']]) # fill out period stats period_stats = v['period_stats'] if isinstance(period_stats, str): period_stats = { kper: period_stats for kper in range(model.nper) } # separate out stress periods with period mean statistics vs. # those to be resampled based on start/end dates resampled_periods = { k: v for k, v in period_stats.items() if v == 'resample' } periods_with_dataset_means = { k: v for k, v in period_stats.items() if k not in resampled_periods } if len(periods_with_dataset_means) > 0: wu_means = get_mean_pumping_rates( v['water_use'], v['water_use_points'], period_stats=periods_with_dataset_means, drop_ids=v.get('drop_ids'), model=model) df = df.append(wu_means) if len(resampled_periods) > 0: wu_resampled = resample_pumping_rates( v['water_use'], v['water_use_points'], drop_ids=v.get('drop_ids'), exclude_steady_state=True, model=model) df = df.append(wu_resampled) # boundary fluxes from parent model if model.perimeter_bc_type == 'flux': assert model.parent is not None, "need parent model for TMR cut" # boundary fluxes kstpkper = [(0, 0)] tmr = Tmr(model.parent, model) # parent periods to copy over kstpkper = [(0, per) for per in model.cfg['model']['parent_stress_periods']] bfluxes = tmr.get_inset_boundary_fluxes(kstpkper=kstpkper) bfluxes['boundname'] = 'boundary_flux' df = df.append(bfluxes) for col in ['per', 'k', 'i', 'j']: df[col] = df[col].astype(int) # drop any k, i, j locations that are inactive if model.version == 'mf6': inactive = model.dis.idomain.array[df.k.values, df.i.values, df.j.values] != 1 else: inactive = model.bas6.ibound.array[df.k.values, df.i.values, df.j.values] != 1 # record dropped wells in csv file # (which might contain wells dropped by other routines) if np.any(inactive): #inactive_i, inactive_j = df.loc[inactive, 'i'].values, df.loc[inactive, 'j'].values dropped = df.loc[inactive].copy() dropped = dropped.groupby(['k', 'i', 'j']).first().reset_index() dropped['reason'] = 'in inactive cell' dropped['routine'] = __name__ + '.setup_wel_data' append_csv(dropped_wells_file, dropped, index=False, float_format='%g') # append to existing file if it exists df = df.loc[~inactive].copy() copy_fluxes_to_subsequent_periods = False if copy_fluxes_to_subsequent_periods and len(df) > 0: df = copy_fluxes_to_subsequent_periods(df) wel_lookup_file = model.cfg['wel']['output_files']['lookup_file'].format( model.name) wel_lookup_file = os.path.join(model._tables_path, os.path.split(wel_lookup_file)[1]) model.cfg['wel']['output_files']['lookup_file'] = wel_lookup_file # verify that all wells have a boundname if df.boundname.isna().any(): no_name = df.boundname.isna() k, i, j = df.loc[no_name, ['k', 'i', 'j']].T.values names = ['({},{},{})'.format(k, i, j) for k, i, j in zip(k, i, j)] df.loc[no_name, 'boundname'] = names assert not df.boundname.isna().any() # save a lookup file with well site numbers/categories df.sort_values(by=['boundname', 'per'], inplace=True) df[['per', 'k', 'i', 'j', 'q', 'boundname']].to_csv(wel_lookup_file, index=False) # convert to one-based and comment out header if df will be written straight to external file if for_external_files: df.rename(columns={'k': '#k'}, inplace=True) df['#k'] += 1 df['i'] += 1 df['j'] += 1 return df
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
def __init__( self, parent_model, inset_model, parent_head_file=None, parent_cell_budget_file=None, parent_length_units=None, inset_length_units=None, inset_parent_layer_mapping=None, inset_parent_period_mapping=None, ): self.inset = inset_model self.parent = parent_model self.inset._set_parent_modelgrid() self.cbc = None self._inset_parent_layer_mapping = inset_parent_layer_mapping self._source_mask = None self._inset_parent_period_mapping = inset_parent_period_mapping self.hpth = None # path to parent heads output file self.cpth = None # path to parent cell budget output file self.pi0 = None self.pj0 = None self.pi1 = None self.pj1 = None self.pi_list = None self.pj_list = None if parent_length_units is None: parent_length_units = self.inset.cfg['parent']['length_units'] if inset_length_units is None: inset_length_units = self.inset.length_units self.length_unit_conversion = convert_length_units( parent_length_units, inset_length_units) if parent_head_file is None: parent_head_file = os.path.join(self.parent.model_ws, '{}.hds'.format(self.parent.name)) if os.path.exists(parent_head_file): self.hpth = parent_cell_budget_file else: self.hpth = parent_head_file if parent_cell_budget_file is None: for extension in 'cbc', 'cbb': parent_cell_budget_file = os.path.join( self.parent.model_ws, '{}.{}'.format(self.parent.name, extension)) if os.path.exists(parent_cell_budget_file): self.cpth = parent_cell_budget_file break else: self.cpth = parent_cell_budget_file if self.hpth is None and self.cpth is None: raise ValueError( "No head or cell budget output files found for parent model {}" .format(self.parent.name)) # get bounding cells in parent model for pfl_nwt model irregular_domain = False # see if irregular domain irregbound_cfg = self.inset.cfg['perimeter_boundary'].get( 'source_data', {}).get('irregular_boundary') if irregbound_cfg is not None: irregular_domain = True irregbound_cfg['variable'] = 'perimeter_boundary' irregbound_cfg['dest_model'] = self.inset sd = ArraySourceData.from_config(irregbound_cfg) data = sd.get_data() idm_outline = data[0] connections = get_horizontal_connections(idm_outline, connection_info=False, layer_elevations=1, delr=1, delc=1, inside=True) self.pi_list, self.pj_list = connections.i.to_list( ), connections.j.to_list() # otherwise just get the corners of the inset if rectangular domain else: self.pi0, self.pj0 = get_ij( self.parent.modelgrid, self.inset.modelgrid.xcellcenters[0, 0], self.inset.modelgrid.ycellcenters[0, 0]) self.pi1, self.pj1 = get_ij( self.parent.modelgrid, self.inset.modelgrid.xcellcenters[-1, -1], self.inset.modelgrid.ycellcenters[-1, -1]) self.parent_nrow_in_inset = self.pi1 - self.pi0 + 1 self.parent_ncol_in_inset = self.pj1 - self.pj0 + 1 # check for an even number of pfl_nwt cells per parent cell in x and y directions x_refinment = self.parent.modelgrid.delr[ 0] / self.inset.modelgrid.delr[0] y_refinment = self.parent.modelgrid.delc[ 0] / self.inset.modelgrid.delc[0] assert int( x_refinment ) == x_refinment, "pfl_nwt delr must be factor of parent delr" assert int( y_refinment ) == y_refinment, "pfl_nwt delc must be factor of parent delc" assert x_refinment == y_refinment, "grid must have same x and y discretization" self.refinement = int(x_refinment)
# check that lakes were set up properly if not simulate_high_k_lakes: assert not np.any(m._isbc2d == 2) assert upw.hk.array.max() < m.cfg['high_k_lakes']['high_k_value'] assert upw.sy.array.min() < m.cfg['high_k_lakes']['sy'] assert upw.ss.array.min() > m.cfg['high_k_lakes']['ss'] else: assert np.any(m._isbc2d == 2) assert upw.hk.array.max() == m.cfg['high_k_lakes']['high_k_value'] assert upw.sy.array.max() == m.cfg['high_k_lakes']['sy'] assert np.allclose(upw.ss.array.min(), m.cfg['high_k_lakes']['ss']) # compare values to parent model for var in ['hk', 'vka']: ix, iy = m.modelgrid.xcellcenters.ravel(), m.modelgrid.ycellcenters.ravel() pi, pj = get_ij(m.parent.modelgrid, ix, iy) parent_layer = {0: 0, 1: 0, 2: 1, 3: 2, 4: 3} for k, pk in parent_layer.items(): parent_vals = m.parent.upw.__dict__[var].array[pk, pi, pj] inset_vals = upw.__dict__[var].array valid_parent = parent_vals != m.cfg['high_k_lakes'].get('high_k_value', -9999) valid_inset = inset_vals[k].ravel() != m.cfg['high_k_lakes'].get('high_k_value', -9999) parent_vals = parent_vals[valid_parent & valid_inset] inset_vals = inset_vals[k].ravel()[valid_parent & valid_inset] assert np.allclose(parent_vals, inset_vals, rtol=0.01) elif case == 1: # test changing vka to anisotropy m.cfg['upw']['layvka'] = [1, 1, 1, 1, 1] m.cfg['upw']['vka'] = [10, 10, 10, 10, 10] upw = m.setup_upw()
def setup_head_observations(model, obs_info_files=None, format='hyd', obsname_column='obsname'): self = model package = format source_data_config = self.cfg[package]['source_data'] # set a 14 character obsname limit for the hydmod package # https://water.usgs.gov/ogw/modflow-nwt/MODFLOW-NWT-Guide/index.html?hyd.htm # 40 character limit for MODFLOW-6 (see IO doc) obsname_character_limit = 40 if format == 'hyd': obsname_character_limit = 14 # TODO: read head observation data using TabularSourceData instead if obs_info_files is None: for key in 'filename', 'filenames': if key in source_data_config: obs_info_files = source_data_config[key] if obs_info_files is None: print("No data for the Observation (OBS) utility.") return # get obs_info_files into dictionary format # filename: dict of column names mappings if isinstance(obs_info_files, str): obs_info_files = [obs_info_files] if isinstance(obs_info_files, list): obs_info_files = { f: self.cfg[package]['default_columns'] for f in obs_info_files } elif isinstance(obs_info_files, dict): for k, v in obs_info_files.items(): if v is None: obs_info_files[k] = self.cfg[package]['default_columns'] check_source_files(obs_info_files.keys()) # dictionaries mapping from obstypes to hydmod input pckg = { 'LK': 'BAS', # head package for high-K lakes; lake package lakes get dropped 'GW': 'BAS', 'head': 'BAS', 'lake': 'BAS', 'ST': 'SFR', 'flux': 'SFR' } arr = { 'LK': 'HD', # head package for high-K lakes; lake package lakes get dropped 'GW': 'HD', 'ST': 'SO', 'flux': 'SO' } print('Reading observation files...') dfs = [] for f, column_info in obs_info_files.items(): print(f) column_mappings = self.cfg[package]['source_data'].get( 'column_mappings') df = read_observation_data(f, column_info, column_mappings=column_mappings) if 'obs_type' in df.columns and 'pckg' not in df.columns: df['pckg'] = [pckg.get(s, 'BAS') for s in df['obs_type']] elif 'pckg' not in df.columns: df['pckg'] = 'BAS' # default to getting heads if 'obs_type' in df.columns and 'intyp' not in df.columns: df['arr'] = [arr.get(s, 'HD') for s in df['obs_type']] elif 'arr' not in df.columns: df['arr'] = 'HD' df['intyp'] = ['I' if p == 'BAS' else 'C' for p in df['pckg']] df[obsname_column] = df[obsname_column].astype(str).str.lower() dfs.append( df[['pckg', 'arr', 'intyp', 'x', 'y', obsname_column, 'file']]) df = pd.concat(dfs, axis=0) print('\nCulling observations to model area...') df['geometry'] = [Point(x, y) for x, y in zip(df.x, df.y)] within = [g.within(self.bbox) for g in df.geometry] df = df.loc[within].copy() print( 'Dropping head observations that coincide with Lake Package Lakes...') i, j = get_ij(self.modelgrid, df.x.values, df.y.values) islak = self.lakarr[0, i, j] != 0 df['i'], df['j'] = i, j df = df.loc[~islak].copy() drop_obs = self.cfg[package].get('drop_observations', []) if len(drop_obs) > 0: print('Dropping head observations specified in {}...'.format( self.cfg.get('filename', 'config file'))) df = df.loc[~df[obsname_column].astype(str).isin(drop_obs)] # make unique observation names for each model layer; applying the character limit # preserve end of obsname, truncating initial characters as needed # (for observations based on lat-lon coordinates such as usgs, or other naming schemes # where names share leading characters) prefix_character_limit = obsname_character_limit # - 2 df[obsname_column] = [ obsname[-prefix_character_limit:] for obsname in df[obsname_column] ] duplicated = df[obsname_column].duplicated(keep=False) # check for duplicate names after truncation if duplicated.sum() > 0: print( 'Warning- {} duplicate observation names encountered. First instance of each name will be used.' .format(duplicated.sum())) print(df.loc[duplicated, [obsname_column, 'file']]) # make sure every head observation is in each layer non_heads = df.loc[df.arr != 'HD'].copy() heads = df.loc[df.arr == 'HD'].copy() heads0 = heads.groupby(obsname_column).first().reset_index() heads0[obsname_column] = heads0[obsname_column].astype(str) heads_all_layers = pd.concat([heads0] * self.nlay).sort_values(by=obsname_column) heads_all_layers['klay'] = list(range(self.nlay)) * len(heads0) heads_all_layers[obsname_column] = [ '{}'.format(obsname) # _{:.0f}'.format(obsname, k) for obsname, k in zip(heads_all_layers[obsname_column], heads_all_layers['klay']) ] df = pd.concat([heads_all_layers, non_heads], axis=0) # dtypes assert df[obsname_column].dtype == np.object df['klay'] = df.klay.astype(int) if format == 'hyd': # get model locations xl, yl = self.modelgrid.get_local_coords(df.x.values, df.y.values) df['xl'] = xl df['yl'] = yl # drop observations located in inactive cels ibdn = model.bas6.ibound.array[df.klay.values, df.i.values, df.j.values] active = ibdn == 1 df.drop(['i', 'j'], axis=1, inplace=True) elif format == 'obs': # mf6 observation utility obstype = {'BAS': 'HEAD'} renames = {'pckg': 'obstype'} df.pckg.replace(obstype, inplace=True) df.rename(columns=renames, inplace=True) df['id'] = list(zip(df.klay, df.i, df.j)) # drop observations located in inactive cels idm = model.idomain[df.klay.values, df.i.values, df.j.values] active = idm == 1 df.drop(['arr', 'intyp', 'i', 'j'], axis=1, inplace=True) df = df.loc[active].copy() return df