pytopocomplexity 0.7.2__tar.gz → 0.7.4__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.1
2
2
  Name: pytopocomplexity
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: A package for topographic complexity analysis
5
5
  Home-page: https://github.com/GeoLarryLai/pyTopoComplexity
6
6
  Author: Larry Syu-Heng Lai
@@ -208,6 +208,7 @@ License: Apache License
208
208
  limitations under the License.
209
209
 
210
210
  Project-URL: Homepage, https://github.com/GeoLarryLai/pyTopoComplexity
211
+ Project-URL: Repository, https://github.com/GeoLarryLai/pyTopoComplexity.git
211
212
  Classifier: Programming Language :: Python :: 3
212
213
  Classifier: License :: OSI Approved :: Apache Software License
213
214
  Classifier: Operating System :: OS Independent
@@ -215,7 +216,7 @@ Requires-Python: >=3.10
215
216
  Description-Content-Type: text/markdown
216
217
  License-File: LICENSE
217
218
 
218
- # pyTopoComlexity (v0.7.2)
219
+ # pyTopoComlexity (v0.7.4)
219
220
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.11239338.svg)](https://doi.org/10.5281/zenodo.11239338)
220
221
 
221
222
  `pytopocomplexity` is an open-source Python package designed to measure the topographic complexity (i.e., surface roughness) of land surfaces using digital elevation model (DEM) data. This package includes modules for three methods commonly used in the fields of geomorphology and oceanography for measuring topographic complexity, which are not fully available in Geographic Information System (GIS) software like QGIS.
@@ -267,7 +268,6 @@ The rugosity index is determined as the ratio of the real surface area to the ge
267
268
  ## Requirements
268
269
  For `pytopocomplexity` package
269
270
  * Python >= 3.10
270
- * `os`
271
271
  * `numpy`
272
272
  * `scipy`
273
273
  * `rasterio`
@@ -1,4 +1,4 @@
1
- # pyTopoComlexity (v0.7.2)
1
+ # pyTopoComlexity (v0.7.4)
2
2
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.11239338.svg)](https://doi.org/10.5281/zenodo.11239338)
3
3
 
4
4
  `pytopocomplexity` is an open-source Python package designed to measure the topographic complexity (i.e., surface roughness) of land surfaces using digital elevation model (DEM) data. This package includes modules for three methods commonly used in the fields of geomorphology and oceanography for measuring topographic complexity, which are not fully available in Geographic Information System (GIS) software like QGIS.
@@ -50,7 +50,6 @@ The rugosity index is determined as the ratio of the real surface area to the ge
50
50
  ## Requirements
51
51
  For `pytopocomplexity` package
52
52
  * Python >= 3.10
53
- * `os`
54
53
  * `numpy`
55
54
  * `scipy`
56
55
  * `rasterio`
@@ -0,0 +1,140 @@
1
+ # pyTopoComlexity (v0.7.4)
2
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.11239338.svg)](https://doi.org/10.5281/zenodo.11239338)
3
+ <p align="left">
4
+ <img src="image/pyTopoComplexity_logo.png" width="30%" height="30%""/>
5
+ </p>
6
+
7
+ `pytopocomplexity` is an open-source Python package designed to measure the topographic complexity (i.e., surface roughness) of land surfaces using digital elevation model (DEM) data. This package includes modules for three methods commonly used in the fields of geomorphology and oceanography for measuring topographic complexity, which are not fully available in Geographic Information System (GIS) software like QGIS.
8
+
9
+ | Modules | Method Descriptions |
10
+ | ------------- | ------------- |
11
+ | pycwtmexhat.py | Quanitfy the wavelet-based curvature of the land surface using two-dimensional continuous wavelet transform (2D-CWT) with a Mexican Hat wevalet |
12
+ | pyfracd.py | Conduct fractal dimension analysis on the land surface |
13
+ | pyrugostiy.py | Calculate rugosity indext of the land surface |
14
+
15
+ In this GitHub repository, each module has a corresponding example Jupyter Notebook file that includes detailed instructions on module usage and brief explanations of the applied theories with cited references. Example raster file data are included in the `~/example/` folder.
16
+
17
+ There is also an additional Jupyter Notebook, `nonlineardiff_landlab.ipynb`, which leverages the power of [`landlab`](https://landlab.readthedocs.io/en/latest/index.html) to perform forward simulation of landscape smoothing through non-linear hillslope diffusion process.
18
+
19
+ > [!CAUTION]
20
+ > This package is under developement and has not officially released. Use it with caution.
21
+
22
+ ## Installation
23
+
24
+ ```
25
+ pip install pytopocomplexity
26
+ ```
27
+
28
+ ## Citation
29
+
30
+ The current version is still a pre-release. If you use the current version of `pytopocomplexity` and associated Jupyter Notebooks (including nonlinear diffusion simulation) in your work, please cite the Zenodo DOI:
31
+ * Lai, L. S.-H. (2024). pyTopoComplexity. Zenodo. https://doi.org/10.5281/zenodo.11239338
32
+
33
+ ## Modules for Surface Complexity Measurement
34
+
35
+ ### 1. `pycwtmexhat`: 2D Continuous Wavelet Transform Method
36
+
37
+ ```python
38
+ from pytopocomplexity import pycwtmexhat
39
+ ```
40
+
41
+ The module `pycwtmexhat` uses two-dimensional continuous wavelet transform (2D-CWT) with a Mexican Hat wevalet to measure the topographic complexity (i.e., surface roughness) of a land surface from a Digital Elevation Model (DEM). Such method quanitfy the wavelet-based curvature of the surface, which has been proposed to be a effective geomorphic metric for identifying and estimating the ages of historical deep-seated landslide deposits.
42
+
43
+ The method and early version of the code was developed by Dr. Adam M. Booth (Portland State Univeristy) in [2009](https://doi.org/10.1016/j.geomorph.2009.02.027), written in MATLAB (Source code available from [Booth's personal website](https://web.pdx.edu/~boothad/tools.html)). This MATLAB code was later revised and adapted by Dr. Sean R. LaHusen (Univeristy of Washington) and Dr. Erich N. Herzig (Univeristy of Washington) in their research ([LaHusen et al., 2020](https://doi.org/10.1126/sciadv.aba6790); [Herzig et al. (2023)](https://doi.org/10.1785/0120230079)). Dr. Larry Syu-Heng Lai (Univeristy of Washington), under the supervision of Dr. Alison R. Duvall (Univeristy of Washington), translated the code into this optimized open-source Python version in 2024.
44
+
45
+ See `pycwtmexhat_example.ipynb` for detailed explanations and usage instructions.
46
+
47
+ <p align="center">
48
+ <img src="image/pycwtmexhat.png" width="100%" height="100%""/>
49
+ </p>
50
+
51
+ ### 2. `pyfracd`: Fractal Dimentsion Analysis
52
+
53
+ ```python
54
+ from pytopocomplexity import pyfracd
55
+ ```
56
+
57
+ The `pyfracd` module calculates local fractal dimensions to assess topographic complexity. It also computes reliability parameters such as the standard error and the coefficient of determination (R²). The development of pyfracd is made possible through the gratitude of Dr. Eulogio Pardo-Iguzquiza, who kindly shared his Fortran code used in his recent publication [Pardo-Igúzquiza and Dowd (2022)](https://doi.org/10.1016/j.icarus.2022.115109).
58
+
59
+ The local fractal dimension is determined by intersecting the surface within a moving window with four vertical planes in principal geographical directions, simplifying the problem to one-dimensional topographic profiles. The fractal dimension of these profiles is estimated using the variogram method, which models the relationship between dissimilarity and distance using a power-law function. While the fractal dimension value does not directly scale with the degree of surface roughness, smoother or more regular surfaces generally have lower fractal dimension values (closer to 2), whereas surfaces with higher fractal dimension values tend to be more complex or irregular. This method has been applied in terrain analysis for understanding spatial variability in surface roughness, classifying geomorphologic features, uncovering hidden spatial structures, and supporting geomorphological and geological mapping on Earth and other planetary bodies.
60
+
61
+ See `pyfracd_example.ipynb` for detailed explanations and usage instructions.
62
+
63
+ <p align="center">
64
+ <img src="image/pyfracd.png" width="100%" height="100%""/>
65
+ </p>
66
+
67
+ ### 3. `pyrugosity`: Rugosity Index
68
+
69
+ ```python
70
+ from pytopocomplexity import pyrugosity
71
+ ```
72
+
73
+ The module `pyrugosity` measure rugosity index of the land surface, which is widely used to assess landscape structural complexity. The development of this module is influenced by another open-source tool [`Rugosity_Calculator`](https://github.com/drk944/Rugosity_Calculator) created by [drk944](https://github.com/drk944).
74
+
75
+ The rugosity index is determined as the ratio of the real surface area to the geometric surface area, highlighting smaller-scale variations in surface height. This module adapt triangulated irregular networks method ([Jenness, 2004](https://doi.org/10.2193/0091-7648(2004)032[0829:CLSAFD]2.0.CO;2)), which approximate the surface area of with within each 9 cell as the sum of 8 truncated-triangle area connecting each cell centerpoint with the centerpoints of the 8 surrounding cells. The geometric surface area is assumed to be the planimetric area of the center cell. By definition, the rugosity index is as a minimum value of one (completely flate surface). Typical valuesrange from one to three although larger values are possible in very steep terrains. Such method has been applied in classifying seafloor types by marine geologists and geomorphologist, small-scale hydrodynamics by oceanographers, and studying available habitats in the landscape by ecologists and coral biologists.
76
+
77
+ See `pyrugosity_example.ipynb` for detailed explanations and usage instructions.
78
+
79
+ <p align="center">
80
+ <img src="image/pyrugosity.png" width="100%" height="100%""/>
81
+ </p>
82
+
83
+ ## Forward Simulation of Landscape Smoothing by Nonlinear Hillslope Diffusion Process
84
+
85
+ In the `~/example/` folder, the Jupyter Notebook file `nonlineardiff_landlab.ipynb` demonstrates the use of [`landlab`](https://landlab.readthedocs.io/en/latest/index.html), an open-source Python framework for simulating landscape evolution, to model topographic smoothing driven by near-surface soil disturbance and downslope soil creep processes. Specifically, this notebook employs the [`TaylorNonLinearDiffuser`](https://landlab.readthedocs.io/en/latest/reference/components/taylor_nonlinear_hillslope_flux.html) component from LandLab, described as one element in the [`terrainBento`](https://github.com/TerrainBento/terrainbento) package (developed by [Barnhart et al. (2019)](https://gmd.copernicus.org/articles/12/1267/2019/), to simulate topographic smoothing over time through non-linear hillslope diffusion processes ([Roering et al., 1999](https://doi.org/10.1029/1998WR900090)).
86
+
87
+ Users need to define the diffusion coefficient (K) for the simulation. The code will automatically detect the units of the XYZ directions (must be in feet or meters) of the input DEM raster file and convert the unit for K accordingly.
88
+
89
+ <p align="center">
90
+ <img src="image/NonlinearDiff_demo.gif" width="65%" height="65%" align="center"/>
91
+ </p>
92
+
93
+ > [!WARNING]
94
+ > There is a known/unresolved stability issue when running `TaylorNonLinearDiffuser` component with a DEM with reprojected coordinate reference system (CRS) through GIS softwares. When using the example DEM, users may only use the pre-reprojected DEM with CRS: NAD83/Washington South (ftUS) (EPSG: 2286) and Z unit in US survey feet (e.g., the DEM files named with *"_f_3ftgrid"* or *"_f_6ftgrid"*).
95
+
96
+ ## Example DEM Raster Files
97
+
98
+ Along with he example Jupyter Notebook files, this repository include example LiDAR DEM files under `~/example/ExampleDEM/` that cover the area and nearby region of a deep-seated landslide occurred in 2014 at Oso area of the North Fork Stillaguamish River (NFSR) valley, Washington State, USA. The souce LiDAR DEM files were compiled from 'Stillaguamish 2014' and 'Snohoco Hazel 2006' projects that was originally contracted by Washington State Department of Transportation (WSDOT), downloaded from the [Washington Lidar Portal](http://lidarportal.dnr.wa.gov) on April 4, 2024.
99
+
100
+ A goal of this work allow users to reproduce the research by [Booth et al. (2017)](https://doi.org/10.1002/2016JF003934) and permit comparison of topographic complexity metrics derived from other regions using `pytopocomplexity` package and the `nonlineardiff_landlab.ipynb` simulation tools presented in this repository.
101
+
102
+ The example DEM raster files have various grid size, coordinate reference system (CRS), and unit of grid value (elevation, Z).
103
+
104
+ | LiDAR DEM Files | CRS | XY Grid Size | Z Unit | Descriptions |
105
+ | ------------- | ------------- | ------------- | ------------- | ------------- |
106
+ | Ososlid2014_f_3ftgrid.tif | NAD83/Washington South (ftUS) (EPSG: 2286) | 3.0 [US survey feet] | US survey feet | 2014 Oso Landslide |
107
+ | Ososlid2014_m_3ftgrid.tif | NAD83/Washington South (EPSG: 32149) | ~0.9144 [meters] | meters | 2014 Oso Landslide |
108
+ | Ososlid2014_f_6ftgrid.tif | NAD83/Washington South (ftUS) (EPSG: 2286) | 6.0 [US survey feet] | US survey feet | 2014 Oso Landslide |
109
+ | Ososlid2014_m_6ftgrid.tif | NAD83/Washington South (EPSG: 32149) | ~1.8288 [meters] | meters | 2014 Oso Landslide |
110
+ | Osoarea2014_f_6ftgrid.tif | NAD83/Washington South (ftUS) (EPSG: 2286) | 6.0 [US survey feet] | US survey feet | 2014 Oso Landslide & nearby NFSR valley |
111
+
112
+ > [!NOTE]
113
+ > When testing the code with the example DEM files, users should place the entire `~/ExampleDEM/` subfolder in the same directory as the Jupyter Notebook files. Both the `pytopocomplexity` package and the `nonlineardiff_landlab.ipynb` land-smoothing modeling tool have the capability to automatically detect the grid spacing and the units of the XYZ directions (must be in feet or meters) of the input DEM raster and compute the results in SI units.
114
+
115
+ ## Requirements
116
+ For `pytopocomplexity` package
117
+ * Python >= 3.10
118
+ * `numpy`
119
+ * `scipy`
120
+ * `rasterio`
121
+ * `dask`
122
+ * `matplotlib`
123
+ * `tqdm`
124
+ * `numba`
125
+
126
+ Additional packages for Jupyter Notebook examples:
127
+ * `glob`
128
+ * `pandas`
129
+ * `jupyter`
130
+
131
+ for landscape smoothing simulation
132
+ * `landlab` for landscape smoothing simulation ([User Guide](https://landlab.readthedocs.io/en/latest/index.html))
133
+ * Used components: `TaylorNonLinearDiffuser`, `esri_ascii`, `imshowhs`
134
+ * `osgeo` [if imported raster is in the geotiff format]
135
+ * `ipywidgets` [optional for interactive visualization]
136
+
137
+ See also the `environment.yml` file which can be used to create a virtual environment.
138
+
139
+ ## License
140
+ pyTopoComlexity is licensed under the [Apache License 2.0](LICENSE).
@@ -1,12 +1,12 @@
1
1
  [build-system]
2
- requires = ["setuptools>=42", "wheel"]
2
+ requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytopocomplexity"
7
- version = "0.7.2"
7
+ dynamic = ["version"]
8
8
  description = "A package for topographic complexity analysis"
9
- readme = "README.md"
9
+ readme = "README-pypi.md"
10
10
  authors = [{name = "Larry Syu-Heng Lai", email = "larrysyuhenglai@gmail.com"}]
11
11
  license = {file = "LICENSE"}
12
12
  classifiers = [
@@ -14,20 +14,25 @@ classifiers = [
14
14
  "License :: OSI Approved :: Apache Software License",
15
15
  "Operating System :: OS Independent",
16
16
  ]
17
+ requires-python = ">=3.10"
17
18
  dependencies = [
18
- "os",
19
- "numpy",
20
- "rasterio",
21
- "matplotlib",
22
- "dask",
23
- "numba",
24
- "scipy",
25
- "tqdm",
19
+ "numpy>=1.20",
20
+ "scipy>=1.6",
21
+ "rasterio>=1.2",
22
+ "dask>=2021.0",
23
+ "matplotlib>=3.3",
24
+ "tqdm>=4.0",
25
+ "numba>=0.53",
26
+ "gdal>=3.0",
27
+ "statsmodels>=0.12",
26
28
  ]
27
- requires-python = ">=3.10"
28
29
 
29
30
  [project.urls]
30
31
  Homepage = "https://github.com/GeoLarryLai/pyTopoComplexity"
32
+ Repository = "https://github.com/GeoLarryLai/pyTopoComplexity.git"
31
33
 
32
34
  [tool.setuptools]
33
35
  packages = ["pytopocomplexity"]
36
+
37
+ [tool.setuptools_scm]
38
+ write_to = "pytopocomplexity/_version.py"
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import numpy as np
3
3
  import rasterio
4
+ import dask
4
5
  import dask.array as da
5
6
  from dask.diagnostics import ProgressBar
6
7
  from scipy.signal import fftconvolve, convolve2d
@@ -98,8 +99,21 @@ class pycwtmexhat:
98
99
  else:
99
100
  raise ValueError("The unit of elevation 'z' must be in feet or meters.")
100
101
 
101
- C2 = self.conv2_mexh(Z, s, Delta)
102
- result = np.abs(C2)
102
+ # Create a dask array from Z
103
+ dask_Z = da.from_array(Z, chunks=Z.shape)
104
+
105
+ # Create a delayed version of conv2_mexh
106
+ delayed_conv2_mexh = dask.delayed(self.conv2_mexh)
107
+
108
+ # Apply the convolution
109
+ C2 = delayed_conv2_mexh(dask_Z, s, Delta)
110
+
111
+ # Compute the absolute value
112
+ result = da.abs(C2)
113
+
114
+ # Compute the result with a progress bar
115
+ with ProgressBar():
116
+ result = result.compute()
103
117
 
104
118
  cropedge = np.ceil(s * 4)
105
119
  fringeval = int(cropedge)
@@ -0,0 +1,259 @@
1
+ import os
2
+ import numpy as np
3
+ import rasterio
4
+ import matplotlib.pyplot as plt
5
+ from numba import jit
6
+ from tqdm.auto import tqdm
7
+ from multiprocessing import Pool, cpu_count
8
+
9
+ class pyfracd:
10
+ def __init__(self, window_size=10, chunk_processing=True, chunksize=(512, 512)):
11
+ self.window_size = window_size
12
+ self.chunk_processing = chunk_processing
13
+ self.chunksize = chunksize
14
+ self.fd2_out = None
15
+ self.se2_out = None
16
+ self.r2_out = None
17
+ self.ft2mUS = 1200/3937 # US survey foot to meter conversion factor
18
+ self.ft2mInt = 0.3048 # International foot to meter conversion factor
19
+
20
+ @staticmethod
21
+ @jit(nopython=True, fastmath=True)
22
+ def vario(z, window_size, imo):
23
+ fac = 1.0 if imo == 1 else np.sqrt(2.0)
24
+ x = np.arange(1, window_size + 1) * fac
25
+ y = np.zeros(window_size)
26
+ npa = np.zeros(window_size, dtype=np.int32)
27
+
28
+ for i in range(window_size):
29
+ diffs = z[:-i-1] - z[i+1:]
30
+ y[i] = np.sum(diffs**2) / (2 * len(diffs))
31
+ npa[i] = len(diffs)
32
+
33
+ mask = npa > 0
34
+ y = y[mask]
35
+ x = x[mask]
36
+
37
+ if len(x) < 2:
38
+ return np.nan, np.nan, np.nan
39
+
40
+ x = np.log(x)
41
+ y = np.log(y)
42
+
43
+ n = len(x)
44
+ sumx = np.sum(x)
45
+ sumy = np.sum(y)
46
+ sumx2 = np.sum(x**2)
47
+ sumxy = np.sum(x * y)
48
+
49
+ b = (n * sumxy - sumx * sumy) / (n * sumx2 - sumx**2)
50
+ a = (sumy - b * sumx) / n
51
+
52
+ fd2 = 3.0 - b / 2.0
53
+ yfit = a + b * x
54
+
55
+ varb = (np.sum((y - yfit)**2) / (n - 2)) / (sumx2 - sumx**2 / n)
56
+ se2 = np.sqrt(varb) / 2.0
57
+
58
+ sum1 = np.sum((yfit - y)**2)
59
+ sum2 = np.sum((y - np.mean(y))**2)
60
+ r2 = 1.0 - sum1 / sum2
61
+
62
+ return fd2, se2, r2
63
+
64
+ def analyze(self, input_dir, variograms=True):
65
+ self.input_dir = input_dir
66
+ with rasterio.open(input_dir) as src:
67
+ meta = src.meta.copy()
68
+ Z = src.read(1)
69
+ Zunit = src.crs.linear_units
70
+ self.fd2_out = np.zeros((src.height, src.width), dtype=np.float32)
71
+ self.se2_out = np.zeros((src.height, src.width), dtype=np.float32)
72
+ self.r2_out = np.zeros((src.height, src.width), dtype=np.float32)
73
+
74
+ # Get the cell size
75
+ cell_size = src.transform[0]
76
+
77
+ if any(unit in Zunit.lower() for unit in ["metre", "meter", "meters"]):
78
+ pass
79
+ elif any(unit in Zunit.lower() for unit in ["foot", "feet", "ft"]):
80
+ if any(unit in Zunit.lower() for unit in ["us", "united states"]):
81
+ Z = Z * self.ft2mUS
82
+ cell_size = cell_size * self.ft2mUS
83
+ else:
84
+ Z = Z * self.ft2mInt
85
+ cell_size = cell_size * self.ft2mInt
86
+ else:
87
+ raise ValueError("The unit of elevation 'z' must be in feet or meters.")
88
+
89
+ if not self.chunk_processing:
90
+ self._process_non_chunked(Z, src.height, src.width)
91
+ else:
92
+ self._process_chunked(Z, src.height, src.width)
93
+
94
+ print(f"FD2 MIN : {np.nanmin(self.fd2_out)}")
95
+ print(f"FD2 MAX : {np.nanmax(self.fd2_out)}")
96
+ print(f"SE MIN : {np.nanmin(self.se2_out)}")
97
+ print(f"SE MAX : {np.nanmax(self.se2_out)}")
98
+ print(f"R2 MIN : {np.nanmin(self.r2_out)}")
99
+ print(f"R2 MAX : {np.nanmax(self.r2_out)}")
100
+
101
+ if variograms:
102
+ self.plot_sample_variograms(Z)
103
+
104
+ # Calculate window size in meters
105
+ window_size_m = cell_size * self.window_size
106
+
107
+ return Z, self.fd2_out, self.se2_out, self.r2_out, meta, window_size_m
108
+
109
+ def _process_non_chunked(self, Z, height, width):
110
+ for j in tqdm(range(height), desc="Analyzing rows"):
111
+ for i in range(width):
112
+ results = []
113
+ # Horizontal slice
114
+ data = Z[j, max(0, i-self.window_size):min(i+self.window_size+1, width)]
115
+ if len(data) > self.window_size:
116
+ results.append(self.vario(data, self.window_size, 1))
117
+ # Vertical slice
118
+ data = Z[max(0, j-self.window_size):min(j+self.window_size+1, height), i]
119
+ if len(data) > self.window_size:
120
+ results.append(self.vario(data, self.window_size, 1))
121
+ # Diagonal slices
122
+ for di, dj in [(1, 1), (1, -1)]:
123
+ data = []
124
+ for k in range(-self.window_size, self.window_size+1):
125
+ if 0 <= j+k*dj < height and 0 <= i+k*di < width:
126
+ data.append(Z[j+k*dj, i+k*di])
127
+ if len(data) > self.window_size:
128
+ results.append(self.vario(np.array(data), self.window_size, 2))
129
+ if results:
130
+ fd2, se2, r2 = np.nanmean(results, axis=0)
131
+ self.fd2_out[j, i] = np.clip(fd2, 2.0, 3.0)
132
+ self.se2_out[j, i] = se2
133
+ self.r2_out[j, i] = r2
134
+
135
+ def _process_chunked(self, Z, height, width):
136
+ chunk_height, chunk_width = self.chunksize
137
+ overlap = self.window_size // 2 + 1
138
+ chunks = []
139
+ for j in range(0, height, chunk_height - 2*overlap):
140
+ for i in range(0, width, chunk_width - 2*overlap):
141
+ chunk = Z[max(0, j-overlap):min(j+chunk_height+overlap, height),
142
+ max(0, i-overlap):min(i+chunk_width+overlap, width)]
143
+ chunks.append((chunk, i, j))
144
+
145
+ with tqdm(total=height, desc="Analyzing rows") as pbar:
146
+ with Pool(cpu_count()) as pool:
147
+ for fd2, se2, r2, i, j in pool.imap(self._process_chunk_parallel, chunks):
148
+ h, w = fd2.shape
149
+ non_overlap_h = max(0, h - 2*overlap)
150
+ non_overlap_w = max(0, w - 2*overlap)
151
+ self.fd2_out[j:j+non_overlap_h, i:i+non_overlap_w] = fd2[overlap:overlap+non_overlap_h, overlap:overlap+non_overlap_w]
152
+ self.se2_out[j:j+non_overlap_h, i:i+non_overlap_w] = se2[overlap:overlap+non_overlap_h, overlap:overlap+non_overlap_w]
153
+ self.r2_out[j:j+non_overlap_h, i:i+non_overlap_w] = r2[overlap:overlap+non_overlap_h, overlap:overlap+non_overlap_w]
154
+ pbar.update(non_overlap_h)
155
+
156
+ def _process_chunk_parallel(self, args):
157
+ Z, offset_i, offset_j = args
158
+ fd2_out = np.zeros(Z.shape, dtype=np.float32)
159
+ se2_out = np.zeros(Z.shape, dtype=np.float32)
160
+ r2_out = np.zeros(Z.shape, dtype=np.float32)
161
+
162
+ for j in range(Z.shape[0]):
163
+ for i in range(Z.shape[1]):
164
+ results = []
165
+
166
+ # Horizontal slice
167
+ data = Z[j, max(0, i-self.window_size):min(i+self.window_size+1, Z.shape[1])]
168
+ if len(data) > self.window_size:
169
+ results.append(self.vario(data, self.window_size, 1))
170
+
171
+ # Vertical slice
172
+ data = Z[max(0, j-self.window_size):min(j+self.window_size+1, Z.shape[0]), i]
173
+ if len(data) > self.window_size:
174
+ results.append(self.vario(data, self.window_size, 1))
175
+
176
+ # Diagonal slices
177
+ for di, dj in [(1, 1), (1, -1)]:
178
+ data = []
179
+ for k in range(-self.window_size, self.window_size+1):
180
+ if 0 <= j+k*dj < Z.shape[0] and 0 <= i+k*di < Z.shape[1]:
181
+ data.append(Z[j+k*dj, i+k*di])
182
+ if len(data) > self.window_size:
183
+ results.append(self.vario(np.array(data), self.window_size, 2))
184
+
185
+ if results:
186
+ fd2, se2, r2 = np.nanmean(results, axis=0)
187
+ fd2_out[j, i] = np.clip(fd2, 2.0, 3.0)
188
+ se2_out[j, i] = se2
189
+ r2_out[j, i] = r2
190
+
191
+ return fd2_out, se2_out, r2_out, offset_i, offset_j
192
+
193
+ def plot_sample_variograms(self, Z):
194
+ middle_row = Z.shape[0] // 2
195
+ horizontal_slice = Z[middle_row, max(0, Z.shape[1]//2-self.window_size):min(Z.shape[1]//2+self.window_size+1, Z.shape[1])]
196
+ self.plot_variogram(horizontal_slice, imo=1, title="Horizontal Variogram (Middle Row)")
197
+
198
+ middle_col = Z.shape[1] // 2
199
+ vertical_slice = Z[max(0, Z.shape[0]//2-self.window_size):min(Z.shape[0]//2+self.window_size+1, Z.shape[0]), middle_col]
200
+ self.plot_variogram(vertical_slice, imo=1, title="Vertical Variogram (Middle Column)")
201
+
202
+ diagonal_slice = [Z[i, i] for i in range(max(0, Z.shape[0]//2-self.window_size), min(Z.shape[0]//2+self.window_size+1, min(Z.shape)))]
203
+ self.plot_variogram(diagonal_slice, imo=2, title="Diagonal Variogram")
204
+
205
+ def plot_variogram(self, z, imo, title=None):
206
+ z = np.array(z)
207
+ fac = 1.0 if imo == 1 else np.sqrt(2.0)
208
+ x = np.arange(1, self.window_size + 1) * fac
209
+ y = np.zeros(self.window_size)
210
+ npa = np.zeros(self.window_size, dtype=np.int32)
211
+
212
+ for i in range(self.window_size):
213
+ diffs = z[:-i-1] - z[i+1:]
214
+ y[i] = np.sum(diffs**2) / (2 * len(diffs))
215
+ npa[i] = len(diffs)
216
+
217
+ mask = npa > 0
218
+ y = y[mask]
219
+ x = x[mask]
220
+
221
+ if len(x) < 2:
222
+ print("Not enough data points for variogram plot")
223
+ return
224
+
225
+ plt.figure(figsize=(5, 3))
226
+ plt.scatter(np.log(x), np.log(y), color='blue', label='Data')
227
+
228
+ coef = np.polyfit(np.log(x), np.log(y), 1)
229
+ poly1d_fn = np.poly1d(coef)
230
+ plt.plot(np.log(x), poly1d_fn(np.log(x)), '--r', label='Fitted Line')
231
+
232
+ plt.xlabel('Log(Lag Distance)')
233
+ plt.ylabel('Log(Semivariance)')
234
+ plt.title(title or 'Variogram Plot')
235
+ plt.legend()
236
+ plt.grid(True)
237
+ plt.show()
238
+
239
+ slope = coef[0]
240
+ fd2 = 3.0 - slope / 2.0
241
+ print(f"Estimated Fractal Dimension: {fd2}")
242
+
243
+ def export_results(self, fd2_output_dir, se2_output_dir, r2_output_dir, meta):
244
+ if meta is None:
245
+ raise ValueError("Metadata is missing. Cannot export the result.")
246
+
247
+ meta.update(dtype=rasterio.float32, count=1, compress='deflate', bigtiff='IF_SAFER')
248
+
249
+ with rasterio.open(fd2_output_dir, 'w', **meta) as dst:
250
+ dst.write(self.fd2_out.astype(rasterio.float32), 1)
251
+ print(f"Processed result saved to {os.path.basename(fd2_output_dir)}")
252
+
253
+ with rasterio.open(se2_output_dir, 'w', **meta) as dst:
254
+ dst.write(self.se2_out.astype(rasterio.float32), 1)
255
+ print(f"Processed result saved to {os.path.basename(se2_output_dir)}")
256
+
257
+ with rasterio.open(r2_output_dir, 'w', **meta) as dst:
258
+ dst.write(self.r2_out.astype(rasterio.float32), 1)
259
+ print(f"Processed result saved to {os.path.basename(r2_output_dir)}")
@@ -8,7 +8,7 @@ from scipy.ndimage import generic_filter
8
8
  from tqdm.auto import tqdm
9
9
 
10
10
  class pyrugosity:
11
- def __init__(self, chunk_processing=False, chunksize=(512, 512)):
11
+ def __init__(self, chunk_processing=True, chunksize=(512, 512)):
12
12
  self.ft2mUS = 1200/3937 # US survey foot to meter conversion factor
13
13
  self.ft2mInt = 0.3048 # International foot to meter conversion factor
14
14
  self.chunk_processing = chunk_processing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytopocomplexity
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: A package for topographic complexity analysis
5
5
  Home-page: https://github.com/GeoLarryLai/pyTopoComplexity
6
6
  Author: Larry Syu-Heng Lai
@@ -208,6 +208,7 @@ License: Apache License
208
208
  limitations under the License.
209
209
 
210
210
  Project-URL: Homepage, https://github.com/GeoLarryLai/pyTopoComplexity
211
+ Project-URL: Repository, https://github.com/GeoLarryLai/pyTopoComplexity.git
211
212
  Classifier: Programming Language :: Python :: 3
212
213
  Classifier: License :: OSI Approved :: Apache Software License
213
214
  Classifier: Operating System :: OS Independent
@@ -215,7 +216,7 @@ Requires-Python: >=3.10
215
216
  Description-Content-Type: text/markdown
216
217
  License-File: LICENSE
217
218
 
218
- # pyTopoComlexity (v0.7.2)
219
+ # pyTopoComlexity (v0.7.4)
219
220
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.11239338.svg)](https://doi.org/10.5281/zenodo.11239338)
220
221
 
221
222
  `pytopocomplexity` is an open-source Python package designed to measure the topographic complexity (i.e., surface roughness) of land surfaces using digital elevation model (DEM) data. This package includes modules for three methods commonly used in the fields of geomorphology and oceanography for measuring topographic complexity, which are not fully available in Geographic Information System (GIS) software like QGIS.
@@ -267,7 +268,6 @@ The rugosity index is determined as the ratio of the real surface area to the ge
267
268
  ## Requirements
268
269
  For `pytopocomplexity` package
269
270
  * Python >= 3.10
270
- * `os`
271
271
  * `numpy`
272
272
  * `scipy`
273
273
  * `rasterio`
@@ -1,4 +1,5 @@
1
1
  LICENSE
2
+ README-pypi.md
2
3
  README.md
3
4
  pyproject.toml
4
5
  setup.py
@@ -0,0 +1,9 @@
1
+ numpy>=1.20
2
+ scipy>=1.6
3
+ rasterio>=1.2
4
+ dask>=2021.0
5
+ matplotlib>=3.3
6
+ tqdm>=4.0
7
+ numba>=0.53
8
+ gdal>=3.0
9
+ statsmodels>=0.12
@@ -1,11 +1,11 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- with open("README.md", "r", encoding="utf-8") as fh:
3
+ with open("README-pypi.md", "r", encoding="utf-8") as fh:
4
4
  long_description = fh.read()
5
5
 
6
6
  setup(
7
7
  name="pytopocomplexity",
8
- version="0.7.2",
8
+ version="0.7.4",
9
9
  author="Larry Syu-Heng Lai",
10
10
  author_email="larrysyuhenglai@gmail.com",
11
11
  description="A package for topographic complexity analysis",
@@ -20,13 +20,15 @@ setup(
20
20
  ],
21
21
  python_requires=">=3.10",
22
22
  install_requires=[
23
- "os",
24
- "numpy",
25
- "rasterio",
26
- "matplotlib",
27
- "dask",
28
- "numba",
29
- "scipy",
30
- "tqdm",
23
+ "numpy>=1.20",
24
+ "scipy>=1.6",
25
+ "rasterio>=1.2",
26
+ "dask>=2021.0",
27
+ "matplotlib>=3.3",
28
+ "tqdm>=4.0",
29
+ "numba>=0.53",
30
+ "gdal>=3.0",
31
+ "statsmodels>=0.12",
31
32
  ],
32
- )
33
+ license="Apache License 2.0",
34
+ )
@@ -1,185 +0,0 @@
1
- import os
2
- import numpy as np
3
- import rasterio
4
- import matplotlib.pyplot as plt
5
- from numba import jit
6
- from scipy.ndimage import generic_filter
7
- from tqdm.auto import tqdm
8
-
9
- class pyfracd:
10
- def __init__(self, npas=10):
11
- self.npas = npas
12
- self.fd2_out = None
13
- self.se2_out = None
14
- self.r2_out = None
15
- self.ft2mUS = 1200/3937 # US survey foot to meter conversion factor
16
- self.ft2mInt = 0.3048 # International foot to meter conversion factor
17
-
18
- @staticmethod
19
- @jit(nopython=True)
20
- def vario(z, npas, imo):
21
- fac = 1.0 if imo == 1 else np.sqrt(2.0)
22
- x = np.arange(1, npas + 1) * fac
23
- y = np.zeros(npas)
24
- npa = np.zeros(npas, dtype=np.int32)
25
-
26
- for i in range(npas):
27
- diffs = z[:-i-1] - z[i+1:]
28
- y[i] = np.sum(diffs**2) / (2 * len(diffs))
29
- npa[i] = len(diffs)
30
-
31
- mask = npa > 0
32
- y = y[mask]
33
- x = x[mask]
34
-
35
- if len(x) < 2:
36
- return np.nan, np.nan, np.nan
37
-
38
- x = np.log(x)
39
- y = np.log(y)
40
-
41
- n = len(x)
42
- sumx = np.sum(x)
43
- sumy = np.sum(y)
44
- sumx2 = np.sum(x**2)
45
- sumxy = np.sum(x * y)
46
-
47
- b = (n * sumxy - sumx * sumy) / (n * sumx2 - sumx**2)
48
- a = (sumy - b * sumx) / n
49
-
50
- fd2 = 3.0 - b / 2.0
51
- yfit = a + b * x
52
-
53
- varb = (np.sum((y - yfit)**2) / (n - 2)) / (sumx2 - sumx**2 / n)
54
- se2 = np.sqrt(varb) / 2.0
55
-
56
- sum1 = np.sum((yfit - y)**2)
57
- sum2 = np.sum((y - np.mean(y))**2)
58
- r2 = 1.0 - sum1 / sum2
59
-
60
- return fd2, se2, r2
61
-
62
- def analyze(self, input_dir, variograms=True):
63
- with rasterio.open(input_dir) as src:
64
- meta = src.meta.copy()
65
- Z = src.read(1)
66
- Zunit = src.crs.linear_units
67
- self.fd2_out = np.zeros((src.height, src.width), dtype=np.float32)
68
- self.se2_out = np.zeros((src.height, src.width), dtype=np.float32)
69
- self.r2_out = np.zeros((src.height, src.width), dtype=np.float32)
70
-
71
- if any(unit in Zunit.lower() for unit in ["metre", "meter", "meters"]):
72
- pass
73
- elif any(unit in Zunit.lower() for unit in ["foot", "feet", "ft"]):
74
- if any(unit in Zunit.lower() for unit in ["us", "united states"]):
75
- Z = Z * self.ft2mUS
76
- else:
77
- Z = Z * self.ft2mInt
78
- else:
79
- raise ValueError("The unit of elevation 'z' must be in feet or meters.")
80
-
81
- for j in tqdm(range(src.height), desc="Analyzing rows"):
82
- for i in range(src.width):
83
- results = []
84
-
85
- data = Z[j, max(0, i-20):min(i+21, src.width)]
86
- if len(data) > self.npas:
87
- results.append(self.vario(data, self.npas, 1))
88
-
89
- data = Z[max(0, j-20):min(j+21, src.height), i]
90
- if len(data) > self.npas:
91
- results.append(self.vario(data, self.npas, 1))
92
-
93
- for di, dj in [(1, 1), (1, -1)]:
94
- data = []
95
- for k in range(-14, 15):
96
- if 0 <= j+k*dj < src.height and 0 <= i+k*di < src.width:
97
- data.append(Z[j+k*dj, i+k*di])
98
- if len(data) > self.npas:
99
- results.append(self.vario(np.array(data), self.npas, 2))
100
-
101
- if results:
102
- fd2, se2, r2 = np.nanmean(results, axis=0)
103
- self.fd2_out[j, i] = np.clip(fd2, 2.0, 3.0)
104
- self.se2_out[j, i] = se2
105
- self.r2_out[j, i] = r2
106
-
107
- print(f"FD2 MIN : {np.nanmin(self.fd2_out)}")
108
- print(f"FD2 MAX : {np.nanmax(self.fd2_out)}")
109
- print(f"SE MIN : {np.nanmin(self.se2_out)}")
110
- print(f"SE MAX : {np.nanmax(self.se2_out)}")
111
- print(f"R2 MIN : {np.nanmin(self.r2_out)}")
112
- print(f"R2 MAX : {np.nanmax(self.r2_out)}")
113
-
114
- if variograms:
115
- self.plot_sample_variograms(Z)
116
-
117
- return Z, self.fd2_out, self.se2_out, self.r2_out, meta
118
-
119
- def plot_variogram(self, z, imo, title=None):
120
- z = np.array(z)
121
- fac = 1.0 if imo == 1 else np.sqrt(2.0)
122
- x = np.arange(1, self.npas + 1) * fac
123
- y = np.zeros(self.npas)
124
- npa = np.zeros(self.npas, dtype=np.int32)
125
-
126
- for i in range(self.npas):
127
- diffs = z[:-i-1] - z[i+1:]
128
- y[i] = np.sum(diffs**2) / (2 * len(diffs))
129
- npa[i] = len(diffs)
130
-
131
- mask = npa > 0
132
- y = y[mask]
133
- x = x[mask]
134
-
135
- if len(x) < 2:
136
- print("Not enough data points for variogram plot")
137
- return
138
-
139
- plt.figure(figsize=(5, 3))
140
- plt.scatter(np.log(x), np.log(y), color='blue', label='Data')
141
-
142
- coef = np.polyfit(np.log(x), np.log(y), 1)
143
- poly1d_fn = np.poly1d(coef)
144
- plt.plot(np.log(x), poly1d_fn(np.log(x)), '--r', label='Fitted Line')
145
-
146
- plt.xlabel('Log(Lag Distance)')
147
- plt.ylabel('Log(Semivariance)')
148
- plt.title(title or 'Variogram Plot')
149
- plt.legend()
150
- plt.grid(True)
151
- plt.show()
152
-
153
- slope = coef[0]
154
- fd2 = 3.0 - slope / 2.0
155
- print(f"Estimated Fractal Dimension: {fd2}")
156
-
157
- def plot_sample_variograms(self, Z):
158
- middle_row = Z.shape[0] // 2
159
- horizontal_slice = Z[middle_row, max(0, Z.shape[1]//2-20):min(Z.shape[1]//2+21, Z.shape[1])]
160
- self.plot_variogram(horizontal_slice, imo=1, title="Horizontal Variogram (Middle Row)")
161
-
162
- middle_col = Z.shape[1] // 2
163
- vertical_slice = Z[max(0, Z.shape[0]//2-20):min(Z.shape[0]//2+21, Z.shape[0]), middle_col]
164
- self.plot_variogram(vertical_slice, imo=1, title="Vertical Variogram (Middle Column)")
165
-
166
- diagonal_slice = [Z[i, i] for i in range(max(0, Z.shape[0]//2-14), min(Z.shape[0]//2+15, min(Z.shape)))]
167
- self.plot_variogram(diagonal_slice, imo=2, title="Diagonal Variogram")
168
-
169
- def export_results(self, fd2_output_dir, se2_output_dir, r2_output_dir, meta):
170
- if meta is None:
171
- raise ValueError("Metadata is missing. Cannot export the result.")
172
-
173
- meta.update(dtype=rasterio.float32, count=1, compress='deflate', bigtiff='IF_SAFER')
174
-
175
- with rasterio.open(fd2_output_dir, 'w', **meta) as dst:
176
- dst.write(self.fd2_out.astype(rasterio.float32), 1)
177
- print(f"Processed result saved to {os.path.basename(fd2_output_dir)}")
178
-
179
- with rasterio.open(se2_output_dir, 'w', **meta) as dst:
180
- dst.write(self.se2_out.astype(rasterio.float32), 1)
181
- print(f"Processed result saved to {os.path.basename(se2_output_dir)}")
182
-
183
- with rasterio.open(r2_output_dir, 'w', **meta) as dst:
184
- dst.write(self.r2_out.astype(rasterio.float32), 1)
185
- print(f"Processed result saved to {os.path.basename(r2_output_dir)}")
@@ -1,8 +0,0 @@
1
- os
2
- numpy
3
- rasterio
4
- matplotlib
5
- dask
6
- numba
7
- scipy
8
- tqdm