nfdata 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nfdata
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Compile and edit input data for the NanoFASE model
5
5
  License: BSD-3-Clause
6
6
  Author: Sam Harrison
@@ -18,9 +18,11 @@ Requires-Dist: geopandas (>=1.0.0,<2.0.0)
18
18
  Requires-Dist: netcdf4 (>=1.7.1.post1,<2.0.0)
19
19
  Requires-Dist: numpy (>=2.0.0,<3.0.0)
20
20
  Requires-Dist: pandas (>=2.2.2,<3.0.0)
21
+ Requires-Dist: pytest (>=8.3.5,<9.0.0)
21
22
  Requires-Dist: rasterio (>=1.3.10,<2.0.0)
22
23
  Requires-Dist: ruamel.yaml (>=0.18.6,<0.19.0)
23
24
  Requires-Dist: shapely (>=2.0.4,<3.0.0)
25
+ Requires-Dist: whitebox (>=2.3.6,<3.0.0)
24
26
  Project-URL: Documentation, https://nerc-ceh.github.io/nanofase
25
27
  Description-Content-Type: text/markdown
26
28
 
@@ -11,16 +11,38 @@ time:
11
11
  n: 365 # Number of timesteps to run the model for
12
12
  dt: 86400 # Length of each timestep in seconds
13
13
  start_date: 2015-01-01 # Start date for the model run
14
+ # If the `condition_dem` config option is present, the compiler will attempt to remove
15
+ # pits, depressions and flat areas from the DEM using WhiteboxTools'
16
+ # `breach_depressions_least_cost`.
17
+ condition_dem:
18
+ # If `save_dem_to_path` is set, the conditioned DEM will be saved to this path
19
+ # (even if the conditioning hasn't fully worked). If not, a temporary directory
20
+ # is used and the resulting DEM is deleted after the processing.
21
+ save_dem_to_path: ./dem.tif
22
+ # Additional config options specified here will be passed to `breach_depressions_least_cost`
23
+ # See https://www.whiteboxgeo.com/manual/wbt_book/available_tools/hydrological_analysis.html#BreachDepressionsLeastCost
24
+ # If a value for `callback` is provided, it is ignored as we implement our own callback to
25
+ # deal with errors. The only required (by Whitebox) parameter is `dist`, which specifies
26
+ # the maximum search distance for breach paths in cells. The default value used in nfdata is 10000.
27
+ dist: 10000
14
28
 
15
29
  # Meteorological and hydrological
16
- flow_dir: # Flow direction is responsible for defining the grid system
17
- type: raster # Type can be raster or csv (see docs)
30
+ # -------------------------------
31
+ # The model requires either a flow direction or DEM raster. If no flow direction
32
+ # is provided, the DEM will be used to calculate flow direction. If both are provided,
33
+ # the flow direction will be used. The DEM must either be conditioned (contain no pits,
34
+ # depressions or flat areas), or if the config option `condition_dem` is set to `True`,
35
+ # the compiler will attempt to condition the DEM using WhiteboxTools'
36
+ # `breach_depressions_least_cost`. This is not guaranteed to work, and therefore users
37
+ # might wish to condition the DEM themselves.
38
+ # `flow_dir` and `dem` must be rasters that are openable by Rasterio.
39
+ flow_dir:
18
40
  path: <root_dir>flow_dir.tif # Path to file, <root_dir> variable can be used from above
19
41
  crs: EPSG:27700 # Manually specify CRS if it is absent or ill-defined in file
20
- dem: # Digital Elevation Model, which defines the height above sea level
21
- type: raster # of each grid cell, in metres
42
+ dem:
43
+ type: raster
22
44
  units: dm
23
- path: <root_dir>dem.grd
45
+ path: <root_dir>dem.tif
24
46
  runoff: # Runoff, usually provided by a hydrological model
25
47
  type: csv
26
48
  path: <root_dir>runoff.csv
@@ -41,7 +63,7 @@ precip: # Precipitation
41
63
  # Soil
42
64
  # Most soil properties can be obtained from the European Soil Data Center (ESDAC): https://esdac.jrc.ec.europa.eu/
43
65
  soil_bulk_density:
44
- type: raster
66
+ type: raster # For spatial variables, only rasters are supported at the moment. Spatiotemporal variables can be CSVs
45
67
  units: t/m**3
46
68
  path: <root_dir>soil_bulk_density.tif
47
69
  # Source example:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "nfdata"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Compile and edit input data for the NanoFASE model"
5
5
  authors = ["Sam Harrison <samharrison.xg@gmail.com>"]
6
6
  license = "BSD-3-Clause"
@@ -24,8 +24,13 @@ netcdf4 = "^1.7.1.post1"
24
24
  shapely = "^2.0.4"
25
25
  Pint = "^0.24.1"
26
26
  f90nml = "^1.4.4"
27
+ pytest = "^8.3.5"
28
+ whitebox = "^2.3.6"
27
29
 
28
30
 
31
+ [tool.poetry.group.dev.dependencies]
32
+ flake8 = "^7.2.0"
33
+
29
34
  [build-system]
30
35
  requires = ["poetry-core"]
31
36
  build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,11 @@
1
+ class RoutingError(Exception):
2
+ """Exception raised for errors while routing waterbodies."""
3
+
4
+
5
+ def whitebox_callback(msg):
6
+ """
7
+ Callback function to catch Whitebox errors
8
+ """
9
+ if 'Error' in msg:
10
+ raise RoutingError(f'Whitebox raised an error: '
11
+ f'{msg}')
@@ -0,0 +1,52 @@
1
+ """
2
+ Utility functions for nfdata.
3
+ """
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Optional, Tuple, Union
7
+
8
+ import numpy as np
9
+ import rasterio
10
+ from affine import Affine
11
+ from rasterio.coords import BoundingBox
12
+ from rasterio.crs import CRS
13
+ from shapely.geometry import Polygon, box
14
+
15
+
16
+ @dataclass
17
+ class RasterInfo:
18
+ bounds: BoundingBox
19
+ crs: CRS
20
+ transform: Affine
21
+ res: Tuple[float, float]
22
+ width: int
23
+ height: int
24
+ dtype: str
25
+ count: int
26
+ shape: tuple
27
+ nodata: Optional[float]
28
+ bbox: Polygon
29
+ arr: Union[np.ndarray, np.ma.MaskedArray]
30
+
31
+
32
+ def extract_raster_info(path: Union[str, Path],
33
+ masked: bool = True) -> RasterInfo:
34
+ """
35
+ Extract data and info from a raster file and store
36
+ it in a RasterInfo dataclass so that it can be closed
37
+ """
38
+ with rasterio.open(path) as src:
39
+ return RasterInfo(
40
+ bounds=src.bounds,
41
+ crs=src.crs,
42
+ transform=src.transform,
43
+ res=src.res,
44
+ width=src.width,
45
+ height=src.height,
46
+ dtype=src.dtypes[0],
47
+ count=src.count,
48
+ shape=src.shape,
49
+ nodata=src.nodata,
50
+ bbox=box(*src.bounds),
51
+ arr=src.read(1, masked=masked)
52
+ )
@@ -1,26 +1,34 @@
1
+ import importlib.resources as pkg_resources
2
+ import os
1
3
  import shutil
