greenrule 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.
- greenrule-0.1.0/CHANGELOG.md +31 -0
- greenrule-0.1.0/CITATION.cff +23 -0
- greenrule-0.1.0/LICENSE +21 -0
- greenrule-0.1.0/MANIFEST.in +9 -0
- greenrule-0.1.0/PKG-INFO +230 -0
- greenrule-0.1.0/README.md +195 -0
- greenrule-0.1.0/examples/quickstart.py +74 -0
- greenrule-0.1.0/pyproject.toml +109 -0
- greenrule-0.1.0/setup.cfg +4 -0
- greenrule-0.1.0/src/greenrule/__init__.py +106 -0
- greenrule-0.1.0/src/greenrule/__main__.py +7 -0
- greenrule-0.1.0/src/greenrule/_common.py +79 -0
- greenrule-0.1.0/src/greenrule/_version.py +1 -0
- greenrule-0.1.0/src/greenrule/analysis.py +176 -0
- greenrule-0.1.0/src/greenrule/cli.py +318 -0
- greenrule-0.1.0/src/greenrule/equity.py +148 -0
- greenrule-0.1.0/src/greenrule/exposure.py +90 -0
- greenrule-0.1.0/src/greenrule/geometry.py +245 -0
- greenrule-0.1.0/src/greenrule/green.py +188 -0
- greenrule-0.1.0/src/greenrule/io.py +196 -0
- greenrule-0.1.0/src/greenrule/network.py +179 -0
- greenrule-0.1.0/src/greenrule/provision.py +113 -0
- greenrule-0.1.0/src/greenrule/py.typed +0 -0
- greenrule-0.1.0/src/greenrule/raster.py +149 -0
- greenrule-0.1.0/src/greenrule/rule333.py +301 -0
- greenrule-0.1.0/src/greenrule/who.py +142 -0
- greenrule-0.1.0/src/greenrule.egg-info/PKG-INFO +230 -0
- greenrule-0.1.0/src/greenrule.egg-info/SOURCES.txt +34 -0
- greenrule-0.1.0/src/greenrule.egg-info/dependency_links.txt +1 -0
- greenrule-0.1.0/src/greenrule.egg-info/entry_points.txt +2 -0
- greenrule-0.1.0/src/greenrule.egg-info/requires.txt +7 -0
- greenrule-0.1.0/src/greenrule.egg-info/top_level.txt +1 -0
- greenrule-0.1.0/tests/conftest.py +31 -0
- greenrule-0.1.0/tests/test_core.py +256 -0
- greenrule-0.1.0/tests/test_io_cli.py +328 -0
- greenrule-0.1.0/tests/test_rules.py +215 -0
|
@@ -0,0 +1,31 @@
|
|
|
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-15
|
|
8
|
+
### Added
|
|
9
|
+
- 3-30-300 rule (Konijnendijk 2023) per home: visible-tree counts from user visibility
|
|
10
|
+
inputs (counts or home-tree pairs; a labelled radius proxy), neighbourhood canopy share
|
|
11
|
+
from rasters, canopy polygons or precomputed values (zones or circular buffers), and
|
|
12
|
+
Euclidean or network distance to the nearest green space with an optional minimum size.
|
|
13
|
+
- WHO Regional Office for Europe (2016) Urban Green Space Indicator: 300 m linear distance
|
|
14
|
+
to the green-space boundary, 0.5 ha core and 1 ha additional minimum sizes.
|
|
15
|
+
- Green space per capita and green share by zone (centroid assignment or grid sampling).
|
|
16
|
+
- NDVI computation and circular-buffer NDVI exposure (mean, median, SD, green fraction).
|
|
17
|
+
- Equity: weighted Gini coefficient, Lorenz curve, population share meeting each
|
|
18
|
+
criterion overall and by zone, Gini of per-zone shares.
|
|
19
|
+
- Built-in street graph from GeoJSON lines with multi-source Dijkstra, point snapping and a
|
|
20
|
+
Euclidean fallback.
|
|
21
|
+
- One-call `assess()`; GeoJSON/CSV input and output.
|
|
22
|
+
- Command line: `greenrule who | rule | exposure | provision | equity`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Raster buffer and polygon statistics no longer copy the whole raster for every home or
|
|
26
|
+
zone (only the window around it is read), which made large rasters very slow.
|
|
27
|
+
- `trees_within_radius` sizes its blocks by the number of trees, so large tree inventories
|
|
28
|
+
no longer allocate multi-gigabyte distance arrays.
|
|
29
|
+
- 3-30-300 description aligned with Konijnendijk (2023): well-established trees, and the
|
|
30
|
+
article's 1 ha park mention is not part of the rule; citation gives the issue number;
|
|
31
|
+
Gini (1912) title spelled with its accents.
|
|
@@ -0,0 +1,23 @@
|
|
|
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: "greenrule"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-15"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://gitlab.com/geophilo1/greenrule_sdk"
|
|
13
|
+
url: "https://pypi.org/project/greenrule/"
|
|
14
|
+
abstract: "Urban green space provision and exposure indicators: the 3-30-300 rule per home (visible trees, neighbourhood canopy cover, distance to the nearest green space), the WHO Regional Office for Europe (2016) 300 m green space proximity indicator, green space per capita, NDVI buffer exposure and equity summaries (Gini coefficient, population shares meeting each criterion), with a built-in network shortest path on GeoJSON lines and a Euclidean fallback."
|
|
15
|
+
keywords:
|
|
16
|
+
- green space
|
|
17
|
+
- 3-30-300
|
|
18
|
+
- tree canopy
|
|
19
|
+
- NDVI
|
|
20
|
+
- accessibility
|
|
21
|
+
- environmental justice
|
|
22
|
+
- urban planning
|
|
23
|
+
- gis
|
greenrule-0.1.0/LICENSE
ADDED
|
@@ -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.
|
greenrule-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: greenrule
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Urban green space provision and exposure indicators: the 3-30-300 rule, the WHO Europe 300 m green space proximity indicator, green space per capita, NDVI buffer exposure and equity summaries.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/geophilo1/greenrule_sdk
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/geophilo1/greenrule_sdk
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/geophilo1/greenrule_sdk/-/work_items
|
|
10
|
+
Project-URL: Changelog, https://gitlab.com/geophilo1/greenrule_sdk/-/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: green-space,urban-green,3-30-300,tree-canopy,ndvi,greenness-exposure,accessibility,network-distance,environmental-justice,gini,urban-planning,public-health,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: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# greenrule
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/greenrule/)
|
|
39
|
+
[](https://pypi.org/project/greenrule/)
|
|
40
|
+
[](https://gitlab.com/geophilo1/greenrule_sdk/-/pipelines)
|
|
41
|
+
[](https://gitlab.com/geophilo1/greenrule_sdk/-/blob/main/LICENSE)
|
|
42
|
+
|
|
43
|
+
Urban green space provision and exposure indicators for homes, neighbourhoods and zones.
|
|
44
|
+
|
|
45
|
+
`greenrule` computes the green-space measures that planning and public-health studies
|
|
46
|
+
report most often: the 3-30-300 rule per home, the WHO Europe 300 m proximity indicator,
|
|
47
|
+
green space per capita, NDVI greenness in buffers around homes, and how evenly all of these
|
|
48
|
+
are distributed across zones. Distances are straight-line or along a street network built
|
|
49
|
+
from GeoJSON lines. It needs only NumPy.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install greenrule
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.9 or newer. Coordinates must be in a projected CRS in metres.
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import greenrule as gr
|
|
63
|
+
|
|
64
|
+
greens = gr.GreenSpaces.from_geojson(gr.read_geojson("parks.geojson"))
|
|
65
|
+
homes = gr.read_points("buildings.geojson") # points, or polygons -> centroids
|
|
66
|
+
pop = homes.column("residents")
|
|
67
|
+
|
|
68
|
+
# WHO Europe (2016): 300 m linear distance, 0.5 ha core and 1 ha additional indicator
|
|
69
|
+
both = gr.who_proximity_both(homes.xy, greens, population=pop)
|
|
70
|
+
both["core_0.5ha"].share, both["additional_1ha"].share
|
|
71
|
+
|
|
72
|
+
# 3-30-300 per home
|
|
73
|
+
canopy = gr.Raster(canopy_array, x0=500000, y0=4300000, dx=1, dy=1) # 1 = tree canopy
|
|
74
|
+
net = gr.Network([gr.read_geojson("streets.geojson")])
|
|
75
|
+
res = gr.assess(
|
|
76
|
+
homes.xy, greens, pop,
|
|
77
|
+
tree_counts=homes.column("visible_trees"), # from street-view or viewshed work
|
|
78
|
+
canopy=canopy, canopy_radius=250, # neighbourhood = 250 m buffer (your choice)
|
|
79
|
+
network=net, max_snap=100, # network distance, Euclidean fallback
|
|
80
|
+
zones=homes.labels("district"),
|
|
81
|
+
)
|
|
82
|
+
res.summary()["rule_3_30_300"]["share_all_three"]
|
|
83
|
+
res.summary()["equity"]["gini_between_zones"]
|
|
84
|
+
gr.write_json(gr.points_to_geojson(homes, res.to_rows()), "homes_green.geojson")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Single pieces are plain functions: `evaluate_3_30_300`, `who_proximity`,
|
|
88
|
+
`euclidean_distance`, `network_distance`, `green_provision`, `ndvi`, `ndvi_exposure`,
|
|
89
|
+
`gini`, `lorenz_curve`, `equity_summary`. See `examples/quickstart.py` for a run on
|
|
90
|
+
synthetic data.
|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
|
|
94
|
+
| Area | Functions | Notes |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| 3-30-300 | `evaluate_3_30_300 -> Rule333Result`, `visible_tree_counts`, `trees_within_radius`, `canopy_share_zones`, `canopy_share_buffers` | thresholds 3 / 30 % / 300 m; visibility is a user input |
|
|
97
|
+
| WHO Europe 2016 | `who_proximity`, `who_proximity_both -> ProximityResult` | 300 m linear distance; 0.5 ha core, 1 ha additional |
|
|
98
|
+
| Distances | `euclidean_distance`, `network_distance -> GreenDistance`, `Network` | boundary distance; entrances or boundary access points; Euclidean fallback |
|
|
99
|
+
| Provision | `green_provision -> Provision` | green m2, green share, m2 per resident by zone |
|
|
100
|
+
| Exposure | `ndvi`, `ndvi_exposure -> Exposure`, `Raster` | mean, median, SD, share of cells above a threshold |
|
|
101
|
+
| Equity | `gini`, `lorenz_curve`, `share_meeting`, `equity_summary` | population-weighted |
|
|
102
|
+
| One call | `assess -> Assessment` | rows per home and a summary |
|
|
103
|
+
| I/O | `read_geojson`, `read_points`, `areas_from_geojson`, `points_to_geojson`, `write_csv`, `write_json` | GeoJSON and CSV |
|
|
104
|
+
|
|
105
|
+
## Methods
|
|
106
|
+
|
|
107
|
+
### The 3-30-300 rule
|
|
108
|
+
|
|
109
|
+
Konijnendijk (2023) proposes that everyone should see at least **3** well-established
|
|
110
|
+
trees from their home (and school or workplace), that every neighbourhood should have at
|
|
111
|
+
least **30 %** tree canopy cover, and that every residence should lie no more than
|
|
112
|
+
**300 m** from the nearest public green space. The article mentions a park of at least
|
|
113
|
+
1 ha (after WHO) but the rule itself sets no minimum size, and it does not say whether
|
|
114
|
+
the 300 m is straight-line or walking distance. These three thresholds are the defaults
|
|
115
|
+
(`MIN_TREES`, `MIN_CANOPY`, `MAX_DISTANCE_M`). The rule leaves several choices open, so
|
|
116
|
+
they are explicit parameters here and should be reported:
|
|
117
|
+
|
|
118
|
+
- **Visible trees** need street-level imagery, a viewshed or a survey; give per-home counts
|
|
119
|
+
(`tree_counts`) or home-tree visibility pairs (`visible_pairs`). `trees_within_radius`
|
|
120
|
+
counts trees within a distance and is labelled as a proxy in the results.
|
|
121
|
+
- **Neighbourhood**: polygons (with canopy from a raster, from canopy polygons by grid
|
|
122
|
+
sampling, or precomputed shares) or a circular buffer around each home on a canopy
|
|
123
|
+
raster. Rasters can be binary (non-zero = canopy) or hold a cover fraction per cell.
|
|
124
|
+
- **Green space**: optional minimum size `min_green_area_ha` (none by default).
|
|
125
|
+
- **Distance**: Euclidean to the green-space boundary, or along the street network.
|
|
126
|
+
|
|
127
|
+
### WHO Europe (2016) Urban Green Space Indicator
|
|
128
|
+
|
|
129
|
+
WHO Regional Office for Europe (2016, chapter 4) defines the indicator as the proportion of
|
|
130
|
+
the population living within **300 m linear distance to the boundary** of a green space,
|
|
131
|
+
with two recommended alternative minimum sizes: **0.5 ha** (core indicator) and **1 ha**
|
|
132
|
+
(additional indicator), and recommends reporting both. Linear distance from the green-space
|
|
133
|
+
edge is treated there as an appropriate proxy for walking distance; additional runs with
|
|
134
|
+
other distances (for example 200 m and 500 m) are recommended. `who_proximity` follows this
|
|
135
|
+
definition (default 0.5 ha); `who_proximity_both` returns both sizes. Selecting the green
|
|
136
|
+
spaces (the report uses Urban Atlas class 1.4.1 "Green urban areas") is up to the user.
|
|
137
|
+
|
|
138
|
+
### Distances and the network
|
|
139
|
+
|
|
140
|
+
- Euclidean distance runs from the home to the nearest point of the green-space boundary
|
|
141
|
+
(0 inside).
|
|
142
|
+
- `Network` splits GeoJSON lines at every vertex and merges vertices within
|
|
143
|
+
`node_tolerance`. Homes and green-space access points are attached to the nearest segment
|
|
144
|
+
by a perpendicular access leg (up to `max_snap`); one multi-source Dijkstra (1959) search
|
|
145
|
+
from all access points gives every home its nearest green space.
|
|
146
|
+
- Access points are mapped entrances (`GreenSpaces.set_entrances`) or, failing that, points
|
|
147
|
+
every `entrance_spacing` metres (default 25) along the boundary.
|
|
148
|
+
- Homes without a network connection get their Euclidean distance (`method =
|
|
149
|
+
"euclidean"`) unless `fallback=None`.
|
|
150
|
+
|
|
151
|
+
### Provision, exposure and equity
|
|
152
|
+
|
|
153
|
+
- **Green space per capita** = green area in a zone / residents (m2 per person), with green
|
|
154
|
+
areas assigned by centroid or by grid sampling. No benchmark is built in. A
|
|
155
|
+
"9 m2 per person" figure is often attributed to the WHO, but WHO Europe (2016) does not
|
|
156
|
+
contain it.
|
|
157
|
+
- **NDVI** = (NIR - Red) / (NIR + Red) (Rouse et al. 1974). Buffer exposure uses cells
|
|
158
|
+
whose centre lies within the radius; the radius and any greenness threshold are study
|
|
159
|
+
choices (Markevych et al. 2017) and have no defaults.
|
|
160
|
+
- **Gini coefficient** from the population-weighted Lorenz curve by the trapezoid rule
|
|
161
|
+
(Gini 1912); with equal weights it equals the mean absolute difference over twice the
|
|
162
|
+
mean. `equity_summary` reports the population share meeting each criterion overall and
|
|
163
|
+
by zone, and the Gini of the per-zone shares. Wolch, Byrne & Newell (2014) review the
|
|
164
|
+
environmental-justice context.
|
|
165
|
+
|
|
166
|
+
## Command line
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
greenrule who homes.geojson parks.geojson --population-field pop --both --summary who.json
|
|
170
|
+
greenrule rule homes.geojson parks.geojson --population-field pop --trees-field visible_trees \
|
|
171
|
+
--canopy canopy.npy --transform=500000,4300000,1,1 --canopy-radius 250 \
|
|
172
|
+
--network streets.geojson --max-snap 100 --zone-field district \
|
|
173
|
+
-o homes_333.geojson --csv homes_333.csv --summary rule.json
|
|
174
|
+
greenrule exposure homes.csv --ndvi ndvi.npy --transform=500000,4300000,10,10 --radius 300
|
|
175
|
+
greenrule provision districts.geojson parks.geojson --population-field pop --method sample \
|
|
176
|
+
--resolution 5 --csv provision.csv
|
|
177
|
+
greenrule equity homes_333.csv --criteria meets_3,meets_30,meets_300 --population population \
|
|
178
|
+
--zone district --value green_distance
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Homes can be GeoJSON or CSV with `x`,`y` columns. Rasters are 2-D `.npy` arrays with
|
|
182
|
+
`--transform x0,y0_top,dx,dy` (write it with `=` when `x0` is negative). Summaries go to
|
|
183
|
+
stdout as JSON unless `--summary` or `-o` is given.
|
|
184
|
+
|
|
185
|
+
## Limitations
|
|
186
|
+
|
|
187
|
+
- Planar coordinates only; no reprojection.
|
|
188
|
+
- Tree visibility is not modelled; it must come from the user.
|
|
189
|
+
- Canopy and green overlap from polygons are grid-sampled; choose a resolution that is
|
|
190
|
+
small relative to the features.
|
|
191
|
+
- The network is undirected and unweighted by mode; there is no turn or barrier model.
|
|
192
|
+
- Polygon-to-polygon intersections are not computed exactly.
|
|
193
|
+
|
|
194
|
+
## References
|
|
195
|
+
|
|
196
|
+
- Dijkstra, E. W. (1959). A note on two problems in connexion with graphs. *Numerische
|
|
197
|
+
Mathematik* 1, 269-271.
|
|
198
|
+
- Gini, C. (1912). *Variabilità e mutabilità.* Bologna: Tipografia di Paolo Cuppini.
|
|
199
|
+
- Konijnendijk, C. C. (2023). Evidence-based guidelines for greener, healthier, more
|
|
200
|
+
resilient neighbourhoods: introducing the 3-30-300 rule. *Journal of Forestry Research*
|
|
201
|
+
34(3), 821-830. https://doi.org/10.1007/s11676-022-01523-z
|
|
202
|
+
- Markevych, I., Schoierer, J., Hartig, T., Chudnovsky, A., Hystad, P., Dzhambov, A. M.,
|
|
203
|
+
et al. (2017). Exploring pathways linking greenspace to health: theoretical and
|
|
204
|
+
methodological guidance. *Environmental Research* 158, 301-317.
|
|
205
|
+
https://doi.org/10.1016/j.envres.2017.06.028
|
|
206
|
+
- Rouse, J. W., Haas, R. H., Schell, J. A. & Deering, D. W. (1974). Monitoring vegetation
|
|
207
|
+
systems in the Great Plains with ERTS. *Third ERTS Symposium*, NASA SP-351, 309-317.
|
|
208
|
+
- WHO Regional Office for Europe (2016). *Urban green spaces and health: a review of
|
|
209
|
+
evidence.* Copenhagen: WHO Regional Office for Europe.
|
|
210
|
+
- Wolch, J. R., Byrne, J. & Newell, J. P. (2014). Urban green space, public health, and
|
|
211
|
+
environmental justice: the challenge of making cities "just green enough". *Landscape
|
|
212
|
+
and Urban Planning* 125, 234-244. https://doi.org/10.1016/j.landurbplan.2014.01.017
|
|
213
|
+
|
|
214
|
+
## Citation
|
|
215
|
+
|
|
216
|
+
```bibtex
|
|
217
|
+
@software{eminoglu_greenrule,
|
|
218
|
+
author = {Eminoğlu, Yusuf},
|
|
219
|
+
title = {greenrule},
|
|
220
|
+
year = {2026},
|
|
221
|
+
version = {0.1.0},
|
|
222
|
+
url = {https://gitlab.com/geophilo1/greenrule_sdk}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
See also [CITATION.cff](CITATION.cff).
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# greenrule
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/greenrule/)
|
|
4
|
+
[](https://pypi.org/project/greenrule/)
|
|
5
|
+
[](https://gitlab.com/geophilo1/greenrule_sdk/-/pipelines)
|
|
6
|
+
[](https://gitlab.com/geophilo1/greenrule_sdk/-/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
Urban green space provision and exposure indicators for homes, neighbourhoods and zones.
|
|
9
|
+
|
|
10
|
+
`greenrule` computes the green-space measures that planning and public-health studies
|
|
11
|
+
report most often: the 3-30-300 rule per home, the WHO Europe 300 m proximity indicator,
|
|
12
|
+
green space per capita, NDVI greenness in buffers around homes, and how evenly all of these
|
|
13
|
+
are distributed across zones. Distances are straight-line or along a street network built
|
|
14
|
+
from GeoJSON lines. It needs only NumPy.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install greenrule
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Python 3.9 or newer. Coordinates must be in a projected CRS in metres.
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import greenrule as gr
|
|
28
|
+
|
|
29
|
+
greens = gr.GreenSpaces.from_geojson(gr.read_geojson("parks.geojson"))
|
|
30
|
+
homes = gr.read_points("buildings.geojson") # points, or polygons -> centroids
|
|
31
|
+
pop = homes.column("residents")
|
|
32
|
+
|
|
33
|
+
# WHO Europe (2016): 300 m linear distance, 0.5 ha core and 1 ha additional indicator
|
|
34
|
+
both = gr.who_proximity_both(homes.xy, greens, population=pop)
|
|
35
|
+
both["core_0.5ha"].share, both["additional_1ha"].share
|
|
36
|
+
|
|
37
|
+
# 3-30-300 per home
|
|
38
|
+
canopy = gr.Raster(canopy_array, x0=500000, y0=4300000, dx=1, dy=1) # 1 = tree canopy
|
|
39
|
+
net = gr.Network([gr.read_geojson("streets.geojson")])
|
|
40
|
+
res = gr.assess(
|
|
41
|
+
homes.xy, greens, pop,
|
|
42
|
+
tree_counts=homes.column("visible_trees"), # from street-view or viewshed work
|
|
43
|
+
canopy=canopy, canopy_radius=250, # neighbourhood = 250 m buffer (your choice)
|
|
44
|
+
network=net, max_snap=100, # network distance, Euclidean fallback
|
|
45
|
+
zones=homes.labels("district"),
|
|
46
|
+
)
|
|
47
|
+
res.summary()["rule_3_30_300"]["share_all_three"]
|
|
48
|
+
res.summary()["equity"]["gini_between_zones"]
|
|
49
|
+
gr.write_json(gr.points_to_geojson(homes, res.to_rows()), "homes_green.geojson")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Single pieces are plain functions: `evaluate_3_30_300`, `who_proximity`,
|
|
53
|
+
`euclidean_distance`, `network_distance`, `green_provision`, `ndvi`, `ndvi_exposure`,
|
|
54
|
+
`gini`, `lorenz_curve`, `equity_summary`. See `examples/quickstart.py` for a run on
|
|
55
|
+
synthetic data.
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
| Area | Functions | Notes |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| 3-30-300 | `evaluate_3_30_300 -> Rule333Result`, `visible_tree_counts`, `trees_within_radius`, `canopy_share_zones`, `canopy_share_buffers` | thresholds 3 / 30 % / 300 m; visibility is a user input |
|
|
62
|
+
| WHO Europe 2016 | `who_proximity`, `who_proximity_both -> ProximityResult` | 300 m linear distance; 0.5 ha core, 1 ha additional |
|
|
63
|
+
| Distances | `euclidean_distance`, `network_distance -> GreenDistance`, `Network` | boundary distance; entrances or boundary access points; Euclidean fallback |
|
|
64
|
+
| Provision | `green_provision -> Provision` | green m2, green share, m2 per resident by zone |
|
|
65
|
+
| Exposure | `ndvi`, `ndvi_exposure -> Exposure`, `Raster` | mean, median, SD, share of cells above a threshold |
|
|
66
|
+
| Equity | `gini`, `lorenz_curve`, `share_meeting`, `equity_summary` | population-weighted |
|
|
67
|
+
| One call | `assess -> Assessment` | rows per home and a summary |
|
|
68
|
+
| I/O | `read_geojson`, `read_points`, `areas_from_geojson`, `points_to_geojson`, `write_csv`, `write_json` | GeoJSON and CSV |
|
|
69
|
+
|
|
70
|
+
## Methods
|
|
71
|
+
|
|
72
|
+
### The 3-30-300 rule
|
|
73
|
+
|
|
74
|
+
Konijnendijk (2023) proposes that everyone should see at least **3** well-established
|
|
75
|
+
trees from their home (and school or workplace), that every neighbourhood should have at
|
|
76
|
+
least **30 %** tree canopy cover, and that every residence should lie no more than
|
|
77
|
+
**300 m** from the nearest public green space. The article mentions a park of at least
|
|
78
|
+
1 ha (after WHO) but the rule itself sets no minimum size, and it does not say whether
|
|
79
|
+
the 300 m is straight-line or walking distance. These three thresholds are the defaults
|
|
80
|
+
(`MIN_TREES`, `MIN_CANOPY`, `MAX_DISTANCE_M`). The rule leaves several choices open, so
|
|
81
|
+
they are explicit parameters here and should be reported:
|
|
82
|
+
|
|
83
|
+
- **Visible trees** need street-level imagery, a viewshed or a survey; give per-home counts
|
|
84
|
+
(`tree_counts`) or home-tree visibility pairs (`visible_pairs`). `trees_within_radius`
|
|
85
|
+
counts trees within a distance and is labelled as a proxy in the results.
|
|
86
|
+
- **Neighbourhood**: polygons (with canopy from a raster, from canopy polygons by grid
|
|
87
|
+
sampling, or precomputed shares) or a circular buffer around each home on a canopy
|
|
88
|
+
raster. Rasters can be binary (non-zero = canopy) or hold a cover fraction per cell.
|
|
89
|
+
- **Green space**: optional minimum size `min_green_area_ha` (none by default).
|
|
90
|
+
- **Distance**: Euclidean to the green-space boundary, or along the street network.
|
|
91
|
+
|
|
92
|
+
### WHO Europe (2016) Urban Green Space Indicator
|
|
93
|
+
|
|
94
|
+
WHO Regional Office for Europe (2016, chapter 4) defines the indicator as the proportion of
|
|
95
|
+
the population living within **300 m linear distance to the boundary** of a green space,
|
|
96
|
+
with two recommended alternative minimum sizes: **0.5 ha** (core indicator) and **1 ha**
|
|
97
|
+
(additional indicator), and recommends reporting both. Linear distance from the green-space
|
|
98
|
+
edge is treated there as an appropriate proxy for walking distance; additional runs with
|
|
99
|
+
other distances (for example 200 m and 500 m) are recommended. `who_proximity` follows this
|
|
100
|
+
definition (default 0.5 ha); `who_proximity_both` returns both sizes. Selecting the green
|
|
101
|
+
spaces (the report uses Urban Atlas class 1.4.1 "Green urban areas") is up to the user.
|
|
102
|
+
|
|
103
|
+
### Distances and the network
|
|
104
|
+
|
|
105
|
+
- Euclidean distance runs from the home to the nearest point of the green-space boundary
|
|
106
|
+
(0 inside).
|
|
107
|
+
- `Network` splits GeoJSON lines at every vertex and merges vertices within
|
|
108
|
+
`node_tolerance`. Homes and green-space access points are attached to the nearest segment
|
|
109
|
+
by a perpendicular access leg (up to `max_snap`); one multi-source Dijkstra (1959) search
|
|
110
|
+
from all access points gives every home its nearest green space.
|
|
111
|
+
- Access points are mapped entrances (`GreenSpaces.set_entrances`) or, failing that, points
|
|
112
|
+
every `entrance_spacing` metres (default 25) along the boundary.
|
|
113
|
+
- Homes without a network connection get their Euclidean distance (`method =
|
|
114
|
+
"euclidean"`) unless `fallback=None`.
|
|
115
|
+
|
|
116
|
+
### Provision, exposure and equity
|
|
117
|
+
|
|
118
|
+
- **Green space per capita** = green area in a zone / residents (m2 per person), with green
|
|
119
|
+
areas assigned by centroid or by grid sampling. No benchmark is built in. A
|
|
120
|
+
"9 m2 per person" figure is often attributed to the WHO, but WHO Europe (2016) does not
|
|
121
|
+
contain it.
|
|
122
|
+
- **NDVI** = (NIR - Red) / (NIR + Red) (Rouse et al. 1974). Buffer exposure uses cells
|
|
123
|
+
whose centre lies within the radius; the radius and any greenness threshold are study
|
|
124
|
+
choices (Markevych et al. 2017) and have no defaults.
|
|
125
|
+
- **Gini coefficient** from the population-weighted Lorenz curve by the trapezoid rule
|
|
126
|
+
(Gini 1912); with equal weights it equals the mean absolute difference over twice the
|
|
127
|
+
mean. `equity_summary` reports the population share meeting each criterion overall and
|
|
128
|
+
by zone, and the Gini of the per-zone shares. Wolch, Byrne & Newell (2014) review the
|
|
129
|
+
environmental-justice context.
|
|
130
|
+
|
|
131
|
+
## Command line
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
greenrule who homes.geojson parks.geojson --population-field pop --both --summary who.json
|
|
135
|
+
greenrule rule homes.geojson parks.geojson --population-field pop --trees-field visible_trees \
|
|
136
|
+
--canopy canopy.npy --transform=500000,4300000,1,1 --canopy-radius 250 \
|
|
137
|
+
--network streets.geojson --max-snap 100 --zone-field district \
|
|
138
|
+
-o homes_333.geojson --csv homes_333.csv --summary rule.json
|
|
139
|
+
greenrule exposure homes.csv --ndvi ndvi.npy --transform=500000,4300000,10,10 --radius 300
|
|
140
|
+
greenrule provision districts.geojson parks.geojson --population-field pop --method sample \
|
|
141
|
+
--resolution 5 --csv provision.csv
|
|
142
|
+
greenrule equity homes_333.csv --criteria meets_3,meets_30,meets_300 --population population \
|
|
143
|
+
--zone district --value green_distance
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Homes can be GeoJSON or CSV with `x`,`y` columns. Rasters are 2-D `.npy` arrays with
|
|
147
|
+
`--transform x0,y0_top,dx,dy` (write it with `=` when `x0` is negative). Summaries go to
|
|
148
|
+
stdout as JSON unless `--summary` or `-o` is given.
|
|
149
|
+
|
|
150
|
+
## Limitations
|
|
151
|
+
|
|
152
|
+
- Planar coordinates only; no reprojection.
|
|
153
|
+
- Tree visibility is not modelled; it must come from the user.
|
|
154
|
+
- Canopy and green overlap from polygons are grid-sampled; choose a resolution that is
|
|
155
|
+
small relative to the features.
|
|
156
|
+
- The network is undirected and unweighted by mode; there is no turn or barrier model.
|
|
157
|
+
- Polygon-to-polygon intersections are not computed exactly.
|
|
158
|
+
|
|
159
|
+
## References
|
|
160
|
+
|
|
161
|
+
- Dijkstra, E. W. (1959). A note on two problems in connexion with graphs. *Numerische
|
|
162
|
+
Mathematik* 1, 269-271.
|
|
163
|
+
- Gini, C. (1912). *Variabilità e mutabilità.* Bologna: Tipografia di Paolo Cuppini.
|
|
164
|
+
- Konijnendijk, C. C. (2023). Evidence-based guidelines for greener, healthier, more
|
|
165
|
+
resilient neighbourhoods: introducing the 3-30-300 rule. *Journal of Forestry Research*
|
|
166
|
+
34(3), 821-830. https://doi.org/10.1007/s11676-022-01523-z
|
|
167
|
+
- Markevych, I., Schoierer, J., Hartig, T., Chudnovsky, A., Hystad, P., Dzhambov, A. M.,
|
|
168
|
+
et al. (2017). Exploring pathways linking greenspace to health: theoretical and
|
|
169
|
+
methodological guidance. *Environmental Research* 158, 301-317.
|
|
170
|
+
https://doi.org/10.1016/j.envres.2017.06.028
|
|
171
|
+
- Rouse, J. W., Haas, R. H., Schell, J. A. & Deering, D. W. (1974). Monitoring vegetation
|
|
172
|
+
systems in the Great Plains with ERTS. *Third ERTS Symposium*, NASA SP-351, 309-317.
|
|
173
|
+
- WHO Regional Office for Europe (2016). *Urban green spaces and health: a review of
|
|
174
|
+
evidence.* Copenhagen: WHO Regional Office for Europe.
|
|
175
|
+
- Wolch, J. R., Byrne, J. & Newell, J. P. (2014). Urban green space, public health, and
|
|
176
|
+
environmental justice: the challenge of making cities "just green enough". *Landscape
|
|
177
|
+
and Urban Planning* 125, 234-244. https://doi.org/10.1016/j.landurbplan.2014.01.017
|
|
178
|
+
|
|
179
|
+
## Citation
|
|
180
|
+
|
|
181
|
+
```bibtex
|
|
182
|
+
@software{eminoglu_greenrule,
|
|
183
|
+
author = {Eminoğlu, Yusuf},
|
|
184
|
+
title = {greenrule},
|
|
185
|
+
year = {2026},
|
|
186
|
+
version = {0.1.0},
|
|
187
|
+
url = {https://gitlab.com/geophilo1/greenrule_sdk}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
See also [CITATION.cff](CITATION.cff).
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""greenrule quickstart on a synthetic district (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 greenrule as gr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rect(x0: float, y0: float, x1: float, y1: float) -> dict:
|
|
16
|
+
return {"type": "Polygon", "coordinates": [[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]]}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
rng = np.random.default_rng(1)
|
|
21
|
+
# 200 homes in a 1.2 km x 0.8 km district (metres, projected CRS)
|
|
22
|
+
homes = np.column_stack([rng.uniform(0, 1200, 200), rng.uniform(0, 800, 200)])
|
|
23
|
+
residents = rng.integers(1, 6, 200)
|
|
24
|
+
greens = gr.GreenSpaces.from_geojson(
|
|
25
|
+
{
|
|
26
|
+
"type": "FeatureCollection",
|
|
27
|
+
"features": [
|
|
28
|
+
{
|
|
29
|
+
"type": "Feature",
|
|
30
|
+
"id": "pocket",
|
|
31
|
+
"properties": {},
|
|
32
|
+
"geometry": rect(100, 100, 180, 180),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "Feature",
|
|
36
|
+
"id": "park",
|
|
37
|
+
"properties": {},
|
|
38
|
+
"geometry": rect(700, 300, 820, 420),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
# canopy raster, 5 m cells: denser canopy in the east
|
|
44
|
+
canopy = (rng.random((160, 240)) < np.linspace(0.1, 0.5, 240)).astype(np.uint8)
|
|
45
|
+
canopy_r = gr.Raster(canopy, x0=0, y0=800, dx=5, dy=5)
|
|
46
|
+
# a simple street grid every 200 m
|
|
47
|
+
lines = [[[x, 0], [x, 800]] for x in range(0, 1201, 200)]
|
|
48
|
+
lines += [[[0, y], [1200, y]] for y in range(0, 801, 200)]
|
|
49
|
+
net = gr.Network(lines)
|
|
50
|
+
# visible-tree counts would come from street-level imagery or a viewshed analysis
|
|
51
|
+
visible = rng.integers(0, 7, 200)
|
|
52
|
+
zones = np.where(homes[:, 0] < 600, "west", "east")
|
|
53
|
+
|
|
54
|
+
res = gr.assess(
|
|
55
|
+
homes,
|
|
56
|
+
greens,
|
|
57
|
+
residents,
|
|
58
|
+
tree_counts=visible,
|
|
59
|
+
canopy=canopy_r,
|
|
60
|
+
canopy_radius=250,
|
|
61
|
+
network=net,
|
|
62
|
+
max_snap=150,
|
|
63
|
+
zones=zones,
|
|
64
|
+
)
|
|
65
|
+
s = res.summary()
|
|
66
|
+
print(json.dumps(s["rule_3_30_300"], indent=1))
|
|
67
|
+
print("WHO 2016 (0.5 ha, 300 m):", round(s["who_2016"]["share_within"], 3))
|
|
68
|
+
print("Gini of zone shares:", s["equity"]["gini_between_zones"])
|
|
69
|
+
both = gr.who_proximity_both(homes, greens, population=residents)
|
|
70
|
+
print({k: round(v.share, 3) for k, v in both.items()})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|