def arc_ascii_3070(test_output_path, rotation=0): filename = os.path.join(test_output_path, 'test_arcascii_3070.asc') array = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) dx = 5. xll, yll = 0., 0. write_raster(filename, array, xll=xll, yll=yll, dx=dx, dy=None, rotation=rotation, proj_str='epsg:3070', nodata=-9999) return filename
def geotiff_3070(test_output_path, rotation=0): filename = os.path.join(test_output_path, 'test_raster_3070.tif') array = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) dx = 5. xll, yll = 0., 0. write_raster(filename, array, xll=xll, yll=yll, dx=dx, dy=None, rotation=rotation, crs='epsg:3070', nodata=-9999) return filename
def test_write_raster_crs(crs, test_output_path): filename = os.path.join(test_output_path, 'test_raster.tif') array = np.array([[0, 1], [2, 3]]) height, width = array.shape dx = 5. xll, yll = 0., 0. write_raster(filename, array, xll=xll, yll=yll, dx=dx, dy=None, rotation=0, crs=crs, nodata=-9999) if crs is not None: with rasterio.open(filename) as src: written_crs = get_authority_crs(src.crs) assert written_crs == get_authority_crs(crs)
def arc_ascii(test_output_path): filename = os.path.join(test_output_path, 'test_raster.asc') array = np.array([[0, 1], [2, 3]]) height, width = array.shape dx = 5. xll, yll = 0., 0. rotation = 0. write_raster(filename, array, xll=xll, yll=yll, dx=dx, dy=None, rotation=rotation, nodata=-9999) xul = _xll_to_xul(xll, height * dx, rotation) yul = _yll_to_yul(yll, height * dx, rotation) transform = get_transform(xul=xul, yul=yul, dx=dx, dy=-dx, rotation=rotation) return filename, transform
def geotiff(test_output_path, rotation=45.): filename = os.path.join(test_output_path, 'test_raster.tif') array = np.array([[0, 1], [2, 3]]) height, width = array.shape dx = 5. xll, yll = 0., 0. write_raster(filename, array, xll=xll, yll=yll, dx=dx, dy=None, rotation=rotation, proj_str='epsg:3070', nodata=-9999) xul = _xll_to_xul(xll, height * dx, rotation) yul = _yll_to_yul(yll, height * dx, rotation) transform = get_transform(xul=xul, yul=yul, dx=dx, dy=-dx, rotation=rotation) return filename, transform
def setup_model_layers(dem_means_raster, facies_classes_netcdf, framework_rasters, modelgrid, facies_class_variable='fac_a', dem_elevation_units='meters', framework_raster_elevation_units='meters', model_length_units='meters', output_folder='.', framework_unit_names=None): """Generate model layering and property zones from voxel-based zone numbers at uniform depths and raster surfaces that represent hydrogeologic contacts. The voxel-based zone numbers may represent hydrogeologically different sediments, as determined from an airborne electromagnetic survey, and are assumed to have priority over the raster surfaces. The model top is set at the land surface (DEM). Subsequent cell bottoms beneath the land surface are set based on the voxel depths subtracted from the DEM elevations, as sampled at the grid cell centers. Voxel cells without valid data are filled with zone numbers based on their position within the raster surfaces of hydrogeologic contacts. For example, a voxel cell that lies above all of the raster surfaces would be assigned a zone number equal to the highest voxel-based zone number + 3 (an arbitrary gap in the zone numbering between the voxel data and the raster surfaces). Similarly, beneath the lowest voxel cell at each location, grid cells are assigned zone values based on their vertical position relative to the raster surfaces. Below the voxel data, botm_array and their bottom elevations correspond to the framework_raster surfaces. The lowermost (last) raster surface forms the model bottom. Parameters ---------- dem_means_raster : str (filepath) Raster representing the land surface, at the highest resolution being contemplated for the model. Usually this is derived by sampling a higher resolution DEM using zonal statistics, taking the mean DEM value for each model cell. facies_classes_netcdf : str (filepath) NetCDF file with the voxel zone data, with a facies_class_variable that contains the zone numbers. framework_rasters : list of strings (filepaths) Raster surfaces describing hydrogelogic contacts surrounding the voxel data. modelgrid : mfsetup.MFsetupGrid instance Modflow-setup grid instance describing the model grid facies_class_variable : str, optional Variable in facies_classes_netcdf containing the zone information, by default 'fac_a' dem_elevation_units : str, optional Elevation units of dem_means_raster, by default 'meters' framework_raster_elevation_units : str, optional Elevation units of the framework_rasters, by default 'meters' model_length_units : str, optional Length units used in the model, by default 'meters' output_folder : str, optional Location where results will be saved, by default '.' framework_unit_names : list, optional Unit names for the framework_rasters, by default None Returns ------- botm_array : 3D numpy array Array of layer elevations, starting with the model top. (Length equal to the number of botm_array + 1) zone_array : 3D numpy array Array of zone numbers, including the values from facies_classes_netcdf, followed by a gap of 3 and then the numbers for the framework_rasters. For example, if there are 5 zones in facies_classes_netcdf, and 3 raster surfaces that intersect cells outside of the voxel data, zones 1-5 would correspond to the voxel zones, and zones 8-10 would correspond to hydrogeologic units bounded by the framework_rasters. Notes ----- In addition to the variables returned, the results are written to text-array and GeoTiff files within the output_folder. Two PDFs of figures are also generated- one showing the layering in cross sections at regular intervals throughout the model grid, and one showing the zones within each layer in plan view. """ # output folders figures_folder = os.path.join(output_folder, 'figures') layers_folder = os.path.join(output_folder, 'botm_array') zones_folder = os.path.join(output_folder, 'zones') # zone rasters are saved to zones_folder/rasters output_folders = [ figures_folder, layers_folder, zones_folder, os.path.join(zones_folder, 'rasters'), ] for folder in output_folders: if not os.path.isdir(folder): os.makedirs(folder) # output PDF with cross sections out_xsection_pdf = os.path.join(figures_folder, 'zone_xsections.pdf') out_maps_pdf = os.path.join(figures_folder, 'zone_maps.pdf') # flopy model grid grid = modelgrid # read in the facies classes and resisitivity values # from framework team # and verify that the grid is aligned with the nhg ds = xr.load_dataset(facies_classes_netcdf) assert point_is_on_nhg(ds.x, ds.y, offset='center') # read in the dem and framework rasters and fill locations of no data # with the next valid elevation above filled_framework_layers = rasters_to_grid( modelgrid, dem_means_raster, framework_rasters, dem_elevation_units=dem_elevation_units, raster_elevation_units=framework_raster_elevation_units, dest_elevation_units=model_length_units) dem_means = filled_framework_layers[0] framework_layer_botms_filled = filled_framework_layers[1:] # make AEM framework z edge elevations by subtracting off depths from model top voxel_z_edges = [] for depth in ds.zb.values: voxel_z_edges.append(dem_means - depth) # get the elevations of the voxel centers as well voxel_z_centers = [] for depth in ds.z.values: voxel_z_centers.append(dem_means - depth) voxel_z_centers = np.array(voxel_z_centers) # get the resistivity facies for each model cell (nearest neighbor) voxel_array = ds[facies_class_variable].sel(x=grid.xcellcenters[0], y=grid.ycellcenters[:, 0], method='nearest') voxel_zones = np.unique(voxel_array.values[~np.isnan(voxel_array.values)]) # fill areas of voxel_array that don't have AEM data with zone values # representing units in the original MERAS framework framework_layer = layers_to_zones(framework_layer_botms_filled, voxel_z_centers) # start framework zone numbers after resistivity facies, +3 framework_zones = framework_layer + np.nanmax(voxel_array.values) + 3 voxel_array = voxel_array.values voxel_array[np.isnan(voxel_array)] = framework_zones[np.isnan(voxel_array)] # put the botm_array together layers = voxels_to_layers(voxel_array, z_edges=voxel_z_edges, model_top=dem_means, model_botm=framework_layer_botms_filled, no_data_value=0) # add framework zones below the voxel botm_array nlay, nrow, ncol = layers.shape nlay -= 1 # botm_array includes the model top framework_layers = list(range(voxel_array.shape[0], nlay)) first_framework_zone = framework_zones.min() n_framwork_zones = framework_layer_botms_filled.shape[0] framework_zone_numbers = np.arange(first_framework_zone, first_framework_zone + n_framwork_zones) # assume that the last len(framework_layers) are sequential framework_zones_below = [] for zone_number in framework_zone_numbers[-len(framework_layers):]: framework_zones_below.append(np.ones((nrow, ncol)) * zone_number) framework_zones_below = np.array(framework_zones_below) # make an array of all zones, including the resisitivity facies # and the zones representing the units in the original meras framework zone_array = np.vstack([voxel_array, framework_zones_below]) # plot cross sections framework_unit_labels = dict( zip(framework_zone_numbers.astype(int), framework_unit_names)) plot_cross_sections(layers, out_xsection_pdf, property_data=zone_array, voxel_start_layer=0, voxel_zones=voxel_zones, cmap='copper', voxel_cmap='viridis', unit_labels=framework_unit_labels) # plot maps showing distribution of zones in each layer plot_zone_maps(layers, out_maps_pdf, zones=zone_array, voxel_zones=voxel_zones, unit_labels=framework_unit_labels) # write out the layer elevation rasters write_raster(os.path.join(layers_folder, 'model_top.tif'), layers[0], xul=grid.xul, yul=grid.yul, dx=grid.delc[0], dy=-grid.delr[0], rotation=0., proj_str='epsg:5070', nodata=-9999, fieldname='elevation') for i, layer in enumerate(layers[1:]): write_raster(os.path.join(layers_folder, 'botm{}.tif'.format(i)), layer, xul=grid.xul, yul=grid.yul, dx=grid.delc[0], dy=-grid.delr[0], rotation=0., crs=grid.crs, nodata=-9999, fieldname='elevation') # write out zone arrays (in GeoTiff and text format) voxel_start_layer = 0 voxel_end_layer = voxel_start_layer + voxel_array.shape[0] for i, zones2d in enumerate(zone_array): np.savetxt(os.path.join(zones_folder, 'res_fac{}.dat'.format(i)), zones2d, fmt='%.0f') write_raster(os.path.join(zones_folder, 'rasters/res_fac{}.tif'.format(i)), zones2d, xul=grid.xul, yul=grid.yul, dx=grid.delc[0], dy=-grid.delr[0], rotation=0., crs=grid.crs, nodata=-9999, fieldname='elevation') return layers, zone_array
def setup_model_layers(dem_means_raster, facies_classes_netcdf, framework_rasters, modelgrid, facies_class_variable='facies-class', facies_zedge_variable='z-edges', dem_elevation_units='meters', framework_raster_elevation_units='meters', model_length_units='meters', output_folder='.', frac_valid_cells_thresh=0.50, framework_unit_names=None): """Generate model layering and property zones from voxel-based zone numbers at uniform depths and raster surfaces that represent hydrogeologic contacts. The voxel-based zone numbers may represent hydrogeologically different sediments, as determined from an airborne electromagnetic survey, and are assumed to have priority over the raster surfaces. The model top is set at the land surface (DEM). Subsequent cell bottoms beneath the land surface are set based on the voxel depths subtracted from the DEM elevations, as sampled at the grid cell centers. Voxel cells without valid data are filled with zone numbers based on their position within the raster surfaces of hydrogeologic contacts. For example, a voxel cell that lies above all of the raster surfaces would be assigned a zone number equal to the highest voxel-based zone number + 3 (an arbitrary gap in the zone numbering between the voxel data and the raster surfaces). Similarly, beneath the lowest voxel cell at each location, grid cells are assigned zone values based on their vertical position relative to the raster surfaces. Below the voxel data, botm_array and their bottom elevations correspond to the framework_raster surfaces. The lowermost (last) raster surface forms the model bottom. Parameters ---------- dem_means_raster : str (filepath) Raster representing the land surface, at the highest resolution being contemplated for the model. Usually this is derived by sampling a higher resolution DEM using zonal statistics, taking the mean DEM value for each model cell. facies_classes_netcdf : str (filepath) NetCDF file with the voxel zone data, with a facies_class_variable that contains the zone numbers. framework_rasters : list of strings (filepaths) Raster surfaces describing hydrogelogic contacts surrounding the voxel data. modelgrid : mfsetup.MFsetupGrid instance Modflow-setup grid instance describing the model grid facies_class_variable : str, optional Variable in facies_classes_netcdf containing the zone information, by default 'facies-class' facies_zedge_variable : str, optional Variable in facies_classes_netcdf containing the vertical depth edge information. by default 'z-edges' dem_elevation_units : str, optional Elevation units of dem_means_raster, by default 'meters' framework_raster_elevation_units : str, optional Elevation units of the framework_rasters, by default 'meters' model_length_units : str, optional Length units used in the model, by default 'meters' output_folder : str, optional Location where results will be saved, by default '.' frac_valid_cells_thresh : float between 0 and 1, optional Layers within facies_classes_netcdf must have at least this fraction of valid (not Nan) cells to be included. framework_unit_names : list, optional Unit names for the framework_rasters, by default None Returns ------- botm_array : 3D numpy array Array of layer elevations, starting with the model top. (Length equal to the number of botm_array + 1) zone_array : 3D numpy array Array of zone numbers, including the values from facies_classes_netcdf, followed by a gap of 3 and then the numbers for the framework_rasters. For example, if there are 5 zones in facies_classes_netcdf, and 3 raster surfaces that intersect cells outside of the voxel data, zones 1-5 would correspond to the voxel zones, and zones 8-10 would correspond to hydrogeologic units bounded by the framework_rasters. Notes ----- In addition to the variables returned, the results are written to text-array and GeoTiff files within the output_folder. Two PDFs of figures are also generated- one showing the layering in cross sections at regular intervals throughout the model grid, and one showing the zones within each layer in plan view. """ # output folders figures_folder = os.path.join(output_folder, 'figures') layers_folder = os.path.join(output_folder, 'botm_array') zones_folder = os.path.join(output_folder, 'zones') # zone rasters are saved to zones_folder/rasters output_folders = [ figures_folder, layers_folder, zones_folder, os.path.join(zones_folder, 'rasters'), ] for folder in output_folders: if not os.path.isdir(folder): os.makedirs(folder) # output PDF with cross sections out_xsection_pdf = os.path.join(figures_folder, 'zone_xsections.pdf') out_maps_pdf = os.path.join(figures_folder, 'zone_maps.pdf') # flopy model grid grid = modelgrid _, nrow, ncol = modelgrid.shape # read in the facies classes and resisitivity values # from framework team # and verify that the grid is aligned with the nhg ds = xr.load_dataset(facies_classes_netcdf) # check that cell corners are aligned with NHG x_edges = ds.x[:-1] - 0.5 * np.diff(ds.x) y_edges = ds.y[:-1] - 0.5 * np.diff(ds.y) assert point_is_on_nhg(x_edges, y_edges) # read in the dem and framework rasters and fill locations of no data # with the next valid elevation above filled_framework_layers = rasters_to_grid( modelgrid, dem_means_raster, framework_rasters, dem_elevation_units=dem_elevation_units, raster_elevation_units=framework_raster_elevation_units, dest_elevation_units=model_length_units) dem_means = filled_framework_layers[0] framework_layer_botms_filled = filled_framework_layers[1:] # make AEM framework z edge elevations by subtracting off depths from model top voxel_z_edges = [] # 1D vector of uniform depths if len(ds[facies_zedge_variable].values.shape) == 1: for depth in ds[facies_zedge_variable].values: voxel_z_edges.append(dem_means - depth) # elevations of the voxel cell centers voxel_z_centers = voxel_z_edges[:-1] + \ 0.5 * np.diff(voxel_z_edges, axis=0) nlay = len(voxel_z_centers) #depth_centers_3d = np.ones((nlay, nrow, ncol)) * \ # np.reshape(depth_centers, (nlay, 1, 1)) # 3D array of depths (edges) by cell else: depth_edges = ds[facies_zedge_variable].interp(x=grid.xcellcenters[0], y=grid.ycellcenters[:, 0], method='linear') voxel_z_edges = dem_means - depth_edges # elevations of the voxel cell centers voxel_z_centers = voxel_z_edges.values[:-1] + \ 0.5 * np.diff(voxel_z_edges.values, axis=0) # get the fraction of nan values in each layer frac_nan = voxel_z_edges.isnull().sum( axis=(1, 2)) / (voxel_z_edges[0].size) nan_thresh = 1 - frac_valid_cells_thresh voxel_z_edges = voxel_z_edges.loc[frac_nan < nan_thresh, :, :] # fill NaN values to the east and west with last value in those directions voxel_z_edges_filled = voxel_z_edges.ffill(dim='x') voxel_z_edges_filled = voxel_z_edges_filled.bfill(dim='x') # convert to numpy array so we can use boolean indexing voxel_z_edges_filled = voxel_z_edges_filled.values min_thickness = 2 # create a 3D boolean array indicated whether each cell # is at least the minimum thickness valid = np.vstack( (np.ones((1, *voxel_z_edges_filled.shape[1:])).astype(bool), np.diff(voxel_z_edges_filled, axis=0) > min_thickness)) # set bottom edges of cells below the minimum thickness to Nans voxel_z_edges_filled[~valid] = np.nan #pdf_xs = PdfPages('STM_res_depth_grad_mrvacut_v1_xsections.pdf') #for row in np.arange(voxel_z_edges_filled.shape[1])[::50]: # z = voxel_z_edges_filled[:, row, :] # x = grid.xcellcenters[row, :] # x = np.tile(x, (z.shape[0], 1)) # fig, ax = plt.subplots() # ax.plot(x.T, z.T) # ax.invert_yaxis() # ax.set_title(f'Row {row}') # pdf_xs.savefig() # plt.close() #pdf_xs.close() # get the resistivity facies for each model cell (nearest neighbor) voxel_array = ds[facies_class_variable].sel(x=grid.xcellcenters[0], y=grid.ycellcenters[:, 0], method='nearest') voxel_zones = np.unique(voxel_array.values[~np.isnan(voxel_array.values)]) # fill areas of voxel_array that don't have AEM data with zone values # representing units in the original MERAS framework framework_layer = layers_to_zones(framework_layer_botms_filled, voxel_z_centers) # start framework zone numbers after resistivity facies, +3 framework_zones = framework_layer + np.nanmax(voxel_array.values) + 3 voxel_array = voxel_array.values voxel_array[np.isnan(voxel_array)] = framework_zones[np.isnan(voxel_array)] # put the botm_array together layers = voxels_to_layers(voxel_array, z_edges=voxel_z_edges, model_top=dem_means, model_botm=framework_layer_botms_filled, no_data_value=0) # add framework zones below the voxel botm_array nlay, nrow, ncol = layers.shape nlay -= 1 # botm_array includes the model top framework_layers = list(range(voxel_array.shape[0], nlay)) first_framework_zone = framework_zones.min() n_framwork_zones = framework_layer_botms_filled.shape[0] framework_zone_numbers = np.arange(first_framework_zone, first_framework_zone + n_framwork_zones) # assume that the last len(framework_layers) are sequential framework_zones_below = [] for zone_number in framework_zone_numbers[-len(framework_layers):]: framework_zones_below.append(np.ones((nrow, ncol)) * zone_number) framework_zones_below = np.array(framework_zones_below) # make an array of all zones, including the resisitivity facies # and the zones representing the units in the original meras framework zone_array = np.vstack([voxel_array, framework_zones_below]) # plot cross sections framework_unit_labels = dict( zip(framework_zone_numbers.astype(int), framework_unit_names)) plot_cross_sections(layers, out_xsection_pdf, property_data=zone_array, voxel_start_layer=0, voxel_zones=voxel_zones, cmap='copper', voxel_cmap='viridis', unit_labels=framework_unit_labels) # plot maps showing distribution of zones in each layer plot_zone_maps(layers, out_maps_pdf, zones=zone_array, voxel_zones=voxel_zones, unit_labels=framework_unit_labels) # write out the layer elevation rasters write_raster(os.path.join(layers_folder, 'model_top.tif'), layers[0], xul=grid.xul, yul=grid.yul, dx=grid.delc[0], dy=-grid.delr[0], rotation=0., proj_str='epsg:5070', nodata=-9999, fieldname='elevation') for i, layer in enumerate(layers[1:]): write_raster(os.path.join(layers_folder, 'botm{}.tif'.format(i)), layer, xul=grid.xul, yul=grid.yul, dx=grid.delc[0], dy=-grid.delr[0], rotation=0., crs=grid.crs, nodata=-9999, fieldname='elevation') # write out zone arrays (in GeoTiff and text format) voxel_start_layer = 0 voxel_end_layer = voxel_start_layer + voxel_array.shape[0] for i, zones2d in enumerate(zone_array): np.savetxt(os.path.join(zones_folder, 'res_fac{}.dat'.format(i)), zones2d, fmt='%.0f') write_raster(os.path.join(zones_folder, 'rasters/res_fac{}.tif'.format(i)), zones2d, xul=grid.xul, yul=grid.yul, dx=grid.delc[0], dy=-grid.delr[0], rotation=0., crs=grid.crs, nodata=-9999, fieldname='elevation') return layers, zone_array