2
4
  import sys
3
- import os
4
- import importlib.resources as pkg_resources
5
- from netCDF4 import Dataset
5
+ from pathlib import Path
6
+
7
+ import f90nml
8
+ import geopandas as gpd
6
9
  import numpy as np
7
10
  import pandas as pd
8
- import geopandas as gpd
9
11
  import rasterio
12
+ from netCDF4 import Dataset
13
+ from pint import UnitRegistry
10
14
  from rasterio.crs import CRS
11
15
  from rasterio.mask import mask
12
- from rasterio.warp import reproject, Resampling
13
- from shapely.geometry import box
16
+ from rasterio.warp import Resampling, reproject
14
17
  from ruamel.yaml import YAML
15
- from pint import UnitRegistry
16
- import f90nml
17
- from .router import Router
18
+ from shapely.geometry import box
19
+
20
+ from . import routing
21
+ from ._utility import extract_raster_info
18
22
 
19
23
 
20
24
  class Compiler:
21
25
 
22
- def __init__(self, task, config_path, model_vars_path):
26
+ def __init__(self, task, config_path, model_vars_path=None):
23
27
  """Initialise the compiler, either to compile or edit the dataset."""
28
+ # If we haven't been provided a model vars file, use the one in the package
29
+ if model_vars_path is None:
30
+ model_vars_path = pkg_resources.files(__package__)\
31
+ .joinpath('model_vars.yaml')
24
32
  # Open the config and model_vars files
25
33
  self.yaml = YAML(typ='safe')
26
34
  with open(config_path, 'r') as config_file, open(model_vars_path, 'r') as model_vars_file:
@@ -56,12 +64,15 @@ class Compiler:
56
64
  self.vars_spatiotemporal.append(k)
57
65
  elif ('dims' in v) and (v['dims'] == ['p', 't', 'y', 'x']):
58
66
  self.vars_spatial_point.append(k)
59
- elif ('dims' in v) and (all(x in v['dims'] for x in ['y', 'x'])) and (len(v['dims']) == 3):
67
+ elif ('dims' in v) and \
68
+ (all(x in v['dims'] for x in ['y', 'x'])) and \
69
+ (len(v['dims']) == 3):
60
70
  self.vars_spatial_1d.append(k)
61
71
  # Setup the unit registry
62
72
  self.ureg = UnitRegistry()
63
73
  # Was a root directory specified?
64
- self.root_dir = self.config['root_dir'] if 'root_dir' in self.config else ''
74
+ self.root_dir = self.config['root_dir'] \
75
+ if 'root_dir' in self.config else ''
65
76
  # Create empty dict ready to save vars to memory
66
77
  self.saved_vars = {}
67
78
  # Do we want to compile or edit a dataset?
@@ -101,22 +112,29 @@ class Compiler:
101
112
  # Define the timestep as a unit, based on that given in config file
102
113
  self.ureg.define('timestep = {0} * second'.format(self.config['time']['dt']))
103
114
  # Set grid properties from the NetCDF file
104
- self.grid = rasterio.open(f'netcdf:{self.config["output_nc_file"]}:flow_dir')
115
+ self.grid = extract_raster_info(f'netcdf:{self.config["output_nc_file"]}:flow_dir',
116
+ masked=True)
105
117
  self.grid_crs = CRS.from_wkt(self.nc['crs'].crs_wkt)
106
118
  self.grid_bbox = box(*self.grid.bounds)
107
- self.grid_mask = np.ma.getmask(self.grid.read(1, masked=True))
119
+ self.grid_mask = np.ma.getmask(self.grid.arr)
108
120
 
109
121
  def create(self):
110
- """Compile the data from multiple input files, specified in the config file, to a NetCDF dataset for
111
- spatial and/or temporal data, and a Fortran namelist file for constants."""
122
+ """
123
+ Compile the data from multiple input files, specified in the config
124
+ file, to a NetCDF dataset for spatial and/or temporal data, and a
125
+ Fortran namelist file for constants.
126
+ """
112
127
  # Set up the dataset:
113
- # - Use flow direction raster to give us the grid system (bounds, CRS, etc)
128
+ # - Use flow direction or DEM rasters to give us the grid system (bounds, CRS, etc)
114
129
  # - Creating the NetCDF dataset with this grid information
115
130
  # - Set tidal bounds in lieu of routing
116
131
  # - Route water bodies (e.g. set inflows, outflows and headwaters) using the flow direction and tidal bounds
117
- print('Setting up dataset...\n'
118
- '\t...parsing flow_dir')
119
- self.parse_flow_dir()
132
+ print('Setting up dataset...\n')
133
+ # Set the flow direction either directly from the flow_dir raster,
134
+ # or if not provided, calculated from the dem raster. Also set the
135
+ # grid, CRS and grid bounding box
136
+ self.flow_dir, self.grid = self.parse_flow_dir()
137
+ print('\t...parsing flow_dir')
120
138
  print('\t...creating NetCDF file')
121
139
  self.setup_netcdf_dataset()
122
140
  self.parse_spatial_var('is_estuary', save=True)
@@ -184,72 +202,126 @@ class Compiler:
184
202
  = os.path.join(self.config['root_dir'], path.split('<root_dir>')[1])
185
203
 
186
204
  def parse_flow_dir(self):
