sprawlmetrics 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.
Files changed (40) hide show
  1. sprawlmetrics-0.1.0/CHANGELOG.md +24 -0
  2. sprawlmetrics-0.1.0/CITATION.cff +21 -0
  3. sprawlmetrics-0.1.0/LICENSE +21 -0
  4. sprawlmetrics-0.1.0/MANIFEST.in +9 -0
  5. sprawlmetrics-0.1.0/PKG-INFO +253 -0
  6. sprawlmetrics-0.1.0/README.md +214 -0
  7. sprawlmetrics-0.1.0/examples/quickstart.py +70 -0
  8. sprawlmetrics-0.1.0/pyproject.toml +120 -0
  9. sprawlmetrics-0.1.0/setup.cfg +4 -0
  10. sprawlmetrics-0.1.0/src/sprawlmetrics/__init__.py +107 -0
  11. sprawlmetrics-0.1.0/src/sprawlmetrics/__main__.py +7 -0
  12. sprawlmetrics-0.1.0/src/sprawlmetrics/_common.py +215 -0
  13. sprawlmetrics-0.1.0/src/sprawlmetrics/_version.py +1 -0
  14. sprawlmetrics-0.1.0/src/sprawlmetrics/analysis.py +197 -0
  15. sprawlmetrics-0.1.0/src/sprawlmetrics/cli.py +211 -0
  16. sprawlmetrics-0.1.0/src/sprawlmetrics/compactness.py +143 -0
  17. sprawlmetrics-0.1.0/src/sprawlmetrics/entropy.py +118 -0
  18. sprawlmetrics-0.1.0/src/sprawlmetrics/gradient.py +209 -0
  19. sprawlmetrics-0.1.0/src/sprawlmetrics/growth.py +107 -0
  20. sprawlmetrics-0.1.0/src/sprawlmetrics/io.py +146 -0
  21. sprawlmetrics-0.1.0/src/sprawlmetrics/lei.py +254 -0
  22. sprawlmetrics-0.1.0/src/sprawlmetrics/patches.py +236 -0
  23. sprawlmetrics-0.1.0/src/sprawlmetrics/py.typed +0 -0
  24. sprawlmetrics-0.1.0/src/sprawlmetrics/sdg.py +94 -0
  25. sprawlmetrics-0.1.0/src/sprawlmetrics/zones.py +186 -0
  26. sprawlmetrics-0.1.0/src/sprawlmetrics.egg-info/PKG-INFO +253 -0
  27. sprawlmetrics-0.1.0/src/sprawlmetrics.egg-info/SOURCES.txt +38 -0
  28. sprawlmetrics-0.1.0/src/sprawlmetrics.egg-info/dependency_links.txt +1 -0
  29. sprawlmetrics-0.1.0/src/sprawlmetrics.egg-info/entry_points.txt +2 -0
  30. sprawlmetrics-0.1.0/src/sprawlmetrics.egg-info/requires.txt +13 -0
  31. sprawlmetrics-0.1.0/src/sprawlmetrics.egg-info/top_level.txt +1 -0
  32. sprawlmetrics-0.1.0/tests/conftest.py +47 -0
  33. sprawlmetrics-0.1.0/tests/test_compactness.py +61 -0
  34. sprawlmetrics-0.1.0/tests/test_entropy_gradient.py +143 -0
  35. sprawlmetrics-0.1.0/tests/test_growth_sdg.py +86 -0
  36. sprawlmetrics-0.1.0/tests/test_io_cli.py +141 -0
  37. sprawlmetrics-0.1.0/tests/test_labels.py +89 -0
  38. sprawlmetrics-0.1.0/tests/test_lei.py +115 -0
  39. sprawlmetrics-0.1.0/tests/test_patches.py +103 -0
  40. sprawlmetrics-0.1.0/tests/test_zones_analysis.py +104 -0
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - Growth metrics: built-up area and share, total growth rate, annual growth rate (linear,
10
+ compound, exponential), annual expansion area, urban expansion intensity index (UEII).
11
+ - Landscape Expansion Index (Liu et al. 2010) per new patch with raster buffers (square or
12
+ disc, any width), infilling / edge-expansion / outlying classification, MEI, AWMEI,
13
+ growth-type raster and per-patch records.
14
+ - FRAGSTATS-style class metrics: NP, total area, mean patch area, LPI, total edge, ED, PD,
15
+ mean and area-weighted shape index, patch compactness; per-patch table.
16
+ - Footprint compactness: normalised moment of inertia, exchange index, proximity index.
17
+ - Shannon entropy across zones (Yeh & Li 2001), ring and sector zone builders.
18
+ - Concentric-ring density profile with Clark (1951) negative exponential fit.
19
+ - SDG 11.3.1 land consumption rate, population growth rate and LCRPGR.
20
+ - Per-zone summaries with LEI growth-type areas; CSV writer.
21
+ - `compare()` one-call report with JSON-safe `to_dict()`.
22
+ - Pure-NumPy run-length connected-component labelling (SciPy used when installed).
23
+ - Raster I/O: `.npy` with NumPy, GeoTIFF via the optional `raster` extra (rasterio).
24
+ - Command line: `sprawlmetrics compare | patches | gradient | lcrpgr`.
@@ -0,0 +1,21 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "geospacephilo@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "sprawlmetrics"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://gitlab.com/geophilo1/sprawlmetrics_sdk"
13
+ url: "https://pypi.org/project/sprawlmetrics/"
14
+ abstract: "Urban expansion and sprawl metrics from multi-date binary built-up rasters: growth rates and expansion intensity, the Landscape Expansion Index (infilling, edge-expansion, outlying), FRAGSTATS-style patch metrics, footprint compactness, Shannon entropy across zones, concentric-ring density gradients with Clark's negative exponential fit, the SDG 11.3.1 land consumption to population growth ratio and per-zone summaries."
15
+ keywords:
16
+ - urban expansion
17
+ - urban sprawl
18
+ - landscape expansion index
19
+ - landscape metrics
20
+ - SDG 11.3.1
21
+ - gis
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
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.
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/sprawlmetrics/py.typed
7
+ recursive-include src/sprawlmetrics *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: sprawlmetrics
3
+ Version: 0.1.0
4
+ Summary: Urban expansion and sprawl metrics from multi-date binary built-up rasters: growth rates, Landscape Expansion Index, patch metrics, compactness, entropy, density gradients and SDG 11.3.1.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/geophilo1/sprawlmetrics_sdk
8
+ Project-URL: Repository, https://gitlab.com/geophilo1/sprawlmetrics_sdk
9
+ Project-URL: Issues, https://gitlab.com/geophilo1/sprawlmetrics_sdk/-/work_items
10
+ Project-URL: Changelog, https://gitlab.com/geophilo1/sprawlmetrics_sdk/-/blob/main/CHANGELOG.md
11
+ Keywords: urban-expansion,urban-sprawl,landscape-expansion-index,lei,landscape-metrics,fragstats,shannon-entropy,density-gradient,compactness,built-up,sdg-11,remote-sensing,urban-planning,gis
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: Intended Audience :: Developers
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.21
29
+ Provides-Extra: fast
30
+ Requires-Dist: scipy>=1.7; extra == "fast"
31
+ Provides-Extra: raster
32
+ Requires-Dist: rasterio>=1.2; extra == "raster"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
36
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
37
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # sprawlmetrics
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/sprawlmetrics.svg)](https://pypi.org/project/sprawlmetrics/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/sprawlmetrics.svg)](https://pypi.org/project/sprawlmetrics/)
44
+ [![pipeline](https://gitlab.com/geophilo1/sprawlmetrics_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/sprawlmetrics_sdk/-/pipelines)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.com/geophilo1/sprawlmetrics_sdk/-/blob/main/LICENSE)
46
+
47
+ **Urban expansion and sprawl metrics from multi-date binary built-up rasters.**
48
+
49
+ `sprawlmetrics` takes two (or more) built-up / non-built-up rasters of the same grid, for
50
+ example classified Landsat or Sentinel-2 scenes or GHSL built-up layers, and computes the
51
+ standard indicators used in urban expansion studies:
52
+
53
+ - **Growth**: built-up area, total and annual growth rate (linear, compound, exponential),
54
+ annual expansion area, and the **urban expansion intensity index** (UEII).
55
+ - **Landscape Expansion Index** (LEI; Liu et al. 2010): sorts every new patch into
56
+ *infilling*, *edge-expansion* or *outlying* growth, and reports MEI and AWMEI.
57
+ - **Patch metrics** (FRAGSTATS definitions): number of patches, mean patch size, largest
58
+ patch index, edge density, patch density, mean and area-weighted shape index, and patch
59
+ compactness.
60
+ - **Footprint compactness**: normalised moment of inertia, exchange index and proximity
61
+ index.
62
+ - **Shannon entropy** of built-up land across rings, sectors or any zones
63
+ (Yeh & Li 2001).
64
+ - **Density gradient**: built-up density in concentric rings around a centre, with
65
+ Clark's (1951) negative exponential fit `D(r) = D0 exp(-b r)`.
66
+ - **SDG 11.3.1**: land consumption rate, population growth rate and their ratio (LCRPGR).
67
+ - **Per-zone summaries** (districts, rings, grid cells), written as CSV.
68
+
69
+ It needs only NumPy. SciPy (optional) speeds up patch labelling, and rasterio (optional)
70
+ reads and writes GeoTIFFs. A command-line tool is included.
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ pip install sprawlmetrics # NumPy only
76
+ pip install "sprawlmetrics[fast]" # + SciPy (faster connected-component labelling)
77
+ pip install "sprawlmetrics[raster]" # + rasterio (GeoTIFF and other GDAL rasters)
78
+ ```
79
+
80
+ Requires Python 3.9 or newer.
81
+
82
+ ## Quickstart
83
+
84
+ ```python
85
+ import numpy as np
86
+ import sprawlmetrics as sm
87
+
88
+ # 0/1 rasters on the same 30 m grid (here: a synthetic city)
89
+ t1 = np.zeros((200, 200), dtype=np.uint8)
90
+ t1[80:120, 80:120] = 1
91
+ t2 = t1.copy()
92
+ t2[75:125, 75:125] = 1 # edge expansion around the core
93
+ t2[20:24, 150:154] = 1 # an outlying settlement
94
+
95
+ report = sm.compare(t1, t2, years=10, cell_size=30, ring_width=300,
96
+ population=(250_000, 310_000))
97
+ print(report.growth["annual_growth_rate_compound_pct"])
98
+ print(report.lei.summary()) # patch counts and areas by growth type, MEI, AWMEI
99
+ print(report.gradient_t2.fit) # Clark fit: D0, b, R²
100
+ print(report.sdg_11_3_1.ratio) # LCRPGR
101
+
102
+ import json
103
+ json.dumps(report.to_dict()) # JSON-safe (NaN -> null)
104
+ ```
105
+
106
+ Single metrics are plain functions:
107
+
108
+ ```python
109
+ lei = sm.landscape_expansion_index(t1, t2, buffer_cells=1, cell_size=30)
110
+ lei.records() # one dict per new patch: area, A_o, A_v, LEI, type
111
+ lei.type_raster # 0 none, 1 infilling, 2 edge-expansion, 3 outlying
112
+
113
+ sm.patch_metrics(t2, cell_size=30).to_dict()
114
+ sm.compactness(t2, cell_size=30)
115
+ sm.zonal_entropy(t2, sm.ring_zones(t2.shape, (100, 100), 300, cell_size=30))
116
+ sm.density_gradient(t2, 300, centre=(100, 100), cell_size=30)
117
+ sm.expansion_intensity(area_t1=144.0, area_t2=225.0, total_area=3600.0, years=10)
118
+ sm.lcrpgr(area_t1=144.0, area_t2=225.0, pop_t1=250_000, pop_t2=310_000, years=10)
119
+ ```
120
+
121
+ Reading rasters (`.npy` always works; GeoTIFF needs the `raster` extra):
122
+
123
+ ```python
124
+ built, valid, info = sm.load_built("built_2000.tif", built_values=[1]) # class code
125
+ built, valid, info = sm.load_built("ghsl_2020.tif", threshold=0.2) # share >= 20 %
126
+ info.cell_size # (xres, yres) from the GeoTIFF transform
127
+ ```
128
+
129
+ ## Command line
130
+
131
+ ```bash
132
+ sprawlmetrics compare built_2000.tif built_2020.tif --years 20 \
133
+ --built-values 1 --ring-width 1000 --centre 512,498 \
134
+ --zones districts.tif --population 1200000,1650000 \
135
+ --lei-raster lei_types.tif --lei-csv lei.csv --zones-csv zones.csv -o report.json
136
+
137
+ sprawlmetrics patches built_2020.tif --threshold 0.2
138
+ sprawlmetrics gradient built_2020.tif --ring-width 500 --csv rings.csv
139
+ sprawlmetrics lcrpgr --area1 144 --area2 225 --pop1 250000 --pop2 310000 --years 10
140
+ ```
141
+
142
+ All commands print JSON (or write it with `-o`). `.npy` inputs work without rasterio; set
143
+ `--cell-size` for them.
144
+
145
+ ## API overview
146
+
147
+ | Area | Functions / classes |
148
+ | --- | --- |
149
+ | One call | `compare(t1, t2, years, ...) -> SprawlReport` (`.to_dict()`) |
150
+ | Growth | `built_area`, `built_share`, `growth_rate`, `annual_growth_rate(method=...)`, `annual_expansion_area`, `expansion_intensity` |
151
+ | LEI | `landscape_expansion_index -> LEIResult` (`mei`, `awmei`, `summary()`, `records()`, `type_raster`), `classify_lei` |
152
+ | Patches | `patch_metrics -> PatchMetrics`, `patch_table`, `shape_index`, `min_perimeter_cells`, `label` |
153
+ | Compactness | `compactness`, `normalized_moment_of_inertia`, `exchange_index`, `proximity_index`, `built_centroid` |
154
+ | Entropy | `shannon_entropy`, `zonal_entropy -> EntropyResult` |
155
+ | Gradient | `density_gradient -> GradientResult`, `fit_negative_exponential -> ExponentialFit` |
156
+ | Zones | `ring_zones`, `sector_zones`, `distance_grid`, `zonal_summary`, `write_csv` |
157
+ | SDG 11.3.1 | `land_consumption_rate`, `population_growth_rate`, `lcrpgr -> LCRPGRResult` |
158
+ | I/O | `read_raster`, `load_built`, `binarize`, `write_raster`, `RasterInfo` |
159
+
160
+ Conventions: rasters are 2-D arrays, non-zero = built-up. `cell_size` is a cell side or an
161
+ `(xres, yres)` pair in map units, so areas come out in map units squared. Centres are
162
+ `(row, col)` indices (fractional values allowed). An optional `valid` mask marks the study
163
+ area; cells outside it count neither as land nor as built-up.
164
+
165
+ ## Methods and sources
166
+
167
+ | Metric | Definition | Source |
168
+ | --- | --- | --- |
169
+ | Growth rate | `100 (A2 - A1) / A1` | standard |
170
+ | Annual growth rate | linear `100 (A2-A1)/(A1 t)`; compound `100((A2/A1)^(1/t) - 1)`; exponential `100 ln(A2/A1)/t` | standard |
171
+ | UEII | `100 (A2 - A1) / (TA t)`, `TA` = land area of the unit | Liu, Zhan & Deng (2005) |
172
+ | LEI | `100 A_o / (A_o + A_v)` in a buffer ring around each new patch; infilling `> 50`, edge-expansion `(0, 50]`, outlying `= 0`; `MEI = mean(LEI)`, `AWMEI = sum(LEI_i a_i)/A` | Liu et al. (2010) |
173
+ | Patch metrics | NP, CA, AREA_MN, LPI `= 100 max(a)/A`, ED `= E/A * 10 000`, PD `= NP/A * 10^6`, SHAPE `= p / min p` (raster form), SHAPE_MN, SHAPE_AM | McGarigal et al. (FRAGSTATS) |
174
+ | Patch compactness | `2 sqrt(pi a) / p` | isoperimetric ratio |
175
+ | NMI | `A^2 / (2 pi I)`, `I` = polar moment of inertia about the centroid | Li et al. (2014) |
176
+ | Exchange, proximity | share of the footprint inside the equal-area circle; `(2r/3) / mean distance` | Angel, Parent & Civco (2010) |
177
+ | Entropy | `H = -sum p_i ln p_i`, `p_i = x_i / sum x`, `x_i` = built-up density of zone `i`; relative `H / ln n` | Yeh & Li (2001) |
178
+ | Density gradient | OLS of `ln D` on ring mid-distance: `D(r) = D0 exp(-b r)` | Clark (1951) |
179
+ | SDG 11.3.1 | `LCR = ln(Urb2/Urb1)/y`, `PGR = ln(Pop2/Pop1)/y`, ratio `LCR/PGR` | UN-Habitat SDG 11.3.1 metadata |
180
+
181
+ Raster details:
182
+
183
+ - Patches use the 8-cell rule by default (FRAGSTATS default); `connectivity=4` is
184
+ available.
185
+ - **LEI buffer** = morphological dilation of the patch by `buffer_cells` cells (square or
186
+ disc) minus the patch itself; cells outside the raster or the study area are left out.
187
+ `A_v` counts every buffer cell not built at t1, including cells of other new patches.
188
+ - **Edge**: the class edge for ED counts cell sides between built-up and valid non-built
189
+ cells; `count_boundary=True` also counts sides on the raster border or against invalid
190
+ cells. A patch's own perimeter (for SHAPE and compactness) always includes them.
191
+ - **ED** is in map units of edge per hectare when cells are in metres (FRAGSTATS m/ha);
192
+ **PD** is patches per 100 ha.
193
+ - **NMI** treats each cell as a uniform square, adding its own polar moment
194
+ `c (xres² + yres²) / 12`, so a square footprint scores exactly `3/π ≈ 0.955` and a disc
195
+ approaches 1.
196
+ - **Ring k** holds cells whose centre lies at `(k-1) w <= d < k w`.
197
+
198
+ ### Relation to `sdg11`
199
+
200
+ The companion package [`sdg11`](https://pypi.org/project/sdg11/) implements the full SDG
201
+ 11.3.1 reporting workflow (degree-of-urbanisation delineation, interpretation and report
202
+ bundles). `sprawlmetrics.lcrpgr` uses the same formula for quick use inside sprawl
203
+ analyses; use `sdg11` for official-style reporting.
204
+
205
+ ## Limitations
206
+
207
+ - The LEI buffer is raster-based; Liu et al. (2010) describe a vector buffer. With one-cell
208
+ buffers the results match the usual raster implementations, but values for very small
209
+ patches depend on the buffer shape and width.
210
+ - No reprojection or resampling: both dates must already be on the same grid.
211
+ - Entropy and gradient results depend on the chosen centre and ring width; report them.
212
+ - The negative exponential fit is OLS on log density and drops rings with zero density;
213
+ it is not a maximum-likelihood fit.
214
+ - UEII class thresholds (e.g. "high-speed", "slow") vary between studies and are not built
215
+ in.
216
+ - Not an official UN product; check SDG values against the current UN-Habitat metadata.
217
+
218
+ ## References
219
+
220
+ - Angel, S., Parent, J. & Civco, D. L. (2010). Ten compactness properties of circles:
221
+ measuring shape in geography. *The Canadian Geographer* 54(4), 441-461.
222
+ - Clark, C. (1951). Urban population densities. *Journal of the Royal Statistical Society,
223
+ Series A* 114(4), 490-496.
224
+ - Li, W., Chen, T., Wentz, E. A. & Fan, C. (2014). NMMI: A mass compactness measure for
225
+ spatial pattern analysis of areal features. *Annals of the AAG* 104(6), 1116-1133.
226
+ - Liu, J., Zhan, J. & Deng, X. (2005). Spatio-temporal patterns and driving forces of urban
227
+ land expansion in China during the economic reform era. *Ambio* 34(6), 450-455.
228
+ - Liu, X., Li, X., Chen, Y., Tan, Z., Li, S. & Ai, B. (2010). A new landscape index for
229
+ quantifying urban expansion using multi-temporal remotely sensed data. *Landscape
230
+ Ecology* 25, 671-682. https://doi.org/10.1007/s10980-010-9454-5
231
+ - McGarigal, K., Cushman, S. A. & Ene, E. FRAGSTATS v4: Spatial Pattern Analysis Program
232
+ for Categorical Maps. University of Massachusetts, Amherst.
233
+ - UN-Habitat. SDG indicator metadata 11.3.1: Ratio of land consumption rate to population
234
+ growth rate.
235
+ - Yeh, A. G.-O. & Li, X. (2001). Measurement and monitoring of urban sprawl in a rapidly
236
+ growing region using entropy. *Photogrammetric Engineering & Remote Sensing* 67(1),
237
+ 83-90.
238
+
239
+ ## Academic citation
240
+
241
+ ```bibtex
242
+ @software{eminoglu_sprawlmetrics,
243
+ author = {Eminoğlu, Yusuf},
244
+ title = {sprawlmetrics},
245
+ year = {2026},
246
+ version = {0.1.0},
247
+ url = {https://gitlab.com/geophilo1/sprawlmetrics_sdk}
248
+ }
249
+ ```
250
+
251
+ ## License
252
+
253
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,214 @@
1
+ # sprawlmetrics
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/sprawlmetrics.svg)](https://pypi.org/project/sprawlmetrics/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/sprawlmetrics.svg)](https://pypi.org/project/sprawlmetrics/)
5
+ [![pipeline](https://gitlab.com/geophilo1/sprawlmetrics_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/sprawlmetrics_sdk/-/pipelines)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.com/geophilo1/sprawlmetrics_sdk/-/blob/main/LICENSE)
7
+
8
+ **Urban expansion and sprawl metrics from multi-date binary built-up rasters.**
9
+
10
+ `sprawlmetrics` takes two (or more) built-up / non-built-up rasters of the same grid, for
11
+ example classified Landsat or Sentinel-2 scenes or GHSL built-up layers, and computes the
12
+ standard indicators used in urban expansion studies:
13
+
14
+ - **Growth**: built-up area, total and annual growth rate (linear, compound, exponential),
15
+ annual expansion area, and the **urban expansion intensity index** (UEII).
16
+ - **Landscape Expansion Index** (LEI; Liu et al. 2010): sorts every new patch into
17
+ *infilling*, *edge-expansion* or *outlying* growth, and reports MEI and AWMEI.
18
+ - **Patch metrics** (FRAGSTATS definitions): number of patches, mean patch size, largest
19
+ patch index, edge density, patch density, mean and area-weighted shape index, and patch
20
+ compactness.
21
+ - **Footprint compactness**: normalised moment of inertia, exchange index and proximity
22
+ index.
23
+ - **Shannon entropy** of built-up land across rings, sectors or any zones
24
+ (Yeh & Li 2001).
25
+ - **Density gradient**: built-up density in concentric rings around a centre, with
26
+ Clark's (1951) negative exponential fit `D(r) = D0 exp(-b r)`.
27
+ - **SDG 11.3.1**: land consumption rate, population growth rate and their ratio (LCRPGR).
28
+ - **Per-zone summaries** (districts, rings, grid cells), written as CSV.
29
+
30
+ It needs only NumPy. SciPy (optional) speeds up patch labelling, and rasterio (optional)
31
+ reads and writes GeoTIFFs. A command-line tool is included.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install sprawlmetrics # NumPy only
37
+ pip install "sprawlmetrics[fast]" # + SciPy (faster connected-component labelling)
38
+ pip install "sprawlmetrics[raster]" # + rasterio (GeoTIFF and other GDAL rasters)
39
+ ```
40
+
41
+ Requires Python 3.9 or newer.
42
+
43
+ ## Quickstart
44
+
45
+ ```python
46
+ import numpy as np
47
+ import sprawlmetrics as sm
48
+
49
+ # 0/1 rasters on the same 30 m grid (here: a synthetic city)
50
+ t1 = np.zeros((200, 200), dtype=np.uint8)
51
+ t1[80:120, 80:120] = 1
52
+ t2 = t1.copy()
53
+ t2[75:125, 75:125] = 1 # edge expansion around the core
54
+ t2[20:24, 150:154] = 1 # an outlying settlement
55
+
56
+ report = sm.compare(t1, t2, years=10, cell_size=30, ring_width=300,
57
+ population=(250_000, 310_000))
58
+ print(report.growth["annual_growth_rate_compound_pct"])
59
+ print(report.lei.summary()) # patch counts and areas by growth type, MEI, AWMEI
60
+ print(report.gradient_t2.fit) # Clark fit: D0, b, R²
61
+ print(report.sdg_11_3_1.ratio) # LCRPGR
62
+
63
+ import json
64
+ json.dumps(report.to_dict()) # JSON-safe (NaN -> null)
65
+ ```
66
+
67
+ Single metrics are plain functions:
68
+
69
+ ```python
70
+ lei = sm.landscape_expansion_index(t1, t2, buffer_cells=1, cell_size=30)
71
+ lei.records() # one dict per new patch: area, A_o, A_v, LEI, type
72
+ lei.type_raster # 0 none, 1 infilling, 2 edge-expansion, 3 outlying
73
+
74
+ sm.patch_metrics(t2, cell_size=30).to_dict()
75
+ sm.compactness(t2, cell_size=30)
76
+ sm.zonal_entropy(t2, sm.ring_zones(t2.shape, (100, 100), 300, cell_size=30))
77
+ sm.density_gradient(t2, 300, centre=(100, 100), cell_size=30)
78
+ sm.expansion_intensity(area_t1=144.0, area_t2=225.0, total_area=3600.0, years=10)
79
+ sm.lcrpgr(area_t1=144.0, area_t2=225.0, pop_t1=250_000, pop_t2=310_000, years=10)
80
+ ```
81
+
82
+ Reading rasters (`.npy` always works; GeoTIFF needs the `raster` extra):
83
+
84
+ ```python
85
+ built, valid, info = sm.load_built("built_2000.tif", built_values=[1]) # class code
86
+ built, valid, info = sm.load_built("ghsl_2020.tif", threshold=0.2) # share >= 20 %
87
+ info.cell_size # (xres, yres) from the GeoTIFF transform
88
+ ```
89
+
90
+ ## Command line
91
+
92
+ ```bash
93
+ sprawlmetrics compare built_2000.tif built_2020.tif --years 20 \
94
+ --built-values 1 --ring-width 1000 --centre 512,498 \
95
+ --zones districts.tif --population 1200000,1650000 \
96
+ --lei-raster lei_types.tif --lei-csv lei.csv --zones-csv zones.csv -o report.json
97
+
98
+ sprawlmetrics patches built_2020.tif --threshold 0.2
99
+ sprawlmetrics gradient built_2020.tif --ring-width 500 --csv rings.csv
100
+ sprawlmetrics lcrpgr --area1 144 --area2 225 --pop1 250000 --pop2 310000 --years 10
101
+ ```
102
+
103
+ All commands print JSON (or write it with `-o`). `.npy` inputs work without rasterio; set
104
+ `--cell-size` for them.
105
+
106
+ ## API overview
107
+
108
+ | Area | Functions / classes |
109
+ | --- | --- |
110
+ | One call | `compare(t1, t2, years, ...) -> SprawlReport` (`.to_dict()`) |
111
+ | Growth | `built_area`, `built_share`, `growth_rate`, `annual_growth_rate(method=...)`, `annual_expansion_area`, `expansion_intensity` |
112
+ | LEI | `landscape_expansion_index -> LEIResult` (`mei`, `awmei`, `summary()`, `records()`, `type_raster`), `classify_lei` |
113
+ | Patches | `patch_metrics -> PatchMetrics`, `patch_table`, `shape_index`, `min_perimeter_cells`, `label` |
114
+ | Compactness | `compactness`, `normalized_moment_of_inertia`, `exchange_index`, `proximity_index`, `built_centroid` |
115
+ | Entropy | `shannon_entropy`, `zonal_entropy -> EntropyResult` |
116
+ | Gradient | `density_gradient -> GradientResult`, `fit_negative_exponential -> ExponentialFit` |
117
+ | Zones | `ring_zones`, `sector_zones`, `distance_grid`, `zonal_summary`, `write_csv` |
118
+ | SDG 11.3.1 | `land_consumption_rate`, `population_growth_rate`, `lcrpgr -> LCRPGRResult` |
119
+ | I/O | `read_raster`, `load_built`, `binarize`, `write_raster`, `RasterInfo` |
120
+
121
+ Conventions: rasters are 2-D arrays, non-zero = built-up. `cell_size` is a cell side or an
122
+ `(xres, yres)` pair in map units, so areas come out in map units squared. Centres are
123
+ `(row, col)` indices (fractional values allowed). An optional `valid` mask marks the study
124
+ area; cells outside it count neither as land nor as built-up.
125
+
126
+ ## Methods and sources
127
+
128
+ | Metric | Definition | Source |
129
+ | --- | --- | --- |
130
+ | Growth rate | `100 (A2 - A1) / A1` | standard |
131
+ | Annual growth rate | linear `100 (A2-A1)/(A1 t)`; compound `100((A2/A1)^(1/t) - 1)`; exponential `100 ln(A2/A1)/t` | standard |
132
+ | UEII | `100 (A2 - A1) / (TA t)`, `TA` = land area of the unit | Liu, Zhan & Deng (2005) |
133
+ | LEI | `100 A_o / (A_o + A_v)` in a buffer ring around each new patch; infilling `> 50`, edge-expansion `(0, 50]`, outlying `= 0`; `MEI = mean(LEI)`, `AWMEI = sum(LEI_i a_i)/A` | Liu et al. (2010) |
134
+ | Patch metrics | NP, CA, AREA_MN, LPI `= 100 max(a)/A`, ED `= E/A * 10 000`, PD `= NP/A * 10^6`, SHAPE `= p / min p` (raster form), SHAPE_MN, SHAPE_AM | McGarigal et al. (FRAGSTATS) |
135
+ | Patch compactness | `2 sqrt(pi a) / p` | isoperimetric ratio |
136
+ | NMI | `A^2 / (2 pi I)`, `I` = polar moment of inertia about the centroid | Li et al. (2014) |
137
+ | Exchange, proximity | share of the footprint inside the equal-area circle; `(2r/3) / mean distance` | Angel, Parent & Civco (2010) |
138
+ | Entropy | `H = -sum p_i ln p_i`, `p_i = x_i / sum x`, `x_i` = built-up density of zone `i`; relative `H / ln n` | Yeh & Li (2001) |
139
+ | Density gradient | OLS of `ln D` on ring mid-distance: `D(r) = D0 exp(-b r)` | Clark (1951) |
140
+ | SDG 11.3.1 | `LCR = ln(Urb2/Urb1)/y`, `PGR = ln(Pop2/Pop1)/y`, ratio `LCR/PGR` | UN-Habitat SDG 11.3.1 metadata |
141
+
142
+ Raster details:
143
+
144
+ - Patches use the 8-cell rule by default (FRAGSTATS default); `connectivity=4` is
145
+ available.
146
+ - **LEI buffer** = morphological dilation of the patch by `buffer_cells` cells (square or
147
+ disc) minus the patch itself; cells outside the raster or the study area are left out.
148
+ `A_v` counts every buffer cell not built at t1, including cells of other new patches.
149
+ - **Edge**: the class edge for ED counts cell sides between built-up and valid non-built
150
+ cells; `count_boundary=True` also counts sides on the raster border or against invalid
151
+ cells. A patch's own perimeter (for SHAPE and compactness) always includes them.
152
+ - **ED** is in map units of edge per hectare when cells are in metres (FRAGSTATS m/ha);
153
+ **PD** is patches per 100 ha.
154
+ - **NMI** treats each cell as a uniform square, adding its own polar moment
155
+ `c (xres² + yres²) / 12`, so a square footprint scores exactly `3/π ≈ 0.955` and a disc
156
+ approaches 1.
157
+ - **Ring k** holds cells whose centre lies at `(k-1) w <= d < k w`.
158
+
159
+ ### Relation to `sdg11`
160
+
161
+ The companion package [`sdg11`](https://pypi.org/project/sdg11/) implements the full SDG
162
+ 11.3.1 reporting workflow (degree-of-urbanisation delineation, interpretation and report
163
+ bundles). `sprawlmetrics.lcrpgr` uses the same formula for quick use inside sprawl
164
+ analyses; use `sdg11` for official-style reporting.
165
+
166
+ ## Limitations
167
+
168
+ - The LEI buffer is raster-based; Liu et al. (2010) describe a vector buffer. With one-cell
169
+ buffers the results match the usual raster implementations, but values for very small
170
+ patches depend on the buffer shape and width.
171
+ - No reprojection or resampling: both dates must already be on the same grid.
172
+ - Entropy and gradient results depend on the chosen centre and ring width; report them.
173
+ - The negative exponential fit is OLS on log density and drops rings with zero density;
174
+ it is not a maximum-likelihood fit.
175
+ - UEII class thresholds (e.g. "high-speed", "slow") vary between studies and are not built
176
+ in.
177
+ - Not an official UN product; check SDG values against the current UN-Habitat metadata.
178
+
179
+ ## References
180
+
181
+ - Angel, S., Parent, J. & Civco, D. L. (2010). Ten compactness properties of circles:
182
+ measuring shape in geography. *The Canadian Geographer* 54(4), 441-461.
183
+ - Clark, C. (1951). Urban population densities. *Journal of the Royal Statistical Society,
184
+ Series A* 114(4), 490-496.
185
+ - Li, W., Chen, T., Wentz, E. A. & Fan, C. (2014). NMMI: A mass compactness measure for
186
+ spatial pattern analysis of areal features. *Annals of the AAG* 104(6), 1116-1133.
187
+ - Liu, J., Zhan, J. & Deng, X. (2005). Spatio-temporal patterns and driving forces of urban
188
+ land expansion in China during the economic reform era. *Ambio* 34(6), 450-455.
189
+ - Liu, X., Li, X., Chen, Y., Tan, Z., Li, S. & Ai, B. (2010). A new landscape index for
190
+ quantifying urban expansion using multi-temporal remotely sensed data. *Landscape
191
+ Ecology* 25, 671-682. https://doi.org/10.1007/s10980-010-9454-5
192
+ - McGarigal, K., Cushman, S. A. & Ene, E. FRAGSTATS v4: Spatial Pattern Analysis Program
193
+ for Categorical Maps. University of Massachusetts, Amherst.
194
+ - UN-Habitat. SDG indicator metadata 11.3.1: Ratio of land consumption rate to population
195
+ growth rate.
196
+ - Yeh, A. G.-O. & Li, X. (2001). Measurement and monitoring of urban sprawl in a rapidly
197
+ growing region using entropy. *Photogrammetric Engineering & Remote Sensing* 67(1),
198
+ 83-90.
199
+
200
+ ## Academic citation
201
+
202
+ ```bibtex
203
+ @software{eminoglu_sprawlmetrics,
204
+ author = {Eminoğlu, Yusuf},
205
+ title = {sprawlmetrics},
206
+ year = {2026},
207
+ version = {0.1.0},
208
+ url = {https://gitlab.com/geophilo1/sprawlmetrics_sdk}
209
+ }
210
+ ```
211
+
212
+ ## License
213
+
214
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,70 @@
1
+ """sprawlmetrics quickstart on a synthetic two-date city (no input files needed).
2
+
3
+ Run: python examples/quickstart.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ import numpy as np
11
+
12
+ import sprawlmetrics as sm
13
+
14
+
15
+ def synthetic_city() -> tuple:
16
+ """A compact core at t1; at t2 it has grown at its edge, filled a gap and leapfrogged."""
17
+ t1 = np.zeros((200, 200), dtype=np.uint8)
18
+ t1[80:120, 80:120] = 1
19
+ t1[95:105, 95:105] = 0 # an unbuilt gap in the core
20
+ t1[60:70, 140:150] = 1 # a satellite town
21
+ t2 = t1.copy()
22
+ t2[75:125, 75:125] = 1 # edge expansion and infilling of the gap
23
+ t2[20:24, 20:24] = 1 # outlying (leapfrog) development
24
+ t2[60:70, 150:156] = 1 # satellite grows at its edge
25
+ return t1, t2
26
+
27
+
28
+ def main() -> None:
29
+ t1, t2 = synthetic_city()
30
+ report = sm.compare(
31
+ t1,
32
+ t2,
33
+ years=10,
34
+ cell_size=30, # metres
35
+ centre=(100, 100),
36
+ ring_width=300,
37
+ population=(250_000, 310_000),
38
+ )
39
+ print(f"Built-up t1: {report.built_t1 / 10_000:.1f} ha, t2: {report.built_t2 / 10_000:.1f} ha")
40
+ print(f"Annual growth (compound): {report.growth['annual_growth_rate_compound_pct']:.2f} %/yr")
41
+ print(f"UEII: {report.growth['ueii']:.3f} % of the study area per year")
42
+
43
+ lei = report.lei.summary()
44
+ for key in ("infilling", "edge_expansion", "outlying"):
45
+ print(f" {key:15s} {lei[key + '_patches']:3d} patches {lei[key + '_area_pct']:5.1f} %")
46
+ print(f"MEI {lei['mei']:.1f} AWMEI {lei['awmei']:.1f}")
47
+
48
+ assert report.gradient_t1 is not None and report.gradient_t2 is not None
49
+ for name, grad in (("t1", report.gradient_t1), ("t2", report.gradient_t2)):
50
+ if grad.fit is not None:
51
+ print(f"Clark fit {name}: D0={grad.fit.d0:.2f} b={grad.fit.b * 1000:.3f}/km")
52
+ assert report.entropy_t1 is not None and report.entropy_t2 is not None
53
+ print(
54
+ f"Relative entropy: {report.entropy_t1.relative_entropy:.3f} -> "
55
+ f"{report.entropy_t2.relative_entropy:.3f}"
56
+ )
57
+ print(
58
+ f"NMI: {report.compactness_t1.normalized_moment_of_inertia:.3f} -> "
59
+ f"{report.compactness_t2.normalized_moment_of_inertia:.3f}"
60
+ )
61
+ assert report.sdg_11_3_1 is not None
62
+ print(f"SDG 11.3.1 LCRPGR: {report.sdg_11_3_1.ratio:.2f} ({report.sdg_11_3_1.interpretation})")
63
+
64
+ # Everything as JSON
65
+ text = json.dumps(report.to_dict(), indent=2)
66
+ print(f"JSON report: {len(text)} characters")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()