-
Notifications
You must be signed in to change notification settings - Fork 1
add multi-gauge facilitation #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0fabe7c
0d62d88
633c073
915e3c3
1526867
7cee75f
6573834
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ The [mHM](https://mhm-ufz.org/) basin extractor. Extract basins for given gaugin | |
| ## Dependencies | ||
|
|
||
| - numpy v1.14.5 or later | ||
| - pandas | ||
| - netCDF4 | ||
| - GDAL | ||
| - pyyaml | ||
|
|
@@ -36,15 +37,15 @@ To get a recent version of GDAL, you can use the ppa of [ubuntugis](https://laun | |
| sudo add-apt-repository ppa:ubuntugis/ppa | ||
| sudo apt-get update | ||
| sudo apt install gdal-bin libgdal-dev | ||
| pip install wheel numpy | ||
| pip install wheel numpy pandas | ||
| pip install GDAL==$(gdal-config --version) | ||
| ``` | ||
|
|
||
| #### MacOS | ||
| GDAL can be installed with [homebrew](https://formulae.brew.sh/formula/gdal): | ||
| ``` | ||
| brew install gdal | ||
| pip install wheel numpy | ||
| pip install wheel numpy pandas | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you don't need to add pandas here (it's only to install gdal correctly) |
||
| pip install GDAL==$(gdal-config --version) | ||
| ``` | ||
|
|
||
|
|
@@ -60,7 +61,7 @@ pipwin install gdal | |
| It is best to use basinex with conda to have gdal and NetCDF installed properly. | ||
| To use the development version of basinex, download this repository and do the following in your conda environment: | ||
|
|
||
| conda install -y gdal netcdf4 pyyaml cxx-compiler | ||
| conda install -y gdal netcdf4 pyyaml cxx-compiler pandas | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here pandas is correct ;-) |
||
| pip install . | ||
|
|
||
| Then you can execute `basinex` in that conda environment. | ||
|
|
@@ -152,13 +153,13 @@ ncfiles: | |
| - `path`: path to the mask file | ||
| - `varname`: name of the mask variable (optional, only needed if the mask is stored in a netcdf file) | ||
| - `latitude-size-correction: False` - **Optional**: | ||
| perform a latitude correction for the given basin size (default: False) | ||
| perform a latitude correction for the basin size of a given gauge (default: False) | ||
| - `AREA = N_cells * res_x * ( cos(LAT) * res_y ) * scaling factor^2` | ||
| - `matching:` - **Required**: gauge matching parameters | ||
| - **Note**: | ||
| The gauge matching is based on the flowaccumulation data. The value for | ||
| any given cell in the flowaccumulation grid is interpreted as the size | ||
| [in cells] of a river basin drainig into the respective cell. | ||
| [in cells] of a river basin draining into the respective cell. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch |
||
| During gauge matching the flowaccumulation grid is searched for a cell | ||
| with a corresponding basin size close to the given gauge basin size. The | ||
| search radius will be increased succesively and can be limited to a | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,7 @@ install_requires = | |
| pyyaml | ||
| gdal | ||
| netcdf4<1.6 | ||
| pandas | ||
| python_requires = >=3.6 | ||
| zip_safe = False | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,9 @@ | |
|
|
||
| import numpy as np | ||
|
|
||
| # precision for rounding to avoid numerical instabilities | ||
| PRECISION = 10 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe make this an optional input in the config.yaml file? |
||
|
|
||
|
|
||
| class SpatialMixin(object): | ||
| def trim(self): | ||
|
|
@@ -83,10 +86,10 @@ def shrink(self, ymin=None, ymax=None, xmin=None, xmax=None): | |
| } | ||
|
|
||
| cellsize = [float(abs(cs)) for cs in self.cellsize] | ||
| top = floor((self.bbox["ymax"] - bbox["ymax"]) / cellsize[0]) | ||
| left = floor((bbox["xmin"] - self.bbox["xmin"]) / cellsize[1]) | ||
| bottom = floor((bbox["ymin"] - self.bbox["ymin"]) / cellsize[0]) | ||
| right = floor((self.bbox["xmax"] - bbox["xmax"]) / cellsize[1]) | ||
| top = floor(round((self.bbox["ymax"] - bbox["ymax"]) / cellsize[0], PRECISION)) | ||
| left = floor(round((bbox["xmin"] - self.bbox["xmin"]) / cellsize[1], PRECISION)) | ||
| bottom = floor(round((bbox["ymin"] - self.bbox["ymin"]) / cellsize[0], PRECISION)) | ||
| right = floor(round((self.bbox["xmax"] - bbox["xmax"]) / cellsize[1], PRECISION)) | ||
|
|
||
| return self.removeCells( | ||
| max(top, 0), max(left, 0), max(bottom, 0), max(right, 0) | ||
|
|
@@ -175,10 +178,10 @@ def enlarge(self, ymin=None, ymax=None, xmin=None, xmax=None): | |
|
|
||
| cellsize = [float(abs(cs)) for cs in self.cellsize] | ||
|
|
||
| top = ceil((bbox["ymax"] - self.bbox["ymax"]) / cellsize[0]) | ||
| left = ceil((self.bbox["xmin"] - bbox["xmin"]) / cellsize[1]) | ||
| bottom = ceil((self.bbox["ymin"] - bbox["ymin"]) / cellsize[0]) | ||
| right = ceil((bbox["xmax"] - self.bbox["xmax"]) / cellsize[1]) | ||
| top = ceil(round((bbox["ymax"] - self.bbox["ymax"]) / cellsize[0], PRECISION)) | ||
| left = ceil(round((self.bbox["xmin"] - bbox["xmin"]) / cellsize[1], PRECISION)) | ||
| bottom = ceil(round((self.bbox["ymin"] - bbox["ymin"]) / cellsize[0], PRECISION)) | ||
| right = ceil(round((bbox["xmax"] - self.bbox["xmax"]) / cellsize[1], PRECISION)) | ||
|
|
||
| return self.addCells(max(top, 0), max(left, 0), max(bottom, 0), max(right, 0)) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| import yaml | ||
|
|
||
| from . import __version__ | ||
|
|
@@ -62,10 +63,10 @@ def gaugeBasinMask(flowdir, gauge): | |
| extract(np.array(flowdir, dtype=np.int32, copy=True), *gauge_idx), | ||
| dtype=np.int32, | ||
| ) | ||
|
|
||
| mask[mask == 0] = flowdir.fill_value | ||
| out = ga.array(mask, **flowdir.header) | ||
| return out.trim() | ||
| out = ga.array(mask, **flowdir.header).trim() | ||
| out._fobj = None | ||
| return out | ||
|
|
||
|
|
||
| def gridBasinMask(gauge): | ||
|
|
@@ -89,6 +90,7 @@ def gridBasinMask(gauge): | |
| fill_value=var.fill_value, | ||
| cellsize=nc.cellsize, | ||
| ) | ||
| out._fobj = None | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is so strange, but thanks for the fix. |
||
| return out.setMask(out <= 0) | ||
|
|
||
|
|
||
|
|
@@ -114,6 +116,7 @@ def openGridFiles(flist): | |
| flist = flist or () | ||
| for fdict in flist: | ||
| out[GridFile(**fdict)] = ga.fromfile(fdict["fname"]) | ||
| out._fobj = None | ||
| return out | ||
|
|
||
|
|
||
|
|
@@ -145,36 +148,54 @@ def commonBbox(fobjs): | |
| return bbox | ||
|
|
||
|
|
||
| def gaugeGrid(grid_template, gauge): | ||
| out = ga.full_like(grid_template, grid_template.fill_value) | ||
| def gaugeGrid(grid_template, gauge, out_in=None): | ||
| if out_in is None: | ||
| out = ga.full_like(grid_template, grid_template.fill_value) | ||
| out._fobj = None | ||
| else: | ||
| out = out_in | ||
| idx = out.indexOf(gauge.y, gauge.x) | ||
| if out_in is not None and not out.mask[idx]: | ||
| warnings.warn(f"There is already a gauge at {idx}, with id '{out.data[idx]}', replacing it by '{gauge.id}'.") | ||
| out.data[idx] = gauge.id | ||
| out.mask[idx] = False | ||
| return out | ||
|
|
||
|
|
||
| def sameExtend(fobjs): | ||
| def sameExtent(fobjs): | ||
| bbox = commonBbox(fobjs) | ||
| for fobj in fobjs: | ||
| if fobj.bbox != bbox: | ||
| return False | ||
| return True | ||
|
|
||
|
|
||
| def writeReport(bpath, mask, scaling_factor, gauge): | ||
| size = (np.sum(~mask.mask) * np.prod(np.abs(mask.cellsize))) * scaling_factor**2 | ||
| error_size = (size - gauge.size) / gauge.size * 100 | ||
| Path(bpath).mkdir(exist_ok=True, parents=True) | ||
| with open(os.path.join(bpath, "report.out"), "w") as f: | ||
| f.write("calculated_catchment_size: {:}\n".format(size)) | ||
| f.write("input_catchment_size : {:}\n".format(gauge.size)) | ||
| f.write("error_catchment_size (%) : {:}\n".format(error_size)) | ||
| f.write("adjusted_y : {:}\n".format(gauge.y)) | ||
| f.write("adjusted_x : {:}\n".format(gauge.x)) | ||
| def writeReport(bpath, updated_gauge, gauge, error): | ||
| if logging.DEBUG >= logging.root.level: | ||
| Path(bpath).mkdir(exist_ok=True, parents=True) | ||
| with open(os.path.join(bpath, "report.out"), "w") as f: | ||
| f.write("error_catchment_size (%) : {:}\n".format(error)) | ||
| if updated_gauge is not None: | ||
| f.write("new_catchment_size : {:}\n".format(updated_gauge.size)) | ||
| f.write("new_y : {:}\n".format(updated_gauge.y)) | ||
| f.write("new_x : {:}\n".format(updated_gauge.x)) | ||
| f.write("input_catchment_size : {:}\n".format(gauge.size)) | ||
| f.write("input_y : {:}\n".format(gauge.y)) | ||
| f.write("input_x : {:}\n".format(gauge.x)) | ||
| return {gauge.id: { | ||
| 'gauge_size': gauge.size, | ||
| 'new_size': updated_gauge.size, | ||
| 'error_size': error, | ||
| 'gauge_x': gauge.x, | ||
| 'new_x': updated_gauge.x, | ||
| 'gauge_y': gauge.y, | ||
| 'new_y': updated_gauge.y, | ||
| 'error_dist': ((gauge.x -updated_gauge.x)**2 + (gauge.y -updated_gauge.y)**2)**(0.5), | ||
| }} | ||
|
|
||
|
|
||
| def maskData(data, mask): | ||
| if all(x == y for x, y in zip(mask.cellsize, data.cellsize)): | ||
| if all(np.isclose(x, y) for x, y in zip(mask.cellsize, data.cellsize)): | ||
| return data.setMask(mask.mask) | ||
|
|
||
| enlarged_mask = mask.enlarge(**data.bbox).astype(float) | ||
|
|
@@ -186,6 +207,13 @@ def maskData(data, mask): | |
|
|
||
| def main(config, gauges): | ||
|
|
||
| flowacc = None | ||
| flowdir = None | ||
| filedict_main = {} | ||
| gaugedict_main = {} | ||
| gaugefile_main = None | ||
| updated_gauge = None | ||
|
|
||
| for gauge in gauges: | ||
| logging.info("processing gauge: %s", gauge.id) | ||
|
|
||
|
|
@@ -194,35 +222,44 @@ def main(config, gauges): | |
| if not gauge.path: | ||
|
|
||
| # create mask if not given | ||
| logging.debug("reading flow accumulation") | ||
| flowacc = ga.fromfile(config["flowacc"]).astype(np.int32) | ||
| if flowacc is None: | ||
| logging.debug("reading flow accumulation") | ||
| flowacc = ga.fromfile(config["flowacc"]) | ||
|
|
||
| logging.debug("reading flow direction") | ||
| flowdir = ga.fromfile(config["flowdir"]).astype(np.int32) | ||
| if flowdir is None: | ||
| logging.debug("reading flow direction") | ||
| flowdir = ga.fromfile(config["flowdir"]).astype(np.int32) | ||
|
|
||
| if gauge.size: | ||
| logging.debug("moving gauge to streamflow") | ||
| gauge = matchFlowacc(gauge, flowacc, **config["matching"]) | ||
| updated_gauge, error = matchFlowacc(gauge, flowacc, **config["matching"]) | ||
|
|
||
| if not gauge: | ||
| warnings.warn("Failed to match the gauge to the flow accumulation grid") | ||
| if updated_gauge is None: | ||
| warnings.warn(f"Failed to match the gauge {gauge.id} to the flow accumulation grid") | ||
| continue | ||
|
|
||
| logging.debug("generating basin mask") | ||
| mask = gaugeBasinMask(flowdir, gauge) | ||
| mask = gaugeBasinMask(flowdir, updated_gauge) | ||
|
|
||
| # write gauge grid if desired | ||
| if "gauge" in config: | ||
| logging.debug("writing gauge file") | ||
| fname = config["gauge"].get("fname", "idgauges.asc") | ||
| fitem = GridFile( | ||
| fname=config["gauge"].get("fname", "idgauges.asc"), | ||
| fname=fname, | ||
| outpath=config["gauge"].get("outpath"), | ||
| ) | ||
| gaugefile = gaugeGrid(flowacc, gauge).shrink(**mask.bbox) | ||
| filedict[fitem] = maskData(gaugefile, mask) | ||
| if len(gauges) > 1: | ||
| if gaugefile_main is None: | ||
| fitem_main = fitem | ||
| gaugefile_main = gaugeGrid(flowacc, updated_gauge, gaugefile_main) | ||
| filedict_main[fitem_main] = gaugefile_main | ||
| else: | ||
| gaugefile = gaugeGrid(flowacc, updated_gauge).shrink(**mask.bbox) | ||
| filedict[fitem] = maskData(gaugefile, mask) | ||
|
|
||
| else: | ||
| logging.debug("reding gauge file") | ||
| logging.debug("reading gauge file") | ||
| mask = gridBasinMask(gauge) | ||
|
|
||
| for fdict in config.get("gridfiles", []): | ||
|
|
@@ -253,19 +290,23 @@ def main(config, gauges): | |
| filedict[fitem] = mask | ||
|
|
||
| if filedict: | ||
| logging.debug("finding common extend") | ||
| logging.debug("finding common extent") | ||
| bbox = commonBbox(tuple(filedict.values())) | ||
|
|
||
| logging.debug("enlarging data to common extend") | ||
| logging.debug("enlarging data to common extent") | ||
| filedict = enlargeFiles(filedict, bbox) | ||
|
|
||
| if not sameExtend(tuple(filedict.values())): | ||
| if not sameExtent(tuple(filedict.values())): | ||
| raise RuntimeError("incompatible cellsizes") | ||
|
|
||
| bpath = os.path.join(config["outpath"], gauge.id) | ||
| writeFiles(bpath, filedict) | ||
| logging.debug("writing report") | ||
| writeReport(bpath, mask, config["matching"]["scaling_factor"], gauge) | ||
| gaugedict_main.update(writeReport(bpath, updated_gauge, gauge, error)) | ||
|
|
||
| writeFiles(config["outpath"], filedict_main) | ||
| if gaugedict_main: | ||
| pd.DataFrame.from_dict(gaugedict_main, orient='index').to_csv(f'{config["outpath"]}/summary.csv') | ||
|
|
||
|
|
||
| def initArgparser(): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you don't need to add pandas here (it's only to install gdal correctly)