187
- """Parse the flow direction variable and define the CRS and mask for other
188
- variables based on this. This step must be performed before NetCDF file is generated."""
189
- if self.config['flow_dir']['type'] == 'raster':
190
- self.grid = rasterio.open(self.config['flow_dir']['path'].replace('<root_dir>', self.config['root_dir']))
191
- # If a CRS has been specified for the flowdir raster, use this instead of the raster's
205
+ """
206
+ Either use the provided flow_dir raster, or if not presented,
207
+ calculate flow direction from the provided digital elevation
208
+ model
209
+ """
210
+ # If flow_dir has been provided, use it directly. Else, check that
211
+ # a dem has been provided and calculate the flow direction from that.
212
+ # If neither provided, error out
213
+ if 'flow_dir' in self.config:
214
+ # Open and read the flow_dir raster
215
+ flow_dir_path = self.config['flow_dir']['path'] \
216
+ .replace('<root_dir>', self.config['root_dir'])
217
+ grid = extract_raster_info(flow_dir_path, masked=True)
218
+ flow_dir = grid.arr
219
+ # If a CRS has been specified, use this instead of the raster's
192
220
  # internal CRS. This is useful if a raster has an ill-defined CRS
193
- self.grid_crs = CRS.from_user_input(self.config['flow_dir']['crs']) \
194
- if 'crs' in self.config['flow_dir'] else self.grid.crs
195
- # Only projected rasters allowed for the moment
196
- if self.grid_crs.is_geographic:
197
- print('Sorry, the flow_dir raster must be projected, not geographic. '
198
- 'I got a geographic CRS: \'{0}\'.'.format(self.grid_crs))
199
- sys.exit()
200
- # Create Shapely box from bounds, to clip other rasters to
201
- self.grid_bbox = box(*self.grid.bounds)
202
- elif self.config['flow_dir']['type'] == 'csv':
203
- print("CSV flow direction not currently supported. Please provide a raster file.")
204
- sys.exit()
205
-
221
+ if 'crs' in self.config['flow_dir']:
222
+ grid.crs = CRS.from_user_input(self.config['flow_dir']['crs'])
223
+ # Set a flag to say we used the flow_dir raster
224
+ var_used = 'flow_dir'
225
+ elif 'dem' in self.config:
226
+ # Path to the DEM
227
+ dem_path = Path(
228
+ self.config['dem']['path'].replace('<root_dir>',
229
+ self.config['root_dir'])
230
+ ).resolve()
231
+ # If we have been asked to condition the DEM, do this first
232
+ if 'condition_dem' in self.config:
233
+ dem_path_conditioned, temp_dir = routing.condition_dem(
234
+ dem_path,
235
+ self.config['condition_dem'],
236
+ )
237
+ else:
238
+ # If no conditioning is required, just use the original DEM
239
+ dem_path_conditioned = dem_path
240
+ # Open and read the DEM, unmasked so that the flow direction
241
+ # calculation routine can use the nodata values
242
+ grid = extract_raster_info(dem_path_conditioned, masked=False)
243
+ # Clean the temporary directory if we created one
244
+ if temp_dir is not None:
245
+ temp_dir.cleanup()
246
+ # If a CRS has been specified, use this instead of the raster's
247
+ # internal CRS. This is useful if a raster has an ill-defined CRS
248
+ if 'crs' in self.config['dem']:
249
+ grid.crs = CRS.from_user_input(self.config['dem']['crs'])
250
+ # Calculate the flow direction, which returns a masked array
251
+ flow_dir = routing.calculate_flow_dir(dem=grid.arr,
252
+ res=grid.res,
253
+ nodata=grid.nodata)
254
+ # Set a flag to say we used the dem raster from grid properties
255
+ var_used = 'dem'
206
256
  else:
207
- print("Unrecognised file type {0} for variable flow_dir. "
208
- "Type should be rs, csv or nc.".format(self.config['flow_dir']['type']))
209
- sys.exit()
257
+ # If neither flow_dir or dem have been provided, error out
258
+ raise Exception('Neither `flow_dir` nor `dem` rasters '
259
+ 'have been provided.')
260
+ # Only projected rasters allowed for the moment
261
+ if grid.crs.is_geographic:
262
+ raise Exception(f'The `{var_used}` raster must be projected, '
263
+ 'not geographic. I got a geographic CRS: '
264
+ f'{grid.crs}')
265
+ return flow_dir, grid
210
266
 
211
267
  def setup_netcdf_dataset(self):
212
- """Create NetCDF file, add required dimensions, coordinate variables and the
213
- flow direction variable."""
214
- self.nc = Dataset(self.config['output']['nc_file'], 'w', format='NETCDF4')
268
+ """
269
+ Create NetCDF file, add required dimensions, coordinate variables
270
+ and the flow direction variable.
271
+ """
272
+ self.nc = Dataset(self.config['output']['nc_file'], 'w',
273
+ format='NETCDF4')
215
274
  self.nc.title = "Input data for NanoFASE model"
216
- self.nc.nanomaterial = self.config['nanomaterial'] if 'nanomaterial' in self.config else 'Unknown'
275
+ self.nc.nanomaterial = self.config['nanomaterial'] \
276
+ if 'nanomaterial' in self.config else 'Unknown'
217
277
  self.nc.Conventions = 'CF-1.8'
218
- self.nc.coordinates = 'crs' # Needed for xarray to distinguish crs as a coordinate rather than a variable
278
+ # Setting the coordinates attribute is needed for xarray to
279
+ # distinguish crs as a coordinate rather than a variable
280
+ self.nc.coordinates = 'crs'
219
281
  crs_var = self.nc.createVariable('crs', 'i4')
220
- crs_var.spatial_ref = self.grid_crs.to_wkt() # QGIS/ArcGIS recognises spatial_ref to define CRS
221
- crs_var.crs_wkt = self.grid_crs.to_wkt() # Latest CF conventions say crs_wkt can be used
222
- crs_var.epsg_code = self.grid_crs.to_epsg() # Not standardised, but might be useful instead of having to decipher WKT
282
+ # QGIS/ArcGIS recognises spatial_ref to define the CRS
283
+ crs_var.spatial_ref = self.grid.crs.to_wkt()
284
+ # Latest CF conventions say crs_wkt can be used
285
+ crs_var.crs_wkt = self.grid.crs.to_wkt()
286
+ # Not standardised, but might be useful instead of having to
287
+ # decipher WKT
288
+ crs_var.epsg_code = self.grid.crs.to_epsg()
223
289
  # Time dimensions and coordinate variable
224
- t_dim = self.nc.createDimension('t', None)
290
+ _ = self.nc.createDimension('t', None)
225
291
  t = self.nc.createVariable('t', 'i4', ('t',))
