Skip to content

Commit

Permalink
Merge pull request #146 from USEPA/dbhart-pr-146
Browse files Browse the repository at this point in the history
Merge main into network gen branch
  • Loading branch information
dbhart authored Nov 18, 2024
2 parents c0d702a + 23cfeb6 commit a10885e
Show file tree
Hide file tree
Showing 22 changed files with 464 additions and 282 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/build_deploy_pages.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# This is a basic workflow to help you get started with Actions

name: docs

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
build:
name: Build the documentation with Sphinx
Expand Down Expand Up @@ -36,6 +37,7 @@ jobs:
deploy:
name: Deploy documentation to GitHub Pages
needs: build
if: github.event_name == 'push'
permissions:
contents: read
pages: write # to deploy to Pages
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/quick_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
strategy:
matrix:
python-version: ['3.9', '3.11']
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand All @@ -33,7 +34,8 @@ jobs:
python -m pip install -e .
- name: Run tests and coverage (unittests plus doctests)
run: |
coverage run --source=wntr --omit="*/tests/*","*/sim/network_isolation/network_isolation.py","*/sim/aml/evaluator.py" -m pytest -m "not time_consuming" --doctest-modules --doctest-glob="*.rst" wntr
coverage run --source=wntr --omit="*/tests/*","*/sim/network_isolation/network_isolation.py","*/sim/aml/evaluator.py" -m pytest -m "not time_consuming" --doctest-modules --doctest-glob="*.rst" wntr
coverage run --source=wntr --omit="*/tests/*","*/sim/network_isolation/network_isolation.py","*/sim/aml/evaluator.py" --append -m pytest --doctest-glob="*.rst" documentation
coverage report --fail-under=70
# coverage run --source=wntr --omit="*/tests/*" --append -m pytest --doctest-glob="*.rst" documentation

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ temp*

