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.
Files changed (36) hide show
  1. greenrule-0.1.0/CHANGELOG.md +31 -0
  2. greenrule-0.1.0/CITATION.cff +23 -0
  3. greenrule-0.1.0/LICENSE +21 -0
  4. greenrule-0.1.0/MANIFEST.in +9 -0
  5. greenrule-0.1.0/PKG-INFO +230 -0
  6. greenrule-0.1.0/README.md +195 -0
  7. greenrule-0.1.0/examples/quickstart.py +74 -0
  8. greenrule-0.1.0/pyproject.toml +109 -0
  9. greenrule-0.1.0/setup.cfg +4 -0
  10. greenrule-0.1.0/src/greenrule/__init__.py +106 -0
  11. greenrule-0.1.0/src/greenrule/__main__.py +7 -0
  12. greenrule-0.1.0/src/greenrule/_common.py +79 -0
  13. greenrule-0.1.0/src/greenrule/_version.py +1 -0
  14. greenrule-0.1.0/src/greenrule/analysis.py +176 -0
  15. greenrule-0.1.0/src/greenrule/cli.py +318 -0
  16. greenrule-0.1.0/src/greenrule/equity.py +148 -0
  17. greenrule-0.1.0/src/greenrule/exposure.py +90 -0
  18. greenrule-0.1.0/src/greenrule/geometry.py +245 -0
  19. greenrule-0.1.0/src/greenrule/green.py +188 -0
  20. greenrule-0.1.0/src/greenrule/io.py +196 -0
  21. greenrule-0.1.0/src/greenrule/network.py +179 -0
  22. greenrule-0.1.0/src/greenrule/provision.py +113 -0
  23. greenrule-0.1.0/src/greenrule/py.typed +0 -0
  24. greenrule-0.1.0/src/greenrule/raster.py +149 -0
  25. greenrule-0.1.0/src/greenrule/rule333.py +301 -0
  26. greenrule-0.1.0/src/greenrule/who.py +142 -0
  27. greenrule-0.1.0/src/greenrule.egg-info/PKG-INFO +230 -0
  28. greenrule-0.1.0/src/greenrule.egg-info/SOURCES.txt +34 -0
  29. greenrule-0.1.0/src/greenrule.egg-info/dependency_links.txt +1 -0
  30. greenrule-0.1.0/src/greenrule.egg-info/entry_points.txt +2 -0
  31. greenrule-0.1.0/src/greenrule.egg-info/requires.txt +7 -0
  32. greenrule-0.1.0/src/greenrule.egg-info/top_level.txt +1 -0
  33. greenrule-0.1.0/tests/conftest.py +31 -0
  34. greenrule-0.1.0/tests/test_core.py +256 -0
  35. greenrule-0.1.0/tests/test_io_cli.py +328 -0
  36. 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
@@ -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/greenrule/py.typed
7
+ recursive-include src/greenrule *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/greenrule.svg)](https://pypi.org/project/greenrule/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/greenrule.svg)](https://pypi.org/project/greenrule/)
40
+ [![pipeline](https://gitlab.com/geophilo1/greenrule_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/greenrule_sdk/-/pipelines)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/greenrule.svg)](https://pypi.org/project/greenrule/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/greenrule.svg)](https://pypi.org/project/greenrule/)
5
+ [![pipeline](https://gitlab.com/geophilo1/greenrule_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/greenrule_sdk/-/pipelines)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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()