ausdem 0.1.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.
ausdem-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sia Ghelichkhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ausdem-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: ausdem
3
+ Version: 0.1.0
4
+ Summary: Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia WCS services as xarray arrays.
5
+ Author-email: Sia Ghelichkhan <ghelichkhani.siavash@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/g-adopt/ausdem
8
+ Project-URL: Repository, https://github.com/g-adopt/ausdem
9
+ Project-URL: Issues, https://github.com/g-adopt/ausdem/issues
10
+ Project-URL: Changelog, https://github.com/g-adopt/ausdem/blob/main/CHANGELOG.md
11
+ Project-URL: Data source, https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data
12
+ Keywords: DEM,elevation,SRTM,LiDAR,Australia,Geoscience Australia,WCS
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: GIS
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: requests>=2.25
28
+ Requires-Dist: numpy
29
+ Requires-Dist: xarray
30
+ Requires-Dist: rioxarray
31
+ Requires-Dist: rasterio
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest; extra == "test"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest; extra == "dev"
36
+ Requires-Dist: build; extra == "dev"
37
+ Requires-Dist: twine; extra == "dev"
38
+ Requires-Dist: ruff; extra == "dev"
39
+ Requires-Dist: matplotlib; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # ausdem
43
+
44
+ [![CI](https://github.com/g-adopt/ausdem/actions/workflows/test.yml/badge.svg)](https://github.com/g-adopt/ausdem/actions/workflows/test.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/ausdem.svg)](https://pypi.org/project/ausdem/)
46
+ [![Python versions](https://img.shields.io/pypi/pyversions/ausdem.svg)](https://pypi.org/project/ausdem/)
47
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
48
+
49
+ Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia
50
+ straight into Python as georeferenced [xarray](https://docs.xarray.dev) arrays.
51
+
52
+ It talks to GA's public [Web Coverage Services](https://services.ga.gov.au),
53
+ asking only for the bounding box you want rather than downloading whole
54
+ continental tiles. The data and services come from GA's
55
+ [Digital Elevation Data](https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data)
56
+ page.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install ausdem
62
+ ```
63
+
64
+ Requires `requests`, `numpy`, `xarray`, `rioxarray` and `rasterio` (pulled in
65
+ automatically).
66
+
67
+ ## Usage
68
+
69
+ ```python
70
+ import ausdem
71
+
72
+ # A small area around Canberra (min_lon, min_lat, max_lon, max_lat)
73
+ dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
74
+
75
+ dem.plot() # it's a normal xarray DataArray
76
+ print(float(dem.max())) # highest point in the box
77
+
78
+ ausdem.save_geotiff(dem, "canberra.tif") # or: dem.rio.to_raster("canberra.tif")
79
+ ```
80
+
81
+ The returned object is an `xarray.DataArray` with dimensions `(y, x)`, a NaN
82
+ no-data mask, and a `.rio` accessor (CRS, transform, `to_raster`). So you get
83
+ the array for analysis and can drop it to a GeoTIFF whenever you like.
84
+
85
+ ### Choosing a dataset
86
+
87
+ ```python
88
+ ausdem.list_datasets()
89
+ # ['lidar_5m', 'srtm_1s_dem', 'srtm_1s_dem_h']
90
+
91
+ dem_h = ausdem.get_dem(bbox, dataset="srtm_1s_dem_h") # hydro-enforced
92
+ lidar = ausdem.get_dem(bbox, dataset="lidar_5m") # 5 m, where surveyed
93
+ ```
94
+
95
+ | key | product | resolution |
96
+ |-----------------|-------------------------------------------|------------|
97
+ | `srtm_1s_dem` | SRTM 1 Second DEM (bare earth), national | ~30 m |
98
+ | `srtm_1s_dem_h` | SRTM 1 Second DEM-H (hydro-enforced) | ~30 m |
99
+ | `lidar_5m` | LiDAR-derived 5 m DEM (surveyed areas) | 5 m |
100
+
101
+ The smoothed `DEM-S` product is not served over WCS; download it from the
102
+ [ELVIS portal](https://elevation.fsdf.org.au/). The LiDAR DEM only covers
103
+ surveyed areas (coastal zone, Murray-Darling floodplains, population centres);
104
+ requests outside coverage either come back as a no-data (NaN) tile or raise
105
+ `ausdem.WCSError`, depending on the area.
106
+
107
+ Pass `resolution=` (in degrees) to resample, e.g. `resolution=0.001` for a
108
+ coarser, lighter grid.
109
+
110
+ ## Command line
111
+
112
+ ```bash
113
+ ausdem 149.0 -35.4 149.1 -35.3 -o canberra.tif
114
+ ausdem 149.0 -35.4 149.1 -35.3 -o canberra_demh.tif -d srtm_1s_dem_h
115
+ ausdem --list
116
+ ```
117
+
118
+ ## Notes
119
+
120
+ Coordinates are decimal degrees. SRTM products are served in WGS84
121
+ (EPSG:4326) and the LiDAR product in GDA94 (EPSG:4283); for input bounding
122
+ boxes the two are interchangeable at this scale. Very large requests are
123
+ rejected client-side to avoid pulling huge rasters by accident; tile your area
124
+ or coarsen the resolution if you hit that.
125
+
126
+ ## Example
127
+
128
+ There is a small standalone script in [`examples/`](examples/) that fetches a
129
+ DEM and plots it:
130
+
131
+ ```bash
132
+ python examples/plot_dem.py # a box near Canberra, writes dem.png
133
+ ```
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ git clone https://github.com/g-adopt/ausdem
139
+ cd ausdem
140
+ pip install -e ".[dev]"
141
+ pytest -m "not network" # offline tests
142
+ pytest # include the live GA WCS test
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT, see [LICENSE](LICENSE).
148
+
149
+ ## Citation
150
+
151
+ If you use ausdem in your work, please cite it. Release archives are deposited
152
+ on Zenodo and a DOI will be added here after the first release; in the meantime
153
+ see [CITATION.cff](CITATION.cff).
ausdem-0.1.0/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # ausdem
2
+
3
+ [![CI](https://github.com/g-adopt/ausdem/actions/workflows/test.yml/badge.svg)](https://github.com/g-adopt/ausdem/actions/workflows/test.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/ausdem.svg)](https://pypi.org/project/ausdem/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/ausdem.svg)](https://pypi.org/project/ausdem/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia
9
+ straight into Python as georeferenced [xarray](https://docs.xarray.dev) arrays.
10
+
11
+ It talks to GA's public [Web Coverage Services](https://services.ga.gov.au),
12
+ asking only for the bounding box you want rather than downloading whole
13
+ continental tiles. The data and services come from GA's
14
+ [Digital Elevation Data](https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data)
15
+ page.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install ausdem
21
+ ```
22
+
23
+ Requires `requests`, `numpy`, `xarray`, `rioxarray` and `rasterio` (pulled in
24
+ automatically).
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ import ausdem
30
+
31
+ # A small area around Canberra (min_lon, min_lat, max_lon, max_lat)
32
+ dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
33
+
34
+ dem.plot() # it's a normal xarray DataArray
35
+ print(float(dem.max())) # highest point in the box
36
+
37
+ ausdem.save_geotiff(dem, "canberra.tif") # or: dem.rio.to_raster("canberra.tif")
38
+ ```
39
+
40
+ The returned object is an `xarray.DataArray` with dimensions `(y, x)`, a NaN
41
+ no-data mask, and a `.rio` accessor (CRS, transform, `to_raster`). So you get
42
+ the array for analysis and can drop it to a GeoTIFF whenever you like.
43
+
44
+ ### Choosing a dataset
45
+
46
+ ```python
47
+ ausdem.list_datasets()
48
+ # ['lidar_5m', 'srtm_1s_dem', 'srtm_1s_dem_h']
49
+
50
+ dem_h = ausdem.get_dem(bbox, dataset="srtm_1s_dem_h") # hydro-enforced
51
+ lidar = ausdem.get_dem(bbox, dataset="lidar_5m") # 5 m, where surveyed
52
+ ```
53
+
54
+ | key | product | resolution |
55
+ |-----------------|-------------------------------------------|------------|
56
+ | `srtm_1s_dem` | SRTM 1 Second DEM (bare earth), national | ~30 m |
57
+ | `srtm_1s_dem_h` | SRTM 1 Second DEM-H (hydro-enforced) | ~30 m |
58
+ | `lidar_5m` | LiDAR-derived 5 m DEM (surveyed areas) | 5 m |
59
+
60
+ The smoothed `DEM-S` product is not served over WCS; download it from the
61
+ [ELVIS portal](https://elevation.fsdf.org.au/). The LiDAR DEM only covers
62
+ surveyed areas (coastal zone, Murray-Darling floodplains, population centres);
63
+ requests outside coverage either come back as a no-data (NaN) tile or raise
64
+ `ausdem.WCSError`, depending on the area.
65
+
66
+ Pass `resolution=` (in degrees) to resample, e.g. `resolution=0.001` for a
67
+ coarser, lighter grid.
68
+
69
+ ## Command line
70
+
71
+ ```bash
72
+ ausdem 149.0 -35.4 149.1 -35.3 -o canberra.tif
73
+ ausdem 149.0 -35.4 149.1 -35.3 -o canberra_demh.tif -d srtm_1s_dem_h
74
+ ausdem --list
75
+ ```
76
+
77
+ ## Notes
78
+
79
+ Coordinates are decimal degrees. SRTM products are served in WGS84
80
+ (EPSG:4326) and the LiDAR product in GDA94 (EPSG:4283); for input bounding
81
+ boxes the two are interchangeable at this scale. Very large requests are
82
+ rejected client-side to avoid pulling huge rasters by accident; tile your area
83
+ or coarsen the resolution if you hit that.
84
+
85
+ ## Example
86
+
87
+ There is a small standalone script in [`examples/`](examples/) that fetches a
88
+ DEM and plots it:
89
+
90
+ ```bash
91
+ python examples/plot_dem.py # a box near Canberra, writes dem.png
92
+ ```
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ git clone https://github.com/g-adopt/ausdem
98
+ cd ausdem
99
+ pip install -e ".[dev]"
100
+ pytest -m "not network" # offline tests
101
+ pytest # include the live GA WCS test
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT, see [LICENSE](LICENSE).
107
+
108
+ ## Citation
109
+
110
+ If you use ausdem in your work, please cite it. Release archives are deposited
111
+ on Zenodo and a DOI will be added here after the first release; in the meantime
112
+ see [CITATION.cff](CITATION.cff).
@@ -0,0 +1,27 @@
1
+ """ausdem: fetch Australian DEM data from Geoscience Australia WCS services.
2
+
3
+ Quick start
4
+ -----------
5
+ >>> import ausdem
6
+ >>> dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3)) # around Canberra
7
+ >>> ausdem.save_geotiff(dem, "canberra.tif")
8
+
9
+ See :func:`ausdem.list_datasets` for the available DEM products.
10
+ """
11
+
12
+ from .client import WCSError
13
+ from .core import get_dem, save_geotiff
14
+ from .datasets import DATASETS, DEFAULT_DATASET, get_dataset, list_datasets
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ "get_dem",
20
+ "save_geotiff",
21
+ "list_datasets",
22
+ "get_dataset",
23
+ "DATASETS",
24
+ "DEFAULT_DATASET",
25
+ "WCSError",
26
+ "__version__",
27
+ ]
@@ -0,0 +1,76 @@
1
+ """Command-line interface: ``ausdem`` downloads a DEM to a GeoTIFF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from .core import get_dem, save_geotiff
9
+ from .datasets import DATASETS, DEFAULT_DATASET, list_datasets
10
+
11
+
12
+ def _build_parser() -> argparse.ArgumentParser:
13
+ p = argparse.ArgumentParser(
14
+ prog="ausdem",
15
+ description="Fetch Australian DEM data from Geoscience Australia.",
16
+ )
17
+ p.add_argument(
18
+ "bbox",
19
+ nargs=4,
20
+ type=float,
21
+ metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"),
22
+ help="Bounding box in decimal degrees.",
23
+ )
24
+ p.add_argument("-o", "--output", required=True, help="Output GeoTIFF path.")
25
+ p.add_argument(
26
+ "-d",
27
+ "--dataset",
28
+ default=DEFAULT_DATASET,
29
+ choices=list_datasets(),
30
+ help=f"DEM product (default: {DEFAULT_DATASET}).",
31
+ )
32
+ p.add_argument(
33
+ "-r",
34
+ "--resolution",
35
+ type=float,
36
+ default=None,
37
+ help="Output pixel size in degrees (default: dataset native).",
38
+ )
39
+ p.add_argument(
40
+ "--list",
41
+ action="store_true",
42
+ help="List available datasets and exit.",
43
+ )
44
+ return p
45
+
46
+
47
+ def main(argv: list[str] | None = None) -> int:
48
+ argv = list(sys.argv[1:] if argv is None else argv)
49
+
50
+ if "--list" in argv:
51
+ for key in list_datasets():
52
+ print(f"{key:16s} {DATASETS[key].title}")
53
+ print(f"{'':16s} {DATASETS[key].description}")
54
+ return 0
55
+
56
+ args = _build_parser().parse_args(argv)
57
+ try:
58
+ dem = get_dem(
59
+ tuple(args.bbox),
60
+ dataset=args.dataset,
61
+ resolution=args.resolution,
62
+ )
63
+ save_geotiff(dem, args.output)
64
+ except Exception as exc: # surface a clean message, not a traceback
65
+ print(f"error: {exc}", file=sys.stderr)
66
+ return 1
67
+
68
+ print(
69
+ f"wrote {args.output} [{args.dataset}] "
70
+ f"shape={tuple(dem.sizes[d] for d in ('y', 'x'))}"
71
+ )
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())
@@ -0,0 +1,89 @@
1
+ """Low-level WCS GetCoverage client for the Geoscience Australia DEM services.
2
+
3
+ The GA services are ArcGIS MapServer WCS endpoints. WCS 2.0.1 GetCoverage is
4
+ unreliable on these endpoints, but WCS 1.0.0 ``GetCoverage`` returns a clean
5
+ 32-bit float GeoTIFF, so that is what we use here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Tuple
11
+
12
+ import requests
13
+
14
+ from .datasets import Dataset
15
+
16
+ # ArcGIS returns an HTML/XML error body (not a TIFF) when a request is invalid
17
+ # or falls entirely outside the data extent.
18
+ _TIFF_MAGIC = (b"II*\x00", b"MM\x00*")
19
+
20
+
21
+ class WCSError(RuntimeError):
22
+ """Raised when the WCS service returns an error instead of a coverage."""
23
+
24
+
25
+ def _parse_error(content: bytes) -> str:
26
+ """Extract a human-readable message from an ArcGIS error body."""
27
+ text = content.decode("utf-8", errors="replace")
28
+ # Strip tags so both the ArcGIS HTML and OGC XML errors read cleanly.
29
+ import re
30
+
31
+ text = re.sub(r"<[^>]+>", " ", text)
32
+ text = " ".join(text.split())
33
+ return text[:500] if text else "empty response"
34
+
35
+
36
+ def get_coverage(
37
+ dataset: Dataset,
38
+ bbox: Tuple[float, float, float, float],
39
+ res_x: float,
40
+ res_y: float,
41
+ timeout: float = 120.0,
42
+ session: requests.Session | None = None,
43
+ ) -> bytes:
44
+ """Issue a WCS 1.0.0 GetCoverage request and return GeoTIFF bytes.
45
+
46
+ Parameters
47
+ ----------
48
+ dataset:
49
+ The :class:`~ausdem.datasets.Dataset` to query.
50
+ bbox:
51
+ ``(min_lon, min_lat, max_lon, max_lat)`` in the dataset's request CRS.
52
+ res_x, res_y:
53
+ Output pixel size in CRS units (degrees).
54
+ timeout:
55
+ Per-request timeout in seconds.
56
+ session:
57
+ Optional :class:`requests.Session` for connection reuse.
58
+
59
+ Returns
60
+ -------
61
+ bytes
62
+ The raw GeoTIFF payload.
63
+ """
64
+ min_x, min_y, max_x, max_y = bbox
65
+ params = {
66
+ "service": "WCS",
67
+ "version": "1.0.0",
68
+ "request": "GetCoverage",
69
+ "coverage": dataset.coverage,
70
+ "CRS": dataset.crs,
71
+ "RESPONSE_CRS": dataset.crs,
72
+ "BBOX": f"{min_x},{min_y},{max_x},{max_y}",
73
+ "RESX": repr(res_x),
74
+ "RESY": repr(res_y),
75
+ "FORMAT": "GeoTIFF",
76
+ }
77
+
78
+ http = session or requests
79
+ resp = http.get(dataset.url, params=params, timeout=timeout)
80
+ resp.raise_for_status()
81
+
82
+ content = resp.content
83
+ if not content.startswith(_TIFF_MAGIC):
84
+ raise WCSError(
85
+ f"{dataset.key}: service did not return a GeoTIFF. "
86
+ f"This usually means the bounding box is outside the data extent "
87
+ f"or too large. Service said: {_parse_error(content)}"
88
+ )
89
+ return content
@@ -0,0 +1,127 @@
1
+ """High-level API: fetch a DEM as an xarray DataArray and save it as GeoTIFF."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Tuple
7
+
8
+ import numpy as np
9
+ import rioxarray # noqa: F401 (registers the .rio accessor on xarray objects)
10
+ import xarray as xr
11
+
12
+ from .client import get_coverage
13
+ from .datasets import DEFAULT_DATASET, get_dataset, list_datasets # noqa: F401
14
+
15
+ # Values at or below this magnitude are treated as the float "no data" sentinel
16
+ # that the GA services use for sea / outside-coverage pixels.
17
+ _NODATA_THRESHOLD = -1e30
18
+
19
+ # Refuse absurdly large requests by default so a typo in the bbox does not pull
20
+ # hundreds of megapixels off the service.
21
+ _MAX_PIXELS = 25_000_000
22
+
23
+
24
+ def _open_geotiff(content: bytes, mask: bool) -> xr.DataArray:
25
+ """Decode GeoTIFF bytes into an in-memory, georeferenced DataArray."""
26
+ from rasterio.io import MemoryFile
27
+
28
+ with MemoryFile(content) as memfile:
29
+ with memfile.open() as src:
30
+ da = rioxarray.open_rasterio(src).load()
31
+
32
+ # Collapse the singleton band dimension: a DEM is a single 2-D surface.
33
+ if "band" in da.dims and da.sizes["band"] == 1:
34
+ da = da.squeeze("band", drop=True)
35
+
36
+ da = da.astype("float32")
37
+ if mask:
38
+ sentinel = (da <= _NODATA_THRESHOLD) | da.isnull()
39
+ da = da.where(~sentinel)
40
+ da.rio.write_nodata(np.nan, inplace=True)
41
+ da.name = "elevation"
42
+ da.attrs.setdefault("units", "m")
43
+ return da
44
+
45
+
46
+ def get_dem(
47
+ bbox: Tuple[float, float, float, float],
48
+ dataset: str = DEFAULT_DATASET,
49
+ resolution: float | Tuple[float, float] | None = None,
50
+ mask: bool = True,
51
+ timeout: float = 120.0,
52
+ ) -> xr.DataArray:
53
+ """Fetch a DEM for ``bbox`` and return it as an xarray DataArray.
54
+
55
+ Parameters
56
+ ----------
57
+ bbox:
58
+ ``(min_lon, min_lat, max_lon, max_lat)`` in decimal degrees.
59
+ dataset:
60
+ Dataset key. One of :func:`ausdem.list_datasets` (default
61
+ ``"srtm_1s_dem"``). Use ``"srtm_1s_dem_h"`` for the hydrologically
62
+ enforced surface or ``"lidar_5m"`` for the 5 m LiDAR DEM where covered.
63
+ resolution:
64
+ Output pixel size in degrees. A single float sets both axes; a
65
+ ``(res_x, res_y)`` tuple sets them independently. Defaults to the
66
+ dataset's native resolution.
67
+ mask:
68
+ If True (default), replace the service's no-data sentinel with NaN.
69
+ timeout:
70
+ Per-request timeout in seconds.
71
+
72
+ Returns
73
+ -------
74
+ xarray.DataArray
75
+ A georeferenced 2-D array (``y``, ``x``) with a ``.rio`` accessor.
76
+ Save it with :func:`save_geotiff` or ``da.rio.to_raster("out.tif")``.
77
+
78
+ Examples
79
+ --------
80
+ >>> import ausdem
81
+ >>> dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
82
+ >>> dem.shape
83
+ (360, 360)
84
+ >>> ausdem.save_geotiff(dem, "canberra.tif")
85
+ """
86
+ ds = get_dataset(dataset)
87
+
88
+ min_lon, min_lat, max_lon, max_lat = bbox
89
+ if min_lon >= max_lon or min_lat >= max_lat:
90
+ raise ValueError(
91
+ "bbox must be (min_lon, min_lat, max_lon, max_lat) with "
92
+ f"min < max; got {bbox!r}"
93
+ )
94
+
95
+ if resolution is None:
96
+ res_x, res_y = ds.res_x, ds.res_y
97
+ elif isinstance(resolution, (int, float)):
98
+ res_x = res_y = float(resolution)
99
+ else:
100
+ res_x, res_y = (float(v) for v in resolution)
101
+ if res_x <= 0 or res_y <= 0:
102
+ raise ValueError(f"resolution must be positive; got {resolution!r}")
103
+
104
+ n_pixels = math.ceil((max_lon - min_lon) / res_x) * math.ceil(
105
+ (max_lat - min_lat) / res_y
106
+ )
107
+ if n_pixels > _MAX_PIXELS:
108
+ raise ValueError(
109
+ f"Request would be ~{n_pixels:,} pixels (> {_MAX_PIXELS:,}). "
110
+ "Use a smaller bbox or a coarser `resolution`."
111
+ )
112
+
113
+ content = get_coverage(ds, bbox, res_x, res_y, timeout=timeout)
114
+ da = _open_geotiff(content, mask=mask)
115
+ da.attrs["dataset"] = ds.key
116
+ da.attrs["source"] = ds.title
117
+ da.attrs["service_url"] = ds.url
118
+ return da
119
+
120
+
121
+ def save_geotiff(da: xr.DataArray, path: str) -> str:
122
+ """Write a DataArray returned by :func:`get_dem` to a GeoTIFF file.
123
+
124
+ Returns the path written, so it can be used inline.
125
+ """
126
+ da.rio.to_raster(path)
127
+ return path
@@ -0,0 +1,118 @@
1
+ """Registry of Geoscience Australia DEM datasets exposed over WCS.
2
+
3
+ Each entry describes a Web Coverage Service (WCS) endpoint published by
4
+ Geoscience Australia together with the metadata needed to issue a
5
+ ``GetCoverage`` request: the coverage identifier, the coordinate reference
6
+ system the service accepts for requests, and the native ground resolution.
7
+
8
+ The catalogue page these come from is:
9
+ https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+ _BASE = "https://services.ga.gov.au/gis/services/{service}/MapServer/WCSServer"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Dataset:
21
+ """Description of a single GA DEM coverage served over WCS."""
22
+
23
+ key: str
24
+ title: str
25
+ service: str
26
+ #: Coverage identifier used by the WCS 1.0.0 ``coverage`` parameter.
27
+ coverage: str
28
+ #: CRS the service accepts for request BBOX and output (EPSG code).
29
+ crs: str
30
+ #: Native pixel size in CRS units (degrees) along x and y.
31
+ res_x: float
32
+ res_y: float
33
+ description: str
34
+
35
+ @property
36
+ def url(self) -> str:
37
+ return _BASE.format(service=self.service)
38
+
39
+
40
+ # One arc-second is 1/3600 of a degree.
41
+ _ARCSEC = 1.0 / 3600.0
42
+
43
+ DATASETS = {
44
+ "srtm_1s_dem": Dataset(
45
+ key="srtm_1s_dem",
46
+ title="SRTM-derived 1 Second DEM (bare earth)",
47
+ service="DEM_SRTM_1Second_2024",
48
+ coverage="1",
49
+ crs="EPSG:4326",
50
+ res_x=_ARCSEC,
51
+ res_y=_ARCSEC,
52
+ description=(
53
+ "National ~30 m bare-earth Digital Elevation Model derived from the "
54
+ "February 2000 Shuttle Radar Topography Mission, with vegetation "
55
+ "offsets removed."
56
+ ),
57
+ ),
58
+ "srtm_1s_dem_h": Dataset(
59
+ key="srtm_1s_dem_h",
60
+ title="SRTM-derived 1 Second DEM-H (hydrologically enforced)",
61
+ service="DEM_SRTM_1Second_Hydro_Enforced_2024",
62
+ coverage="1",
63
+ crs="EPSG:4326",
64
+ res_x=_ARCSEC,
65
+ res_y=_ARCSEC,
66
+ description=(
67
+ "National ~30 m hydrologically enforced DEM (DEM-H): the smoothed "
68
+ "SRTM DEM conditioned with drainage enforcement so that water flows "
69
+ "consistently downhill. Best choice for hydrological modelling."
70
+ ),
71
+ ),
72
+ "lidar_5m": Dataset(
73
+ key="lidar_5m",
74
+ title="LiDAR-derived 5 m DEM of Australia",
75
+ service="DEM_LiDAR_5m_2025",
76
+ coverage="1",
77
+ # This service only accepts GDA94 geographic (EPSG:4283), not 4326.
78
+ crs="EPSG:4283",
79
+ res_x=5.5063478185957097e-05,
80
+ res_y=5.16012325277870332e-05,
81
+ description=(
82
+ "5 m bare-earth DEM compiled from 200+ LiDAR surveys (2001-2015). "
83
+ "Coverage is limited to surveyed areas: the populated coastal zone, "
84
+ "the Murray-Darling floodplains and individual population centres. "
85
+ "Requests outside surveyed areas return no data."
86
+ ),
87
+ ),
88
+ }
89
+
90
+ #: Datasets that exist in the SRTM 1 Second package but are NOT served over WCS
91
+ #: (download-only via the ELVIS portal). Kept here for discoverability.
92
+ DOWNLOAD_ONLY = {
93
+ "srtm_1s_dem_s": (
94
+ "SRTM-derived 1 Second DEM-S (smoothed). Not available as a WCS "
95
+ "coverage; download from the ELVIS portal: https://elevation.fsdf.org.au/"
96
+ ),
97
+ }
98
+
99
+ DEFAULT_DATASET = "srtm_1s_dem"
100
+
101
+
102
+ def get_dataset(key: str) -> Dataset:
103
+ """Return the :class:`Dataset` for ``key`` or raise a helpful error."""
104
+ try:
105
+ return DATASETS[key]
106
+ except KeyError:
107
+ available = ", ".join(sorted(DATASETS))
108
+ hint = ""
109
+ if key in DOWNLOAD_ONLY:
110
+ hint = f"\n'{key}' is download-only: {DOWNLOAD_ONLY[key]}"
111
+ raise KeyError(
112
+ f"Unknown dataset '{key}'. Available WCS datasets: {available}.{hint}"
113
+ ) from None
114
+
115
+
116
+ def list_datasets() -> list[str]:
117
+ """Return the keys of all datasets available over WCS."""
118
+ return sorted(DATASETS)
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: ausdem
3
+ Version: 0.1.0
4
+ Summary: Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia WCS services as xarray arrays.
5
+ Author-email: Sia Ghelichkhan <ghelichkhani.siavash@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/g-adopt/ausdem
8
+ Project-URL: Repository, https://github.com/g-adopt/ausdem
9
+ Project-URL: Issues, https://github.com/g-adopt/ausdem/issues
10
+ Project-URL: Changelog, https://github.com/g-adopt/ausdem/blob/main/CHANGELOG.md
11
+ Project-URL: Data source, https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data
12
+ Keywords: DEM,elevation,SRTM,LiDAR,Australia,Geoscience Australia,WCS
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: GIS
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: requests>=2.25
28
+ Requires-Dist: numpy
29
+ Requires-Dist: xarray
30
+ Requires-Dist: rioxarray
31
+ Requires-Dist: rasterio
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest; extra == "test"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest; extra == "dev"
36
+ Requires-Dist: build; extra == "dev"
37
+ Requires-Dist: twine; extra == "dev"
38
+ Requires-Dist: ruff; extra == "dev"
39
+ Requires-Dist: matplotlib; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # ausdem
43
+
44
+ [![CI](https://github.com/g-adopt/ausdem/actions/workflows/test.yml/badge.svg)](https://github.com/g-adopt/ausdem/actions/workflows/test.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/ausdem.svg)](https://pypi.org/project/ausdem/)
46
+ [![Python versions](https://img.shields.io/pypi/pyversions/ausdem.svg)](https://pypi.org/project/ausdem/)
47
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
48
+
49
+ Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia
50
+ straight into Python as georeferenced [xarray](https://docs.xarray.dev) arrays.
51
+
52
+ It talks to GA's public [Web Coverage Services](https://services.ga.gov.au),
53
+ asking only for the bounding box you want rather than downloading whole
54
+ continental tiles. The data and services come from GA's
55
+ [Digital Elevation Data](https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data)
56
+ page.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install ausdem
62
+ ```
63
+
64
+ Requires `requests`, `numpy`, `xarray`, `rioxarray` and `rasterio` (pulled in
65
+ automatically).
66
+
67
+ ## Usage
68
+
69
+ ```python
70
+ import ausdem
71
+
72
+ # A small area around Canberra (min_lon, min_lat, max_lon, max_lat)
73
+ dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
74
+
75
+ dem.plot() # it's a normal xarray DataArray
76
+ print(float(dem.max())) # highest point in the box
77
+
78
+ ausdem.save_geotiff(dem, "canberra.tif") # or: dem.rio.to_raster("canberra.tif")
79
+ ```
80
+
81
+ The returned object is an `xarray.DataArray` with dimensions `(y, x)`, a NaN
82
+ no-data mask, and a `.rio` accessor (CRS, transform, `to_raster`). So you get
83
+ the array for analysis and can drop it to a GeoTIFF whenever you like.
84
+
85
+ ### Choosing a dataset
86
+
87
+ ```python
88
+ ausdem.list_datasets()
89
+ # ['lidar_5m', 'srtm_1s_dem', 'srtm_1s_dem_h']
90
+
91
+ dem_h = ausdem.get_dem(bbox, dataset="srtm_1s_dem_h") # hydro-enforced
92
+ lidar = ausdem.get_dem(bbox, dataset="lidar_5m") # 5 m, where surveyed
93
+ ```
94
+
95
+ | key | product | resolution |
96
+ |-----------------|-------------------------------------------|------------|
97
+ | `srtm_1s_dem` | SRTM 1 Second DEM (bare earth), national | ~30 m |
98
+ | `srtm_1s_dem_h` | SRTM 1 Second DEM-H (hydro-enforced) | ~30 m |
99
+ | `lidar_5m` | LiDAR-derived 5 m DEM (surveyed areas) | 5 m |
100
+
101
+ The smoothed `DEM-S` product is not served over WCS; download it from the
102
+ [ELVIS portal](https://elevation.fsdf.org.au/). The LiDAR DEM only covers
103
+ surveyed areas (coastal zone, Murray-Darling floodplains, population centres);
104
+ requests outside coverage either come back as a no-data (NaN) tile or raise
105
+ `ausdem.WCSError`, depending on the area.
106
+
107
+ Pass `resolution=` (in degrees) to resample, e.g. `resolution=0.001` for a
108
+ coarser, lighter grid.
109
+
110
+ ## Command line
111
+
112
+ ```bash
113
+ ausdem 149.0 -35.4 149.1 -35.3 -o canberra.tif
114
+ ausdem 149.0 -35.4 149.1 -35.3 -o canberra_demh.tif -d srtm_1s_dem_h
115
+ ausdem --list
116
+ ```
117
+
118
+ ## Notes
119
+
120
+ Coordinates are decimal degrees. SRTM products are served in WGS84
121
+ (EPSG:4326) and the LiDAR product in GDA94 (EPSG:4283); for input bounding
122
+ boxes the two are interchangeable at this scale. Very large requests are
123
+ rejected client-side to avoid pulling huge rasters by accident; tile your area
124
+ or coarsen the resolution if you hit that.
125
+
126
+ ## Example
127
+
128
+ There is a small standalone script in [`examples/`](examples/) that fetches a
129
+ DEM and plots it:
130
+
131
+ ```bash
132
+ python examples/plot_dem.py # a box near Canberra, writes dem.png
133
+ ```
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ git clone https://github.com/g-adopt/ausdem
139
+ cd ausdem
140
+ pip install -e ".[dev]"
141
+ pytest -m "not network" # offline tests
142
+ pytest # include the live GA WCS test
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT, see [LICENSE](LICENSE).
148
+
149
+ ## Citation
150
+
151
+ If you use ausdem in your work, please cite it. Release archives are deposited
152
+ on Zenodo and a DOI will be added here after the first release; in the meantime
153
+ see [CITATION.cff](CITATION.cff).
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ausdem/__init__.py
5
+ ausdem/cli.py
6
+ ausdem/client.py
7
+ ausdem/core.py
8
+ ausdem/datasets.py
9
+ ausdem.egg-info/PKG-INFO
10
+ ausdem.egg-info/SOURCES.txt
11
+ ausdem.egg-info/dependency_links.txt
12
+ ausdem.egg-info/entry_points.txt
13
+ ausdem.egg-info/requires.txt
14
+ ausdem.egg-info/top_level.txt
15
+ tests/test_ausdem.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ausdem = ausdem.cli:main
@@ -0,0 +1,15 @@
1
+ requests>=2.25
2
+ numpy
3
+ xarray
4
+ rioxarray
5
+ rasterio
6
+
7
+ [dev]
8
+ pytest
9
+ build
10
+ twine
11
+ ruff
12
+ matplotlib
13
+
14
+ [test]
15
+ pytest
@@ -0,0 +1 @@
1
+ ausdem
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ausdem"
7
+ dynamic = ["version"]
8
+ description = "Fetch Australian Digital Elevation Model (DEM) data from Geoscience Australia WCS services as xarray arrays."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "Sia Ghelichkhan", email = "ghelichkhani.siavash@gmail.com" }]
12
+ license = { text = "MIT" }
13
+ keywords = ["DEM", "elevation", "SRTM", "LiDAR", "Australia", "Geoscience Australia", "WCS"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: GIS",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.25",
29
+ "numpy",
30
+ "xarray",
31
+ "rioxarray",
32
+ "rasterio",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ test = ["pytest"]
37
+ dev = ["pytest", "build", "twine", "ruff", "matplotlib"]
38
+
39
+ [project.scripts]
40
+ ausdem = "ausdem.cli:main"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/g-adopt/ausdem"
44
+ Repository = "https://github.com/g-adopt/ausdem"
45
+ Issues = "https://github.com/g-adopt/ausdem/issues"
46
+ Changelog = "https://github.com/g-adopt/ausdem/blob/main/CHANGELOG.md"
47
+ "Data source" = "https://www.ga.gov.au/scientific-topics/national-location-information/digital-elevation-data"
48
+
49
+ [tool.setuptools.dynamic]
50
+ version = { attr = "ausdem.__version__" }
51
+
52
+ [tool.setuptools.packages.find]
53
+ include = ["ausdem*"]
54
+
55
+ [tool.pytest.ini_options]
56
+ markers = ["network: test that hits the live GA WCS service"]
57
+
58
+ [tool.ruff]
59
+ line-length = 88
ausdem-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,71 @@
1
+ """Tests for ausdem.
2
+
3
+ The network test is marked so it can be skipped offline:
4
+ pytest -m "not network"
5
+ """
6
+
7
+ import numpy as np
8
+ import pytest
9
+
10
+ import ausdem
11
+ from ausdem import core
12
+
13
+
14
+ def test_list_datasets():
15
+ keys = ausdem.list_datasets()
16
+ assert "srtm_1s_dem" in keys
17
+ assert "srtm_1s_dem_h" in keys
18
+ assert "lidar_5m" in keys
19
+
20
+
21
+ def test_unknown_dataset_hint():
22
+ with pytest.raises(KeyError):
23
+ ausdem.get_dataset("does_not_exist")
24
+ # DEM-S should point the user at the download-only note.
25
+ with pytest.raises(KeyError, match="download-only"):
26
+ ausdem.get_dataset("srtm_1s_dem_s")
27
+
28
+
29
+ def test_bad_bbox():
30
+ with pytest.raises(ValueError):
31
+ ausdem.get_dem((149.1, -35.4, 149.0, -35.3)) # min_lon > max_lon
32
+
33
+
34
+ def test_pixel_guard():
35
+ # Continental bbox at native res blows past the pixel cap before any I/O.
36
+ with pytest.raises(ValueError, match="pixels"):
37
+ ausdem.get_dem((113.0, -44.0, 154.0, -10.0), resolution=0.0002777)
38
+
39
+
40
+ def test_open_geotiff_masks_sentinel():
41
+ # Build a tiny GeoTIFF in memory with a sentinel value and check masking.
42
+ from rasterio.io import MemoryFile
43
+ from rasterio.transform import from_origin
44
+
45
+ data = np.array([[10.0, -3.4e38], [20.0, 30.0]], dtype="float32")
46
+ with MemoryFile() as mem:
47
+ with mem.open(
48
+ driver="GTiff",
49
+ height=2,
50
+ width=2,
51
+ count=1,
52
+ dtype="float32",
53
+ crs="EPSG:4326",
54
+ transform=from_origin(149.0, -35.0, 0.1, 0.1),
55
+ ) as dst:
56
+ dst.write(data, 1)
57
+ content = mem.read()
58
+
59
+ da = core._open_geotiff(content, mask=True)
60
+ assert da.name == "elevation"
61
+ assert bool(np.isnan(da.values).any())
62
+ assert np.nanmax(da.values) == 30.0
63
+
64
+
65
+ @pytest.mark.network
66
+ def test_get_dem_live():
67
+ dem = ausdem.get_dem((149.0, -35.4, 149.1, -35.3))
68
+ assert dem.dims == ("y", "x")
69
+ assert dem.rio.crs is not None
70
+ assert np.isfinite(dem.values).any()
71
+ assert dem.attrs["dataset"] == "srtm_1s_dem"