examples/*.inp
wntr/tests/*.png
wntr/tests/*.tif

documentation/_local
documentation/apidoc
Binary file added documentation/figures/sample_elevations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 127 additions & 22 deletions documentation/gis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
>>> hydrant_data = gpd.read_file(examples_dir+'/data/Net1_hydrant_data.geojson')
>>> valve_data = gpd.read_file(examples_dir+'/data/Net1_valve_data.geojson')

.. doctest::
:hide:
:skipif: gpd is None or rasterio is None

>>> elevation_data_path = examples_dir+'/data/Net1_elevation_data.tif'

.. _geospatial:

Geospatial capabilities
Expand All @@ -47,7 +53,8 @@ The following section describes capabilities in WTNR that use GeoPandas GeoDataF

.. note::
Functions that use GeoDataFrames require the Python package **geopandas** :cite:p:`jvfm21`
and **rtree** :cite:p:`rtree`. Both are optional dependencies of WNTR.
and **rtree** :cite:p:`rtree`, and functions that use raster files require the
Python package **rasterio**. All three are optional dependencies of WNTR.
Note that **shapely** is installed with geopandas.

The following examples use a water network generated from Net1.inp.
Expand Down Expand Up @@ -112,13 +119,13 @@ For example, the junctions GeoDataFrame contains the following information:
:skipif: gpd is None

>>> print(wn_gis.junctions.head())
node_type elevation initial_quality geometry
name
10 Junction 216.408 5.000e-04 POINT (20.00000 70.00000)
11 Junction 216.408 5.000e-04 POINT (30.00000 70.00000)
12 Junction 213.360 5.000e-04 POINT (50.00000 70.00000)
13 Junction 211.836 5.000e-04 POINT (70.00000 70.00000)
21 Junction 213.360 5.000e-04 POINT (30.00000 40.00000)
base_demand demand_pattern elevation initial_quality demand_category geometry
name
10 0.000 1 216.408 5.000e-04 None POINT (20.00000 70.00000)
11 0.009 1 216.408 5.000e-04 None POINT (30.00000 70.00000)
12 0.009 1 213.360 5.000e-04 None POINT (50.00000 70.00000)
13 0.006 1 211.836 5.000e-04 None POINT (70.00000 70.00000)
21 0.009 1 213.360 5.000e-04 None POINT (30.00000 40.00000)

Each GeoDataFrame contains attributes and geometry:

Expand Down Expand Up @@ -334,23 +341,23 @@ and then translates the GeoDataFrames coordinates to EPSG:3857.

>>> wn_gis = wntr.network.to_gis(wn, crs='EPSG:4326')
>>> print(wn_gis.junctions.head())
node_type elevation initial_quality geometry
name
10 Junction 216.408 5.000e-04 POINT (20.00000 70.00000)
11 Junction 216.408 5.000e-04 POINT (30.00000 70.00000)
12 Junction 213.360 5.000e-04 POINT (50.00000 70.00000)
13 Junction 211.836 5.000e-04 POINT (70.00000 70.00000)
21 Junction 213.360 5.000e-04 POINT (30.00000 40.00000)
base_demand demand_pattern elevation initial_quality demand_category geometry
name
10 0.000 1 216.408 5.000e-04 None POINT (20.00000 70.00000)
11 0.009 1 216.408 5.000e-04 None POINT (30.00000 70.00000)
12 0.009 1 213.360 5.000e-04 None POINT (50.00000 70.00000)
13 0.006 1 211.836 5.000e-04 None POINT (70.00000 70.00000)
21 0.009 1 213.360 5.000e-04 None POINT (30.00000 40.00000)

>>> wn_gis.to_crs('EPSG:3857')
>>> print(wn_gis.junctions.head())
node_type elevation initial_quality geometry
name
10 Junction 216.408 5.000e-04 POINT (2226389.816 11068715.659)
11 Junction 216.408 5.000e-04 POINT (3339584.724 11068715.659)
12 Junction 213.360 5.000e-04 POINT (5565974.540 11068715.659)
13 Junction 211.836 5.000e-04 POINT (7792364.356 11068715.659)
21 Junction 213.360 5.000e-04 POINT (3339584.724 4865942.280)
base_demand demand_pattern elevation initial_quality demand_category geometry
name
10 0.000 1 216.408 5.000e-04 None POINT (2226389.816 11068715.659)
11 0.009 1 216.408 5.000e-04 None POINT (3339584.724 11068715.659)
12 0.009 1 213.360 5.000e-04 None POINT (5565974.540 11068715.659)
13 0.006 1 211.836 5.000e-04 None POINT (7792364.356 11068715.659)
21 0.009 1 213.360 5.000e-04 None POINT (3339584.724 4865942.280)

Snap point geometries to the nearest point or line
----------------------------------------------------
Expand Down Expand Up @@ -822,3 +829,101 @@ the census tracts (polygons) is different than the junction and pipe attributes.
:alt: Intersection of junctions and pipes with mean income demographic data in EPANET example Net1

Net1 with mean income demographic data intersected with junctions and pipes.

Sample raster at points geometries
--------------------------------------

The :class:`~wntr.gis.sample_raster` function can be used to sample a raster file at point geometries,
such as the nodes of a water network. A common use case for this function is to assign elevation to the
nodes of a water network model, however other geospatial information such as climate or hazard data could be sampled
using this function.

The network file, Net1.inp, in EPSG:4326 CRS is used in the example below.
The raster data in the GeoTIFF format is also in EPSG:4326 CRS.
See :ref:`crs` for more information.

.. doctest::
:skipif: gpd is None

>>> wn = wntr.network.WaterNetworkModel('networks/Net1.inp') # doctest: +SKIP
>>> wn_gis = wntr.network.to_gis(wn, crs='EPSG:4326')

Sample elevations at junctions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Elevation is an essential attribute for accurate simulation of pressure in a water network and is
commonly provided in GeoTIFF (.tif) files. The following example shows how such files can be sampled
and assigned to the junctions and tanks of a network. Note that elevation data generally needs
to be adjusted to account for buried pipes.

.. doctest::
:skipif: gpd is None or rasterio is None

>>> elevation_data_path = 'data/Net1_elevation_data.tif' # doctest: +SKIP
>>> junctions = wn_gis.junctions
>>> junction_elevations = wntr.gis.sample_raster(junctions, elevation_data_path)
>>> print(junction_elevations)
name
10 1400.0
11 2100.0
12 3500.0
13 4900.0
21 1200.0
22 2000.0
23 2800.0
31 300.0
32 500.0
dtype: float64

.. doctest::
:skipif: gpd is None or rasterio is None

>>> tanks = wn_gis.tanks
>>> tank_elevations = wntr.gis.sample_raster(tanks, elevation_data_path)
>>> print(tank_elevations)
name
2 4500.0
dtype: float64

To use these elevations for hydraulic simulations,
they need to be added to the water network object.

.. doctest::
:skipif: gpd is None or rasterio is None

>>> for junction_name in wn.junction_name_list:
... junction = wn.get_node(junction_name)
... junction.elevation = junction_elevations[junction_name]

.. doctest::
:skipif: gpd is None or rasterio is None

>>> for tank_name in wn.tank_name_list:
... tank = wn.get_node(tank_name)
... tank.elevation = tank_elevations[tank_name]

The sampled elevations can be plotted as follows. The
resulting :numref:`fig-sample-elevations` illustrates Net1 with the elevations
sampled from the raster file.

.. doctest::
:skipif: gpd is None or rasterio is None

>>> ax = wntr.graphics.plot_network(wn, node_attribute="elevation", link_width=1.5,
... node_size=40, node_colorbar_label='Raster Elevation')

.. doctest::
:skipif: gpd is None or rasterio is None
:hide:

>>> bounds = ax.axis('equal')
>>> plt.tight_layout()
>>> plt.savefig('sample_elevations.png', dpi=300)
>>> plt.close()

.. _fig-sample-elevations:
.. figure:: figures/sample_elevations.png
:width: 640
:alt: Net1 with elevations sampled from raster.

Net1 with elevations sampled from raster.
2 changes: 2 additions & 0 deletions documentation/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ The following Python packages are optional:
https://pypi.org/project/utm/
* geopandas :cite:p:`jvfm21`: used to work with geospatial data,
https://geopandas.org/
* rasterio :cite:p:`rasterio`: used to work with raster data,
https://rasterio.readthedocs.io/
* rtree :cite:p:`rtree`: used for overlay operations in geopandas,
https://rtree.readthedocs.io/
* openpyxl :cite:p:`gacl18`: used to read/write to Microsoft® Excel® spreadsheets,
Expand Down
47 changes: 25 additions & 22 deletions documentation/model_io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,27 +206,28 @@ GeoJSON files
GeoJSON files are commonly used to store geographic data structures.
More information on GeoJSON files can be found at https://geojson.org.

To use GeoJSON files in WNTR, a set of valid base column names are required.
Valid base GeoJSON column names can be obtained using the
:class:`~wntr.network.io.valid_gis_names` function.
The following example returns valid base GeoJSON column names for junctions.
When reading GeoJSON files into WNTR, the file should contain columns from the set of valid column names.
Valid GeoJSON column names can be obtained using the :class:`~wntr.network.io.valid_gis_names` function.
By default, the function returns a complete set of required and optional column names.
A minimal list of column names containing commonly used attributes can be obtained by setting ``complete_list`` to False.
The minimal set correspond with attributes used in :class:`~wntr.network.model.WaterNetworkModel.add_junction`, :class:`~wntr.network.model.WaterNetworkModel.add_tank`, etc.
Columns that are optional (i.e., ``initial_quality``) and not included in the GeoJSON file are defined using default values.

The following examples return the complete and minimal lists of valid GeoJSON column names for junctions.

.. doctest::
:skipif: gpd is None

>>> geojson_column_names = wntr.network.io.valid_gis_names()
>>> print(geojson_column_names['junctions'])
['name', 'elevation', 'coordinates', 'emitter_coefficient', 'initial_quality', 'minimum_pressure', 'required_pressure', 'pressure_exponent', 'tag']

A minimal list of valid column names can also be obtained by setting ``complete_list`` to False.
Column names that are optional (i.e., ``initial_quality``) and not included in the GeoJSON file are defined using default values.
['name', 'base_demand', 'demand_pattern', 'elevation', 'demand_category', 'geometry', 'emitter_coefficient', 'initial_quality', 'minimum_pressure', 'required_pressure', 'pressure_exponent', 'tag']

.. doctest::
:skipif: gpd is None

>>> geojson_column_names = wntr.network.io.valid_gis_names(complete_list=False)
>>> print(geojson_column_names['junctions'])
['name', 'elevation', 'coordinates']
['name', 'base_demand', 'demand_pattern', 'elevation', 'demand_category', 'geometry']

Note that GeoJSON files can contain additional custom column names that are assigned to WaterNetworkModel objects.

Expand All @@ -253,7 +254,7 @@ Note that patterns, curves, sources, controls, and options are not stored in the

The :class:`~wntr.network.io.read_geojson` function creates a WaterNetworkModel from a
dictionary of GeoJSON files.
Valid base column names and additional custom attributes are added to the model.
Valid column names and additional custom attributes are added to the model.
The function can also be used to append information from GeoJSON files into an existing WaterNetworkModel.

.. doctest::
Expand Down Expand Up @@ -299,30 +300,32 @@ To use Esri Shapefiles in WNTR, several formatting requirements are enforced:
node and link attribute names are often longer. For this reason, it is
assumed that the first 10 characters of each attribute are unique.

* To create WaterNetworkModel from Shapefiles, a set of valid field names are required.
Valid base Shapefiles field names can be obtained using the
:class:`~wntr.network.io.valid_gis_names` function.
For Shapefiles, the `truncate` input parameter should be set to 10 (characters).
The following example returns valid base Shapefile field names for junctions.
Note that attributes like ``base_demand`` are truncated to ``base_deman``.
* When reading Shapefiles files into WNTR, the file should contain fields from the set of valid column names.
Valid Shapefiles field names can be obtained using the
:class:`~wntr.network.io.valid_gis_names` function. By default, the function
returns a complete set of required and optional field names.
A minimal list of field names containing commonly used attributes can be obtained by setting ``complete_list`` to False.
The minimal set correspond with attributes used in `add_junction`, `add_tank`, etc.
Fields that are optional (i.e., ``initial_quality``) and not included in the Shapefile are defined using default values.

For Shapefiles, the `truncate_names` input parameter should be set to 10 (characters).
The following examples return the complete and minimal lists of valid Shapefile field names for junctions.
Note that attributes like ``minimum_pressure`` are truncated to ``minimum_pr``.

.. doctest::
:skipif: gpd is None

>>> shapefile_field_names = wntr.network.io.valid_gis_names(truncate_names=10)
>>> print(shapefile_field_names['junctions'])
['name', 'elevation', 'coordinate', 'emitter_co', 'initial_qu', 'minimum_pr', 'required_p', 'pressure_e', 'tag']

A minimal list of valid field names can also be obtained by setting ``complete_list`` to False.
Field names that are optional (i.e., ``initial_quality``) and not included in the Shapefile are defined using default values.
['name', 'base_deman', 'demand_pat', 'elevation', 'demand_cat', 'geometry', 'emitter_co', 'initial_qu', 'minimum_pr', 'required_p', 'pressure_e', 'tag']

.. doctest::
:skipif: gpd is None

>>> shapefile_field_names = wntr.network.io.valid_gis_names(complete_list=False,
... truncate_names=10)
>>> print(shapefile_field_names['junctions'])
['name', 'elevation', 'coordinate']
['name', 'base_deman', 'demand_pat', 'elevation', 'demand_cat', 'geometry']

* Shapefiles can contain additional custom field names that are assigned to WaterNetworkModel objects.

Expand All @@ -349,7 +352,7 @@ Note that patterns, curves, sources, controls, and options are not stored in the

The :class:`~wntr.network.io.read_shapefile` function creates a WaterNetworkModel from a dictionary of
Shapefile directories.
Valid base field names and additional custom field names are added to the model.
Valid field names and additional custom field names are added to the model.
The function can also be used to append information from Shapefiles into an existing WaterNetworkModel.

.. doctest::
Expand Down
8 changes: 8 additions & 0 deletions documentation/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ @misc{jvfm21
year = "2021"
}

@misc{rasterio,
author = {Sean Gillies and others},
organization = {Mapbox},
title = {{Rasterio: geospatial raster I/O for {Python} programmers}},
year = {2013--},
url = {"https://github.com/rasterio/rasterio"}
}

@article{jcmg11,
author = "Joyner, David and \v{C}ert\'{i}k, Ond\v{r}ej and Meurer, Aaron and Granger, Brian E.",
address = "New York, NY, USA",
Expand Down
Binary file added examples/data/Net1_elevation_data.tif
Binary file not shown.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ utm
openpyxl
geopandas<1.0
fiona<1.10
rasterio
rtree

# Documentation
Expand Down
Loading

0 comments on commit a10885e

Please sign in to comment.