226
- t.units = "seconds since {0} 00:00:00".format(self.config['time']['start_date'])
292
+ t.units = "seconds since {0} 00:00:00" \
293
+ .format(self.config['time']['start_date'])
227
294
  t.standard_name = 'time'
228
295
  t.calendar = 'gregorian'
229
- t[:] = [i*int(self.config['time']['dt']) for i in range(int(self.config['time']['n']))]
296
+ t[:] = [i*int(self.config['time']['dt'])
297
+ for i in range(int(self.config['time']['n']))]
230
298
  # x dimension and coordinate variable
231
- x_dim = self.nc.createDimension('x', self.grid.width)
299
+ _ = self.nc.createDimension('x', self.grid.width)
232
300
  x = self.nc.createVariable('x', 'f4', ('x',))
233
301
  x.units = 'm'
234
302
  x.standard_name = 'projection_x_coordinate'
235
303
  x.axis = 'X'
236
- x[:] = [self.grid.bounds.left + i * self.grid.res[0] + 0.5 * self.grid.res[0] for i in range(self.grid.width)]
304
+ x[:] = [self.grid.bounds.left + i * self.grid.res[0]
305
+ + 0.5 * self.grid.res[0] for i in range(self.grid.width)]
237
306
  # y dimension and coordinate variable
238
- y_dim = self.nc.createDimension('y', self.grid.height)
307
+ _ = self.nc.createDimension('y', self.grid.height)
239
308
  y = self.nc.createVariable('y', 'f4', ('y',))
240
309
  y.units = 'm'
241
310
  y.standard_name = 'projection_y_coordinate'
242
311
  y.axis = 'Y'
243
- y[:] = [self.grid.bounds.top - i * self.grid.res[1] - 0.5 * self.grid.res[1] for i in range(self.grid.height)]
244
- # Grid dimension (2D), max number of waterbodies per cell and grid bounds
245
- d_dim = self.nc.createDimension('d', 2)
246
- w_dim = self.nc.createDimension('w', 7)
247
- box_dim = self.nc.createDimension('box', 4)
248
- p_dim = self.nc.createDimension('p')
312
+ y[:] = [self.grid.bounds.top - i * self.grid.res[1]
313
+ - 0.5 * self.grid.res[1] for i in range(self.grid.height)]
314
+ # Other useful dimensions (grid, number of waterbodies, bounding
315
+ # box and point sources)
316
+ _ = self.nc.createDimension('d', 2)
317
+ _ = self.nc.createDimension('w', 7)
318
+ _ = self.nc.createDimension('box', 4)
319
+ _ = self.nc.createDimension('p')
249
320
  # Grid properties - shape
250
321
  grid_shape = self.nc.createVariable('grid_shape', 'i4', ('d',))
251
322
  grid_shape.units = ''
252
- grid_shape.long_name = 'number of grid cells along each (x,y) grid axis'
323
+ grid_shape.long_name = \
324
+ 'number of grid cells along each (x,y) grid axis'
253
325
  grid_shape[:] = self.grid.shape[::-1]
254
326
  # Grid resolution
255
327
  grid_res = self.nc.createVariable('grid_res', 'f4', ('d',))
@@ -261,9 +333,8 @@ class Compiler:
261
333
  grid_bounds.units = ''
262
334
  grid_bounds.long_name = 'bounding box of the grid'
263
335
  grid_bounds[:] = self.grid.bounds
264
- # Get the flow direction array from the raster
265
- self.flow_dir = self.grid.read(1, masked=True)
266
- # Use the extent of the flow direction array to create a mask for all other data
336
+ # Use the extent of the flow direction array to create a mask for all
337
+ # other data
267
338
  self.grid_mask = np.ma.getmask(self.flow_dir)
268
339
  nc_var = self.nc.createVariable('flow_dir', 'i4', ('y', 'x'))
269
340
  nc_var.long_name = 'flow direction of water in grid cell'
@@ -329,7 +400,7 @@ class Compiler:
329
400
  with rasterio.open(path) as rs:
330
401
  out_bounds = rs.bounds
331
402
  # Mask clips the raster to the grid bounding box or smaller
332
- out_img, _ = mask(rs, [self.grid_bbox], crop=True, filled=False)
403
+ out_img, _ = mask(rs, [self.grid.bbox], crop=True, filled=False)
333
404
  # The raster might still be smaller than the grid box, so let's check and
334
405
  # construct mask array of the correct size if so
335
406
  if out_bounds.left > self.grid.bounds.left or out_bounds.right < self.grid.bounds.right \
@@ -459,8 +530,11 @@ class Compiler:
459
530
  # Add this time step to the NetCDF file as a masked array
460
531
  nc_var[t-1, :, :] = values.magnitude
461
532
  else:
462
- print("Spatiotemporal variable ({0}) in raster format must be provided by one raster file per time step, with path denoted by /{t/}".format(var_name))
463
-
533
+ raise Exception(f'Spatiotemporal variable ({var_name}) in '
534
+ 'raster format must be provided by one raster '
535
+ 'file per time step, with the time step '
536
+ 'index denoted by /{t/} in the path.')
537
+
464
538
  elif var_dict['type'] == 'csv':
465
539
  df = pd.read_csv(var_dict['path'], header=0)
466
540
  # Loop through the timesteps and create pivot table to obtain spatial array for each
@@ -470,8 +544,10 @@ class Compiler:
470
544
  values = np.ma.masked_where(self.grid_mask, pt.values)
471
545
  # Check the pivot table's shape is that of the grid we're using
472
546
  if pt.shape != self.grid.shape:
473
- print("Inconsistent shape between {0} csv file and overall grid system ({1} and {2}). Check indices set correctly.".format(var, pt.shape, grid.shape))
474
- sys.exit()
547
+ raise Exception(f'Inconsistent shape between {var} csv '
548
+ f'file and overall grid system '
549
+ f'({pt.shape} and {self.grid.shape}). '
550
+ 'Check indices are set correctly.')
475
551
  # Should the array be clipped?
476
552
  if 'clip' in var_dict:
477
553
  try:
@@ -561,7 +637,7 @@ class Compiler:
561
637
  # Open the supplied land use raster
562
638
  # Open the raster and clip to extent of grid (defined by flowdir raster)
563
639
  with rasterio.open(self.vars['land_use']['path']) as rs:
564
- out_img, out_transform = mask(rs, [self.grid_bbox], crop=True, filled=False)
640
+ out_img, out_transform = mask(rs, [self.grid.bbox], crop=True, filled=False)
565
641
  src_arr = out_img[0]
566
642
 
567
643
  # Prepare a dict of lists (empty for the moment) to store arrays to be summed
@@ -597,7 +673,7 @@ class Compiler:
597
673
  for name, old_arr in nf_final_arrs.items():
598
674
  if old_arr.shape == src_arr.shape:
599
675
  # Re-read flowdir as reproject fills new_arr
600
- new_arr = self.grid.read(1, masked=True)
676
+ new_arr = self.flow_dir.copy()
601
677
  # Reproject. Remember we're not converting CRS here, so clc_rs
602
678
  # CRS can be used as src and dst
603
679
  reproject(
@@ -623,52 +699,75 @@ class Compiler:
623
699
  def routing(self):
624
700
  """Use the flow direction to route the waterbody network."""
625
701
  # Create the empty arrays to begin with a mask ready to be filled
626
- router = Router(self.flow_dir) # Set up the router to deal with common routing tasks
627
- outflow_arr = np.ma.zeros((*self.flow_dir.shape, 2), dtype=np.dtype('i2'))
628
- outflow_arr.mask = self.grid_mask # Set the grid mask
629
- inflows_arr = np.ma.array(np.ma.empty((*self.flow_dir.shape, 7, 2), dtype=np.dtype('i2')), mask=True) # Max of seven inflows
630
- n_waterbodies = np.ma.array(np.ma.empty(self.flow_dir.shape, dtype=np.dtype('i2')), mask=True)
631
- is_headwater = np.ma.array(np.ma.empty(self.flow_dir.shape, dtype=np.dtype('u1')), mask=True)
702
+ outflow_arr = np.ma.zeros((*self.flow_dir.shape, 2),
703
+ dtype=np.dtype('i2'))
704
+ # Set the grid mask
705
+ outflow_arr.mask = self.grid_mask
706
+ # Create the relevant empty arrays
707
+ inflows_arr = np.ma.array(np.ma.empty((*self.flow_dir.shape, 7, 2),
708
+ dtype=np.dtype('i2')),
709
+ mask=True)
710
+ n_waterbodies = np.ma.array(np.ma.empty(self.flow_dir.shape,
711
+ dtype=np.dtype('i2')),
712
+ mask=True)
713
+ is_headwater = np.ma.array(np.ma.empty(self.flow_dir.shape,
714
+ dtype=np.dtype('u1')),
715
+ mask=True)
632
716
 
633
717
  # Use the flow direction to set outflow and inflows to each cell
634
- for index, cell in np.ndenumerate(self.flow_dir):
718
+ for index, _ in np.ndenumerate(self.flow_dir):
635
719
  y, x = index[0] + 1, index[1] + 1
636
- if not self.grid_mask[index]: # Only for non-masked elements
637
- outflow_arr[index] = router.outflow_from_flow_dir(x, y)
638
- inflows_arr[index] = router.inflows_from_flow_dir(x, y)
639
- n_waterbodies[index], is_headwater[index]\
640
- = router.n_waterbodies_from_inflows(x, y, outflow_arr[index], inflows_arr[index])
641
-
720
+ # Only for non-masked elements
721
+ if not self.grid_mask[index]:
722
+ outflow_arr[index] = \
723
+ routing.outflow_from_flow_dir(self.flow_dir, x, y)
724
+ inflows_arr[index] = \
725
+ routing.inflows_from_flow_dir(self.flow_dir, x, y)
726
+ n_waterbodies[index], is_headwater[index] = \
727
+ routing.n_waterbodies_from_inflows(self.flow_dir,
728
+ outflow_arr[index],
729
+ inflows_arr[index])
730
+
642
731
  # Create NetCDF vars for these arrays. Firstly, outflow
643
- nc_var = self.nc.createVariable('outflow', np.dtype('i2'), ('y', 'x', 'd'))
732
+ nc_var = self.nc.createVariable('outflow',
733
+ np.dtype('i2'),
734
+ ('y', 'x', 'd'))
644
735
  nc_var.long_name = 'index of grid cell outflow'
645
736
  nc_var.units = ''
646
737
  nc_var.grid_mapping = 'crs'
647
738
  nc_var[:] = outflow_arr
648
739
 
649
740
  # Inflows
650
- nc_var = self.nc.createVariable('inflows', np.dtype('i2'), ('y', 'x', 'w', 'd'))
741
+ nc_var = self.nc.createVariable('inflows',
742
+ np.dtype('i2'),
743
+ ('y', 'x', 'w', 'd'))
651
744
  nc_var.long_name = 'indices of grid cell inflows'
652
745
  nc_var.units = ''
653
746
  nc_var.grid_mapping = 'crs'
654
747
  nc_var[:] = inflows_arr
655
748
 
656
749
  # Number of waterbodies per cell
657
- nc_var = self.nc.createVariable('n_waterbodies', np.dtype('i2'), ('y', 'x'))
750
+ nc_var = self.nc.createVariable('n_waterbodies',
751
+ np.dtype('i2'),
752
+ ('y', 'x'))
658
753
  nc_var.long_name = 'number of waterbodies in grid cell'
659
754
  nc_var.units = ''
660
755
  nc_var.grid_mapping = 'crs'
661
756
  nc_var[:] = n_waterbodies
662
757
 
663
758
  # Is cell a headwater?
664
- nc_var = self.nc.createVariable('is_headwater', np.dtype('u1'), ('y', 'x'))
759
+ nc_var = self.nc.createVariable('is_headwater',
760
+ np.dtype('u1'),
761
+ ('y', 'x'))
665
762
  nc_var.long_name = 'is this cell a headwater?'
666
763
  nc_var.units = ''
667
764
  nc_var.grid_mapping = 'crs'
668
765
  nc_var[:] = is_headwater
669
766
 
670
767
  def in_model_domain(self, point):
671
- """Check if a point is in the model domain."""
768
+ """
769
+ Check if a point is in the model domain.
770
+ """
672
771
  if (point.x >= self.grid.bounds.left)\
673
772
  and (point.x < self.grid.bounds.right)\
674
773
  and (point.y > self.grid.bounds.bottom)\
@@ -676,7 +775,7 @@ class Compiler:
676
775
  return True
677
776
  else:
678
777
  return False
679
-
778
+
680
779
  @staticmethod
681
780
  def create_constants(constants_yaml, output_path):
682
781
  """Method to just create a constants file, bypassing
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env python
2
- """NanoFASE data is responsible for compiling and editing data for use in NanoFASE model."""
3
- import sys
4
- import os
2
+ """
3
+ NanoFASE data is responsible for compiling and editing data for use in
4
+ the NanoFASE model.
5
+ """
5
6
  import argparse
6
7
  import importlib.resources as pkg_resources
8
+
7
9
  from .compiler import Compiler
8
10
 
9
11
 
@@ -0,0 +1,292 @@
1
+ """
2
+ Utilities relating to routing water bodies, such as creating
3
+ a flow direction grid from a digital elevation model
4
+ """
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import numpy as np
11
+ from whitebox.whitebox_tools import WhiteboxTools
12
+
13
+ from ._errors import RoutingError, whitebox_callback
14
+
15
+
16
+ def in_model_domain(flow_dir, i, j):
17
+ """
18
+ Check if index [j,i] is in the model domain.
19
+ """
20
+ i_in_domain = 0 <= i < flow_dir.mask.shape[1]
21
+ j_in_domain = 0 <= j < flow_dir.mask.shape[0]
22
+ not_masked = flow_dir[j, i] is not np.ma.masked \
23
+ if (i_in_domain and j_in_domain) else False
24
+ return i_in_domain and j_in_domain and not_masked
25
+
26
+
27
+ def outflow_from_flow_dir(flow_dir, x, y):
28
+ """
29
+ Get the outflow cell reference given the current cell
30
+ reference and a flow direction.
31
+ """
32
+ xy_out = {
33
+ 1: [x+1, y],
34
+ 2: [x+1, y+1],
35
+ 4: [x, y+1],
36
+ 8: [x-1, y+1],
37
+ 16: [x-1, y],
38
+ 32: [x-1, y-1],
39
+ 64: [x, y-1],
40
+ 128: [x+1, y-1]
41
+ }
42
+ return xy_out[flow_dir[y-1, x-1]]
43
+
44
+
45
+ def inflows_from_flow_dir(flow_dir, x, y):
46
+ """Get the inflow cell references given the current cell
47
+ reference and a flow direction."""
48
+ j, i = y - 1, x - 1
49
+ # Flow direction that means the cell with indices inflows[j_n-j, i_n-i]
50
+ # flows into this cell
51
+ inflow_flow_dir = {
52
+ (-1, -1): 2,
53
+ (0, -1): 1,
54
+ (1, -1): 128,
55
+ (-1, 0): 4,
56
+ (0, 0): 0,
57
+ (1, 0): 64,
58
+ (-1, 1): 8,
59
+ (0, 1): 16,
60
+ (1, 1): 32
61
+ }
62
+ inflow_cells = []
63
+ # Loop through the neighbours and check if they're inflows to this cell
64
+ for j_n in range(j-1, j+2):
65
+ for i_n in range(i-1, i+2):
66
+ if in_model_domain(flow_dir, i_n, j_n):
67
+ if flow_dir[j_n, i_n] == inflow_flow_dir[(j_n-j, i_n-i)]:
68
+ inflow_cells.append([i_n+1, j_n+1])
69
+ # Create masked array from the inflow_cells list
70
+ inflow_cells_ma = np.ma.array(np.ma.empty((7, 2), dtype=int), mask=True)
71
+ if len(inflow_cells) > 0: # Only fill if there are inflows
72
+ inflow_cells_ma[0:len(inflow_cells)] = inflow_cells
73
+ return inflow_cells_ma
74
+
75
+
76
+ def n_waterbodies_from_inflows(flow_dir, outflow, inflows):
77
+ """
78
+ Calculate the number of waterbodies from the inflows to the cell.
79
+ """
80
+ j_out, i_out = outflow[1] - 1, outflow[0] - 1
81
+ # Count the unmasked elements to get n_inflows
82
+ n_inflows = inflows.count(axis=0)[0]
83
+ # If there are no inflows but the outflow is to the model domain, it
84
+ # must be a headwater. Else, number of waterbodies is same as number of
85
+ # inflows
86
+ if n_inflows == 0 and in_model_domain(flow_dir, i_out, j_out):
87
+ n_waterbodies = 1
88
+ is_headwater = 1
89
+ else:
90
+ n_waterbodies = n_inflows
91
+ is_headwater = 0
92
+ return n_waterbodies, is_headwater
93
+
94
+
95
+ def calculate_flow_dir(dem,
96
+ res,
97
+ nodata: Optional[float] = np.nan,
98
+ treat_nodata_as_drain=True,
99
+ attempt_to_resolve=False,
100
+ lonely_cell_flowdir=1,
101
+ error_if_unresolved=True):
102
+ """
103
+ Calculate the D8 flow direction from a digital elevation model
104
+
105
+ Parameters
106
+ ----------
107
+ dem : array-like with dtype=float
108
+ The digital elevation model to calculate flow direction from
109
+ res : length-2 tuple
110
+ The (y, x) resolution of the DEM, used to calculate distances
111
+ for non-square grid cells
112
+ nodata : float, default=np.nan
113
+ The value in `dem` that should be treated as nodata. Defaults
114
+ to NaN
115
+ treat_nodata_as_drain : bool, default=True
116
+ Should nodata cells be treated as a drain, i.e. with lower
117
+ elevation than all other cells? If so, this forces the flow
118
+ direction towards nodata cells by setting their elevation to
119
+ -9999. This is particularly useful where nodata represents
120
+ waterbodies. The `dem` is padded with one pixel of nodata on
121
+ each side in order to calculate flow directions from edge cells,
122
+ and so this flag also affects whether water drains from `dem`
123
+ boundaries. If this is set to False, you may end up with
124
+ unresolved cells
125
+ attempt_to_resolve : bool, default=False
126
+ If the resulting flow direction raster has unresolved cells
127
+ (flow direction = 0), setting `attempt_to_resolve` to True uses
128
+ same flow direction as that from the neighbouring cell with the
129
+ steepest sloe to this cell, effectively presuming the slope carries
130
+ on in the same direction. Warning: this might result in
131
+ neighbouring cells that flow into each other, effectively creating
132
+ "pools" from which flows can't drain
133
+ lonely_cell_flowdir : int or np.nan, default=1
134
+ What flow direction to assign to individual cells surrounded by
135
+ nodata. By default, flow direction is assumed to be eastwards (1).
136
+ If `nodata` or `np.nan`, the cell is removed from the output
137
+ raster. Note that setting to 0 and specifying
138
+ `error_if_unresolved=True` will result in an error if your `dem`
139
+ has lonely cells. There are no further checks on the validity of
140
+ this value, so you can use this to flag lonely cells by setting to
141
+ an arbitrary value
142
+ error_if_unresolved : bool, default=True
143
+ Whether to raise an error if unresolved (flow direction = 0) cells
144
+ are encountered
145
+
146
+ Returns
147
+ -------
148
+ np.ma.masked_array(np.int16)
149
+ Masked flow direction array
150
+ """
151
+
152
+ # Mappings between D8 codes and directions
153
+ directions = np.array([
154
+ (-1, 1), # 128 - NE
155
+ (-1, 0), # 64 - N
156
+ (-1, -1), # 32 - NW
157
+ (0, -1), # 16 - W
158
+ (1, -1), # 8 - SW
159
+ (1, 0), # 4 - S
160
+ (1, 1), # 2 - SE
161
+ (0, 1), # 1 - E
162
+ ])
163
+ d8_codes = np.array([128, 64, 32, 16, 8, 4, 2, 1])
164
+ d8_codes_opposite = np.roll(d8_codes, 4)
165
+ # To make processing easier, we convert nodata values to NaNs for
166
+ # the DEM and -32768 for the flowdir array
167
+ dem = np.where(dem == nodata, np.nan, dem)
168
+ if (lonely_cell_flowdir == nodata) or np.isnan(lonely_cell_flowdir):
169
+ lonely_cell_flowdir = -32768
170
+ # In case there are data right to the edges, we pad with one pixel
171
+ # of NaNs
172
+ dem_padded = np.pad(dem, ((1, 1), (1, 1)), constant_values=np.nan)
173
+ # Get the number of (padded) rows and cols
174
+ nrows, ncols = dem_padded.shape
175
+ # Create a new array to store the (unpadded) flowdir in
176
+ flow_dir = np.full_like(dem, -32768, dtype=np.int16)
177
+ # If `treat_nodata_as_drain` is True, we presume that nodata cells
178
+ # (including the padded region) are water and should be at some
179
+ # negative elevation to force flow to drain into them. If False,
180
+ # they are simply ignored in the flow direction calculation
181
+ dem_adj = np.nan_to_num(dem_padded, nan=-np.inf) \
182
+ if treat_nodata_as_drain else dem_padded
183
+
184
+ # Loop over the cells (except the pad)
185
+ for row in range(1, nrows - 1):
186
+ for col in range(1, ncols - 1):
187
+ # Get the value for this cell
188
+ this_elevation = dem_padded[row, col]
189
+ # If this cell is nodata, then we're not interested in the flow
190
+ # direction
191
+ if np.isnan(this_elevation):
192
+ continue
193
+ # Init vars to be update in the loop through neighbouring cells
194
+ max_slope = -np.inf
195
+ min_slope = 0
196
+ heighest_neighbour_direction = 0
197
+ best_direction = 0
198
+ lonely_cell = True
199
+ # Loop through neighbouring cells
200
+ for i, (dr, dc) in enumerate(directions):
201
+ # Get the elevation of this neighbouring cell
202
+ neighbour_elevation = dem_adj[row + dr, col + dc]
203
+ # If the neighbouring cell is nodata and we're not treating
204
+ # nodata as drains, then ignore the influence of this cell
205
+ if np.isnan(neighbour_elevation):
206
+ continue
207
+ # If neighbouring cell has data, then set the lonely_cell
208
+ # flag to False to say the central cell isn't a single cell
209
+ # surrounded by nodata (including nodata cells treated as
210
+ # drains)
211
+ if neighbour_elevation != -np.inf:
212
+ lonely_cell = False
213
+ # Difference in elevation and slope
214
+ dz = this_elevation - neighbour_elevation
215
+ distance = np.sqrt((res[0]*dr)**2 + (res[1]*dc)**2)
216
+ slope = dz / distance
217
+ # If slope is steeper than the current maximum, and
218
+ # it's positive (flow *away* from this cell), then
219
+ # set this as the best direction
220
+ if (slope > max_slope) and (slope > 0.0):
221
+ max_slope = slope
222
+ best_direction = d8_codes[i]
223
+ elif (slope < min_slope):
224
+ # If it is a negative slope (this neighbour is higher
225
+ # than the central cell), store this in case we need it
226
+ # to try and resolve cells later
227
+ min_slope = slope
228
+ heighest_neighbour_direction = d8_codes_opposite[i]
229
+
230
+ # Fill the flow direction array. If no candidate has been
231
+ # found then the best_direction is 0 and the cell is unresolved
232
+ flow_dir_ = best_direction
233
+
234
+ # If this is a lonely cell, set it to the specified value
235
+ if lonely_cell:
236
+ flow_dir_ = lonely_cell_flowdir
237
+ # If we're meant to, try and resolve this cell and raise an
238
+ # error for unresolved cells
239
+ if (flow_dir_ == 0):
240
+ if attempt_to_resolve:
241
+ flow_dir_ = heighest_neighbour_direction
242
+ if error_if_unresolved:
243
+ neighbouring_dem = dem_adj[row-1:row+2, col-1:col+2]
244
+ raise RoutingError(
245
+ f"Could not resolve flow direction for cell ({col-1}, {row-1}). "
246
+ f"DEM for surrounding cells:\n{neighbouring_dem}"
247
+ )
248
+
249
+ # Set the best value for the flow direction
250
+ flow_dir[row - 1, col - 1] = flow_dir_
251
+
252
+ # Return the flow direction array as a masked array
253
+ return np.ma.masked_equal(flow_dir, -32768)
254
+
255
+
256
+ def condition_dem(dem_path, config):
257
+ """
258
+ Condition a DEM using Whitebox to remove pits, depressions
259
+ and flat areas
260
+ """
261
+ # If we have been passed a dict of parameters, use these
262
+ if isinstance(config, dict):
263
+ # If a path to save the conditioned DEM has been provided,
264
+ # use this
265
+ if 'save_dem_to_path' in config:
266
+ # Get the path and remove it from the dict so that we
267
+ # don't pass it to the WhiteboxTools function
268
+ dem_path_conditioned = config.pop('save_dem_to_path')
269
+ dem_path_conditioned = Path(dem_path_conditioned).resolve()
270
+ temp_dir = None
271
+ else:
272
+ # If no path has been provided, create a temporary file
273
+ temp_dir = tempfile.TemporaryDirectory()
274
+ dem_path_conditioned = os.path.join(temp_dir.name,
275
+ 'dem.tif')
276
+ # Use any remainning parameters in the dict as kwargs
277
+ kwargs = config
278
+ # Set dist=10000 if no value given
279
+ kwargs['dist'] = config.get('dist', 10000)
280
+ # Don't use any callback specified as we need to use our
281
+ # own to catch errors
282
+ if 'callback' in kwargs:
283
+ del kwargs['callback']
284
+
285
+ wbt = WhiteboxTools()
286
+ # Run the conditioning function and save the resulting DEM
287
+ # to the specified path
288
+ wbt.breach_depressions_least_cost(dem=dem_path,
289
+ output=dem_path_conditioned,
290
+ callback=whitebox_callback,
291
+ **kwargs)
292
+ return dem_path_conditioned, temp_dir
@@ -1,111 +0,0 @@
1
- import numpy as np
2
-
3
-
4
- class Router:
5
- """The Router class is responsible for using the flow direction raster to route water bodies within the
6
- model domain, setting variables required by the NanoFASE model, such as inflows, outflows and headwaters."""
7
-
8
- def __init__(self, flow_dir):
9
- """Initiliase the routing class with the flow direction array."""
10
- self.flow_dir = flow_dir
11
- self.grid_mask = self.flow_dir.mask
12
-
13
- def outflow_from_flow_dir(self, x, y):
14
- """Get the outflow cell reference given the current cell
15
- reference and a flow direction."""
16
- xy_out = {
17
- 1: [x+1, y],
18
- 2: [x+1, y+1],
19
- 4: [x, y+1],
20
- 8: [x-1, y+1],
21
- 16: [x-1, y],
22
- 32: [x-1, y-1],
23
- 64: [x, y-1],
24
- 128: [x+1, y-1]
25
- }
26
- return xy_out[self.flow_dir[y-1, x-1]]
27
-
28
- def inflows_from_flow_dir(self, x, y):
29
- """Get the inflow cell references given the current cell
30
- reference and a flow direction."""
31
- j, i = y - 1, x - 1
32
- # Flow direction that means the cell with indices inflows[j_n-j, i_n-i]
33
- # flows into this cell
34
- inflow_flow_dir = {
35
- (-1, -1): 2,
36
- (0, -1): 1,
37
- (1, -1): 128,
38
- (-1, 0): 4,
39
- (0, 0): 0,
40
- (1, 0): 64,
41
- (-1, 1): 8,
42
- (0, 1): 16,
43
- (1, 1): 32
44
- }
45
- inflow_cells = []
46
- # Loop through the neighbours and check if they're inflows to this cell
47
- for j_n in range(j-1, j+2):
48
- for i_n in range(i-1, i+2):
49
- if self.in_model_domain(i_n, j_n):
50
- if self.flow_dir[j_n, i_n] == inflow_flow_dir[(j_n-j, i_n-i)]:
51
- inflow_cells.append([i_n+1, j_n+1])
52
- # Create masked array from the inflow_cells list
53
- inflow_cells_ma = np.ma.array(np.ma.empty((7,2), dtype=int), mask=True)
54
- if len(inflow_cells) > 0: # Only fill if there are inflows
55
- inflow_cells_ma[0:len(inflow_cells)] = inflow_cells
56
- return inflow_cells_ma
57
-
58
- def in_model_domain(self, i, j):
59
- """Check if index [j,i] is in model domain."""
60
- i_in_domain = 0 <= i < self.grid_mask.shape[1]
61
- j_in_domain = 0 <= j < self.grid_mask.shape[0]
62
- not_masked = self.flow_dir[j, i] is not np.ma.masked if (i_in_domain and j_in_domain) else False
63
- return i_in_domain and j_in_domain and not_masked
64
-
65
- def n_waterbodies_from_inflows(self, x, y, outflow, inflows):
66
- """Calculate the number of waterbodies from the inflows to the cell."""
67
- # j, i = y - 1, x - 1
68
- j_out, i_out = outflow[1] - 1, outflow[0] - 1
69
- n_inflows = inflows.count(axis=0)[0] # Count the unmasked elements to get n_inflows
70
- # If there are no inflows but the outflow is to the model domain, it
71
- # must be a headwater. Else, number of waterbodies is same as number of inflows
72
- if n_inflows == 0 and self.in_model_domain(i_out, j_out):
73
- n_waterbodies = 1
74
- is_headwater = 1
75
- else:
76
- n_waterbodies = n_inflows
77
- is_headwater = 0
78
- return n_waterbodies, is_headwater
79
-
80
- # def generate_waterbody_code(self, x, y, outflow, inflows, is_estuary, is_headwater):
81
- # """Generates character code of the waterbodies of this cell, of the format
82
- # <type><index>(<inflow_point>:<outflow_point>/<n_waterbodies_along_branch>)...
83
- # where type = r,e,l,s (river, estuary, lake, sea), index is 1-indexed, inflow
84
- # and outflow_side are integers representing the geometrical point of inflow
85
- # (0 for centre, 1 for top-left, 2 for top centre, etc.) and n_waterbodies_along_branch
86
- # is the number of waterbodies between inflow and outflow."""
87
- # outflow_point = self.point_index(outflow[1]-x, outflow[0]-y)
88
- # wb_type = 'e' if is_estuary > 0 else 'r'
89
- # wb_char = ''
90
- # for i, inflow in enumerate(inflows):
91
- # if not inflow.mask.any(): # Only if this cell isn't masked
92
- # inflow_point = self.point_index(inflow[1]-x, inflow[0]-y)
93
- # wb_char = wb_char + '{0}{1}({2}:{3}/01)'.format(wb_type, i+1, inflow_point, outflow_point)
94
- # # If this is a headwater, there must be a waterbody with inflow at centre of cell
95
- # if is_headwater:
96
- # wb_char = '{0}1(0:{1}/01)'.format(wb_type, outflow_point)
97
- # return wb_char
98
-
99
- # def point_index(self, di, dj):
100
- # point_index = {
101
- # (-1, -1): 1,
102
- # (0, -1): 8,
103
- # (1, -1): 7,
104
- # (-1, 0): 2,
105
- # (0, 0): 0,
106
- # (1, 0): 6,
107
- # (-1, 1): 3,
108
- # (0, 1): 4,
109
- # (1, 1): 5
110
- # }
111
- # return point_index[dj, di]
File without changes
File without changes
File without changes
File without changes
File without changes