skyview 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.
@@ -0,0 +1,20 @@
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
+ - Raster sky view factor from a DSM or building-height array: horizon search in N azimuths
10
+ (Zakšek et al., 2011), isotropic (`1 - mean sin γ`) and cosine-weighted
11
+ (`1 - mean sin² γ`) variants, observer height, edge handling, nodata.
12
+ - Positive and negative topographic openness (Yokoyama et al., 2002).
13
+ - Exact vector SVF at points from building footprints and heights (ray / edge
14
+ intersection, holes, points on roofs), point grids and footprint rasterisation.
15
+ - Analytic infinite-canyon SVF (Oke, 1981) at any cross-street position, both weightings,
16
+ plus floor- and wall-averaged view factors (Johnson & Watson, 1984).
17
+ - Street-canyon width, height, H/W, enclosure and axial bearing from footprints and
18
+ centrelines; Boeing (2019) orientation histogram, entropy and orientation order.
19
+ - GeoJSON output, `.npy` raster I/O and GeoTIFF I/O through the optional `raster` extra.
20
+ - `skyview` command line tool: `raster`, `openness`, `points`, `streets`, `canyon`.
@@ -0,0 +1,20 @@
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: "skyview"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/skyview_sdk"
13
+ url: "https://pypi.org/project/skyview/"
14
+ abstract: "Sky view factor, topographic openness and street-canyon H/W metrics from DSM rasters or building footprints, validated against Oke's analytic canyon formula."
15
+ keywords:
16
+ - sky view factor
17
+ - urban canyon
18
+ - openness
19
+ - urban climate
20
+ - gis
skyview-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.
@@ -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/skyview/py.typed
7
+ recursive-include src/skyview *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
skyview-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: skyview
3
+ Version: 0.1.0
4
+ Summary: Sky view factor, topographic openness and street-canyon H/W metrics from DSM rasters or building footprints, validated against Oke's analytic canyon formula.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/skyview_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/skyview_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/skyview_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/skyview_sdk/blob/main/CHANGELOG.md
11
+ Keywords: sky-view-factor,svf,urban-canyon,street-canyon,aspect-ratio,openness,dsm,horizon,urban-climate,urban-heat-island,building-footprints,geojson,gis,urban-planning
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: Topic :: Scientific/Engineering :: Atmospheric Science
23
+ Classifier: Intended Audience :: Science/Research
24
+ Classifier: Intended Audience :: Developers
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.20
30
+ Provides-Extra: raster
31
+ Requires-Dist: rasterio>=1.3; extra == "raster"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
36
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # skyview
40
+
41
+ [![PyPI version](https://img.shields.io/pypi/v/skyview.svg)](https://pypi.org/project/skyview/)
42
+ [![Python versions](https://img.shields.io/pypi/pyversions/skyview.svg)](https://pypi.org/project/skyview/)
43
+ [![CI](https://github.com/YusufEminoglu/skyview_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/skyview_sdk/actions/workflows/ci.yml)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/skyview_sdk/blob/main/LICENSE)
45
+
46
+ **Sky view factor and street-canyon geometry for urban planning and urban climate work.**
47
+
48
+ `skyview` measures how much sky is visible from the ground and how enclosed
49
+ streets are. It works from either a gridded surface model or from building
50
+ footprints with heights:
51
+
52
+ - **SVF from a raster.** Sky view factor of every cell of a DSM or
53
+ building-height array. Horizon angles are searched in N directions
54
+ (Zakšek et al., 2011). Isotropic and cosine-weighted variants.
55
+ - **Topographic openness.** Positive and negative openness (Yokoyama et al., 2002).
56
+ - **SVF at points from footprints.** The horizon is found exactly by casting rays against
57
+ footprint edges, with no rasterising. Holes (courtyards), observer height, and
58
+ points on roofs are supported.
59
+ - **Analytic canyon formulas.** Oke's SVF for an infinite symmetric canyon at any
60
+ cross-street position, plus floor- and wall-averaged view factors. Use them to validate
61
+ your own numbers.
62
+ - **Street-canyon metrics.** Width, height, H/W ratio, enclosure and axial bearing along
63
+ street centrelines. Street-orientation entropy and order (Boeing, 2019).
64
+ - **Output** as numpy arrays, GeoJSON, `.npy` or GeoTIFF, and a `skyview` command line tool.
65
+
66
+ numpy is the only required dependency. GeoTIFF reading and writing uses the
67
+ optional `raster` extra (rasterio).
68
+
69
+ For hours of direct sun and building shadows, use the companion package
70
+ [`urbanshadow`](https://pypi.org/project/urbanshadow/). It works from the same
71
+ footprints-and-heights input.
72
+
73
+ ```bash
74
+ pip install skyview # numpy only
75
+ pip install "skyview[raster]" # + GeoTIFF I/O via rasterio
76
+ ```
77
+
78
+ Requires Python 3.9 or newer.
79
+
80
+ ---
81
+
82
+ ## Quick start (Python)
83
+
84
+ ```python
85
+ import numpy as np
86
+ from skyview import Building, BuildingSet, canyon_svf, sky_view_factor, street_canyon
87
+
88
+ # Closed form: centre of an infinite canyon with H/W = 1 (Oke 1981)
89
+ canyon_svf(1.0) # 0.4472 (cosine-weighted)
90
+ canyon_svf(1.0, "isotropic") # 0.2952
91
+
92
+ # Raster: 1 m building-height grid
93
+ dsm = np.zeros((200, 200))
94
+ dsm[50:150, 60:90] = 24.0
95
+ svf = sky_view_factor(dsm, pixel_size=1.0, n_directions=32, max_radius=150.0)
96
+
97
+ # Footprints: exact horizon at points
98
+ blocks = BuildingSet([
99
+ Building.box(-40, -300, -10, 300, 18.0),
100
+ Building.box(10, -300, 40, 300, 18.0),
101
+ ])
102
+ blocks.sky_view_factor([0.0, 5.0], [0.0, 0.0], n_directions=360, weighting="cosine")
103
+
104
+ # Street canyon along a centreline
105
+ c = street_canyon([(0, -250), (0, 250)], blocks, spacing=25.0)
106
+ c.width_mean, c.height_mean, c.hw_mean, c.bearing # 20.0, 18.0, 0.9, 0.0
107
+ ```
108
+
109
+ See [`examples/quickstart.py`](examples/quickstart.py) for the same canyon computed in three
110
+ ways.
111
+
112
+ ## Quick start (command line)
113
+
114
+ ```bash
115
+ # SVF raster from a DSM (GeoTIFF needs the raster extra; .npy needs --pixel-size)
116
+ skyview raster dsm.tif -o svf.tif --directions 32 --radius 100 --weighting isotropic
117
+
118
+ # Positive and negative openness
119
+ skyview openness dsm.tif -o openness_pos.tif --negative openness_neg.tif
120
+
121
+ # SVF at points, or on a grid over the buildings (projected GeoJSON, metres)
122
+ skyview points buildings.geojson --points sensors.geojson -o sensors_svf.geojson
123
+ skyview points buildings.geojson --grid 5 --observer-height 1.1 -o grid_svf.geojson
124
+
125
+ # H/W and orientation for street centrelines
126
+ skyview streets buildings.geojson streets.geojson -o canyons.geojson
127
+
128
+ # Analytic reference values for a canyon
129
+ skyview canyon 1.0
130
+ ```
131
+
132
+ Building heights come from `--height-field` (default `height`). Storey counts
133
+ can be used instead with `--levels-field` and `--floor-height`. `--default-height`
134
+ fills any gaps.
135
+
136
+ ## API overview
137
+
138
+ | Function / class | What it does |
139
+ | --- | --- |
140
+ | `sky_view_factor(dsm, pixel_size, n_directions, max_radius, weighting, observer_height, edge)` | SVF of every raster cell |
141
+ | `horizon_angles(...)`, `elevation_angle_extremes(...)` | horizon angle stack `(n_dir, rows, cols)` |
142
+ | `svf_from_horizon(angles, weighting)` | SVF from your own horizon angles |
143
+ | `openness(dsm, pixel_size, n_directions=8, max_radius)` | positive / negative openness, degrees |
144
+ | `Building`, `BuildingSet`, `buildings_from_geojson(...)` | footprint prisms and fast edge arrays |
145
+ | `BuildingSet.sky_view_factor(x, y, ...)`, `BuildingSet.horizon_angles(x, y, ...)` | exact vector SVF at points |
146
+ | `point_grid`, `points_in_buildings`, `rasterize_buildings` | grids, masks, footprints to a height raster |
147
+ | `canyon_svf(hw, weighting, position)`, `canyon_floor_mean_svf`, `canyon_wall_mean_svf`, `canyon_horizon_angle` | analytic canyon |
148
+ | `street_canyon`, `street_canyons` → `StreetCanyon` | width, height, H/W, enclosure, bearing per street |
149
+ | `orientation_histogram`, `orientation_entropy`, `orientation_order` | street orientation statistics |
150
+ | `read_raster`, `write_raster`, `read_geojson`, `write_geojson`, `points_to_geojson`, `canyons_to_geojson` | I/O |
151
+
152
+ ## Method and sources
153
+
154
+ **Horizon search (raster).** Zakšek, Oštir and Kokalj (2011),
155
+ *Remote Sensing* 3(2), 398–415, https://doi.org/10.3390/rs3020398, describe
156
+ the search. From every cell, rays are cast in `n_directions` azimuths, measured
157
+ clockwise from grid north, out to `max_radius`. Along each ray the elevation angle
158
+ `atan((z − z0)/d)` is computed for every visited cell, and the maximum is the
159
+ horizon angle γᵢ. The visited cells are those hit by sampling the ray every pixel
160
+ and rounding to the nearest cell. `d` is the exact distance between the two cell
161
+ centres.
162
+
163
+ **Two SVF definitions.**
164
+
165
+ - *Isotropic*: `SVF = 1 − (1/n) Σ sin γᵢ`. This is the share of the hemisphere's
166
+ solid angle that is visible. Zakšek et al. use this form for relief visualisation.
167
+ - *Cosine-weighted*: `SVF = 1 − (1/n) Σ sin² γᵢ`. This is the radiative view factor
168
+ of a horizontal surface, the quantity in the urban-climate canyon literature
169
+ (Oke; Johnson & Watson).
170
+
171
+ Both follow from integrating over the visible part of each azimuth slice:
172
+ `∫ cos e de` and `∫ 2 sin e cos e de` from γ to 90°. Horizon angles below 0 are
173
+ clamped, so SVF ≤ 1.
174
+
175
+ **Analytic canyon** (`skyview.analytic`), for an infinite canyon of height H and
176
+ width W:
177
+
178
+ - centre-line cosine SVF: `cos(arctan(2H/W))`. Oke, T. R. (1981), *J. Climatology* 1,
179
+ 237–254; Oke (1987), *Boundary Layer Climates*.
180
+ - off-centre point at distances a and b from the walls: `(a/√(a²+H²) + b/√(b²+H²))/2`
181
+ (cosine) and `1 − (arctan(H/a) + arctan(H/b))/π` (isotropic).
182
+ - floor-averaged `√(1+(H/W)²) − H/W` and wall-averaged `(1 + W/H − √(1+(W/H)²))/2`.
183
+ Johnson, G. T. and Watson, I. D. (1984), *J. Climate Appl. Meteorol.* 23, 329–335.
184
+ These are Hottel's crossed-strings rule.
185
+
186
+ The test suite checks these closed forms against numerical integration of the
187
+ horizon profile. It also checks that floor and wall factors close the energy
188
+ balance of the canyon opening.
189
+
190
+ **Openness.** Yokoyama, R., Shirasawa, M. and Pike, R. J. (2002),
191
+ *Photogrammetric Engineering & Remote Sensing* 68(3), 257–265. Positive
192
+ openness is the mean of `90° − max elevation angle` over the directions, and
193
+ negative openness is the mean of `90° + min elevation angle`. Both equal 90° on
194
+ flat ground.
195
+
196
+ **Vector horizon.** Each ray is intersected with every footprint edge. A building
197
+ prism has a constant height, so its highest elevation angle along a ray is at the
198
+ edge where the ray enters it. Taking the maximum over all edge crossings is
199
+ therefore exact for flat ground and prismatic buildings.
200
+
201
+ **Street canyons.** Stations are placed every `spacing` metres along a centreline.
202
+ Perpendicular rays find the first building on each side within `max_distance`.
203
+ Per station, `W = d_left + d_right`, `H = (h_left + h_right)/2`, and the aspect
204
+ ratio is H/W, the main canyon descriptor in Oke (1988), *Energy and Buildings* 11,
205
+ 103–113. The summary also gives `svf_oke`, Oke's centre-line SVF for the mean
206
+ H/W, which is an idealised estimate.
207
+
208
+ **Street orientation.** Boeing, G. (2019), *Applied Network Science* 4, 67,
209
+ https://doi.org/10.1007/s41109-019-0189-1. The method uses 36 bins of 10°
210
+ centred on 0°, 10°, … . Each segment is counted in both directions, weighted by
211
+ length by default. The entropy is `H = −Σ P ln P`, and the orientation order is
212
+ `φ = 1 − ((H − ln 4)/(ln 36 − ln 4))²`.
213
+
214
+ ## Validation
215
+
216
+ - Flat ground gives SVF = 1 for both weightings and openness = 90°/90°.
217
+ - Raster canyon (wall cells extended past the raster edge with `edge="nearest"`):
218
+ results are within 0.01 of Oke's formula for H/W from about 0.45 to 1.9.
219
+ - Vector canyon with 720 directions: within 2·10⁻⁴ of the analytic value at the
220
+ centre and off-centre positions.
221
+ - Exact hand-computed horizons for a single tower, a pit, a peak and a square
222
+ courtyard.
223
+
224
+ ## Limitations
225
+
226
+ - Coordinates and pixel sizes must be in metres in a projected CRS. Geographic
227
+ (lon/lat) data must be reprojected first.
228
+ - In the raster method, surfaces sit at cell centres and rays are sampled by
229
+ nearest-cell rounding. Walls are therefore effectively half a cell further away
230
+ than the footprint edge, and thin features may be missed between ray samples
231
+ at coarse direction counts. Use a fine pixel size and 32 or more directions in
232
+ dense fabric.
233
+ - Raster run time grows with `n_directions × max_radius / pixel_size` passes over
234
+ the array. This is plain numpy and is meant for district-scale rasters, not whole
235
+ cities at 0.5 m.
236
+ - The vector method assumes flat ground and flat-roofed prisms. Trees, terrain
237
+ and roof shapes are ignored, so use a DSM for those.
238
+ - `edge="ignore"` treats space outside the raster as unobstructed, so SVF is
239
+ overestimated near the raster border. Pad your area of interest by `max_radius`.
240
+ - The street-canyon summary uses the first building hit on each side. Setbacks,
241
+ gaps and intersections reduce `enclosure` but are not modelled in more detail.
242
+ - Cosine weighting assumes a horizontal receiver and an isotropic sky radiance.
243
+ Anisotropic sky models are out of scope.
244
+
245
+ ## Academic citation
246
+
247
+ ```bibtex
248
+ @software{eminoglu_skyview,
249
+ author = {Eminoğlu, Yusuf},
250
+ title = {skyview},
251
+ year = {2026},
252
+ version = {0.1.0},
253
+ url = {https://github.com/YusufEminoglu/skyview_sdk}
254
+ }
255
+ ```
256
+
257
+ ## License
258
+
259
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,221 @@
1
+ # skyview
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/skyview.svg)](https://pypi.org/project/skyview/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/skyview.svg)](https://pypi.org/project/skyview/)
5
+ [![CI](https://github.com/YusufEminoglu/skyview_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/skyview_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/skyview_sdk/blob/main/LICENSE)
7
+
8
+ **Sky view factor and street-canyon geometry for urban planning and urban climate work.**
9
+
10
+ `skyview` measures how much sky is visible from the ground and how enclosed
11
+ streets are. It works from either a gridded surface model or from building
12
+ footprints with heights:
13
+
14
+ - **SVF from a raster.** Sky view factor of every cell of a DSM or
15
+ building-height array. Horizon angles are searched in N directions
16
+ (Zakšek et al., 2011). Isotropic and cosine-weighted variants.
17
+ - **Topographic openness.** Positive and negative openness (Yokoyama et al., 2002).
18
+ - **SVF at points from footprints.** The horizon is found exactly by casting rays against
19
+ footprint edges, with no rasterising. Holes (courtyards), observer height, and
20
+ points on roofs are supported.
21
+ - **Analytic canyon formulas.** Oke's SVF for an infinite symmetric canyon at any
22
+ cross-street position, plus floor- and wall-averaged view factors. Use them to validate
23
+ your own numbers.
24
+ - **Street-canyon metrics.** Width, height, H/W ratio, enclosure and axial bearing along
25
+ street centrelines. Street-orientation entropy and order (Boeing, 2019).
26
+ - **Output** as numpy arrays, GeoJSON, `.npy` or GeoTIFF, and a `skyview` command line tool.
27
+
28
+ numpy is the only required dependency. GeoTIFF reading and writing uses the
29
+ optional `raster` extra (rasterio).
30
+
31
+ For hours of direct sun and building shadows, use the companion package
32
+ [`urbanshadow`](https://pypi.org/project/urbanshadow/). It works from the same
33
+ footprints-and-heights input.
34
+
35
+ ```bash
36
+ pip install skyview # numpy only
37
+ pip install "skyview[raster]" # + GeoTIFF I/O via rasterio
38
+ ```
39
+
40
+ Requires Python 3.9 or newer.
41
+
42
+ ---
43
+
44
+ ## Quick start (Python)
45
+
46
+ ```python
47
+ import numpy as np
48
+ from skyview import Building, BuildingSet, canyon_svf, sky_view_factor, street_canyon
49
+
50
+ # Closed form: centre of an infinite canyon with H/W = 1 (Oke 1981)
51
+ canyon_svf(1.0) # 0.4472 (cosine-weighted)
52
+ canyon_svf(1.0, "isotropic") # 0.2952
53
+
54
+ # Raster: 1 m building-height grid
55
+ dsm = np.zeros((200, 200))
56
+ dsm[50:150, 60:90] = 24.0
57
+ svf = sky_view_factor(dsm, pixel_size=1.0, n_directions=32, max_radius=150.0)
58
+
59
+ # Footprints: exact horizon at points
60
+ blocks = BuildingSet([
61
+ Building.box(-40, -300, -10, 300, 18.0),
62
+ Building.box(10, -300, 40, 300, 18.0),
63
+ ])
64
+ blocks.sky_view_factor([0.0, 5.0], [0.0, 0.0], n_directions=360, weighting="cosine")
65
+
66
+ # Street canyon along a centreline
67
+ c = street_canyon([(0, -250), (0, 250)], blocks, spacing=25.0)
68
+ c.width_mean, c.height_mean, c.hw_mean, c.bearing # 20.0, 18.0, 0.9, 0.0
69
+ ```
70
+
71
+ See [`examples/quickstart.py`](examples/quickstart.py) for the same canyon computed in three
72
+ ways.
73
+
74
+ ## Quick start (command line)
75
+
76
+ ```bash
77
+ # SVF raster from a DSM (GeoTIFF needs the raster extra; .npy needs --pixel-size)
78
+ skyview raster dsm.tif -o svf.tif --directions 32 --radius 100 --weighting isotropic
79
+
80
+ # Positive and negative openness
81
+ skyview openness dsm.tif -o openness_pos.tif --negative openness_neg.tif
82
+
83
+ # SVF at points, or on a grid over the buildings (projected GeoJSON, metres)
84
+ skyview points buildings.geojson --points sensors.geojson -o sensors_svf.geojson
85
+ skyview points buildings.geojson --grid 5 --observer-height 1.1 -o grid_svf.geojson
86
+
87
+ # H/W and orientation for street centrelines
88
+ skyview streets buildings.geojson streets.geojson -o canyons.geojson
89
+
90
+ # Analytic reference values for a canyon
91
+ skyview canyon 1.0
92
+ ```
93
+
94
+ Building heights come from `--height-field` (default `height`). Storey counts
95
+ can be used instead with `--levels-field` and `--floor-height`. `--default-height`
96
+ fills any gaps.
97
+
98
+ ## API overview
99
+
100
+ | Function / class | What it does |
101
+ | --- | --- |
102
+ | `sky_view_factor(dsm, pixel_size, n_directions, max_radius, weighting, observer_height, edge)` | SVF of every raster cell |
103
+ | `horizon_angles(...)`, `elevation_angle_extremes(...)` | horizon angle stack `(n_dir, rows, cols)` |
104
+ | `svf_from_horizon(angles, weighting)` | SVF from your own horizon angles |
105
+ | `openness(dsm, pixel_size, n_directions=8, max_radius)` | positive / negative openness, degrees |
106
+ | `Building`, `BuildingSet`, `buildings_from_geojson(...)` | footprint prisms and fast edge arrays |
107
+ | `BuildingSet.sky_view_factor(x, y, ...)`, `BuildingSet.horizon_angles(x, y, ...)` | exact vector SVF at points |
108
+ | `point_grid`, `points_in_buildings`, `rasterize_buildings` | grids, masks, footprints to a height raster |
109
+ | `canyon_svf(hw, weighting, position)`, `canyon_floor_mean_svf`, `canyon_wall_mean_svf`, `canyon_horizon_angle` | analytic canyon |
110
+ | `street_canyon`, `street_canyons` → `StreetCanyon` | width, height, H/W, enclosure, bearing per street |
111
+ | `orientation_histogram`, `orientation_entropy`, `orientation_order` | street orientation statistics |
112
+ | `read_raster`, `write_raster`, `read_geojson`, `write_geojson`, `points_to_geojson`, `canyons_to_geojson` | I/O |
113
+
114
+ ## Method and sources
115
+
116
+ **Horizon search (raster).** Zakšek, Oštir and Kokalj (2011),
117
+ *Remote Sensing* 3(2), 398–415, https://doi.org/10.3390/rs3020398, describe
118
+ the search. From every cell, rays are cast in `n_directions` azimuths, measured
119
+ clockwise from grid north, out to `max_radius`. Along each ray the elevation angle
120
+ `atan((z − z0)/d)` is computed for every visited cell, and the maximum is the
121
+ horizon angle γᵢ. The visited cells are those hit by sampling the ray every pixel
122
+ and rounding to the nearest cell. `d` is the exact distance between the two cell
123
+ centres.
124
+
125
+ **Two SVF definitions.**
126
+
127
+ - *Isotropic*: `SVF = 1 − (1/n) Σ sin γᵢ`. This is the share of the hemisphere's
128
+ solid angle that is visible. Zakšek et al. use this form for relief visualisation.
129
+ - *Cosine-weighted*: `SVF = 1 − (1/n) Σ sin² γᵢ`. This is the radiative view factor
130
+ of a horizontal surface, the quantity in the urban-climate canyon literature
131
+ (Oke; Johnson & Watson).
132
+
133
+ Both follow from integrating over the visible part of each azimuth slice:
134
+ `∫ cos e de` and `∫ 2 sin e cos e de` from γ to 90°. Horizon angles below 0 are
135
+ clamped, so SVF ≤ 1.
136
+
137
+ **Analytic canyon** (`skyview.analytic`), for an infinite canyon of height H and
138
+ width W:
139
+
140
+ - centre-line cosine SVF: `cos(arctan(2H/W))`. Oke, T. R. (1981), *J. Climatology* 1,
141
+ 237–254; Oke (1987), *Boundary Layer Climates*.
142
+ - off-centre point at distances a and b from the walls: `(a/√(a²+H²) + b/√(b²+H²))/2`
143
+ (cosine) and `1 − (arctan(H/a) + arctan(H/b))/π` (isotropic).
144
+ - floor-averaged `√(1+(H/W)²) − H/W` and wall-averaged `(1 + W/H − √(1+(W/H)²))/2`.
145
+ Johnson, G. T. and Watson, I. D. (1984), *J. Climate Appl. Meteorol.* 23, 329–335.
146
+ These are Hottel's crossed-strings rule.
147
+
148
+ The test suite checks these closed forms against numerical integration of the
149
+ horizon profile. It also checks that floor and wall factors close the energy
150
+ balance of the canyon opening.
151
+
152
+ **Openness.** Yokoyama, R., Shirasawa, M. and Pike, R. J. (2002),
153
+ *Photogrammetric Engineering & Remote Sensing* 68(3), 257–265. Positive
154
+ openness is the mean of `90° − max elevation angle` over the directions, and
155
+ negative openness is the mean of `90° + min elevation angle`. Both equal 90° on
156
+ flat ground.
157
+
158
+ **Vector horizon.** Each ray is intersected with every footprint edge. A building
159
+ prism has a constant height, so its highest elevation angle along a ray is at the
160
+ edge where the ray enters it. Taking the maximum over all edge crossings is
161
+ therefore exact for flat ground and prismatic buildings.
162
+
163
+ **Street canyons.** Stations are placed every `spacing` metres along a centreline.
164
+ Perpendicular rays find the first building on each side within `max_distance`.
165
+ Per station, `W = d_left + d_right`, `H = (h_left + h_right)/2`, and the aspect
166
+ ratio is H/W, the main canyon descriptor in Oke (1988), *Energy and Buildings* 11,
167
+ 103–113. The summary also gives `svf_oke`, Oke's centre-line SVF for the mean
168
+ H/W, which is an idealised estimate.
169
+
170
+ **Street orientation.** Boeing, G. (2019), *Applied Network Science* 4, 67,
171
+ https://doi.org/10.1007/s41109-019-0189-1. The method uses 36 bins of 10°
172
+ centred on 0°, 10°, … . Each segment is counted in both directions, weighted by
173
+ length by default. The entropy is `H = −Σ P ln P`, and the orientation order is
174
+ `φ = 1 − ((H − ln 4)/(ln 36 − ln 4))²`.
175
+
176
+ ## Validation
177
+
178
+ - Flat ground gives SVF = 1 for both weightings and openness = 90°/90°.
179
+ - Raster canyon (wall cells extended past the raster edge with `edge="nearest"`):
180
+ results are within 0.01 of Oke's formula for H/W from about 0.45 to 1.9.
181
+ - Vector canyon with 720 directions: within 2·10⁻⁴ of the analytic value at the
182
+ centre and off-centre positions.
183
+ - Exact hand-computed horizons for a single tower, a pit, a peak and a square
184
+ courtyard.
185
+
186
+ ## Limitations
187
+
188
+ - Coordinates and pixel sizes must be in metres in a projected CRS. Geographic
189
+ (lon/lat) data must be reprojected first.
190
+ - In the raster method, surfaces sit at cell centres and rays are sampled by
191
+ nearest-cell rounding. Walls are therefore effectively half a cell further away
192
+ than the footprint edge, and thin features may be missed between ray samples
193
+ at coarse direction counts. Use a fine pixel size and 32 or more directions in
194
+ dense fabric.
195
+ - Raster run time grows with `n_directions × max_radius / pixel_size` passes over
196
+ the array. This is plain numpy and is meant for district-scale rasters, not whole
197
+ cities at 0.5 m.
198
+ - The vector method assumes flat ground and flat-roofed prisms. Trees, terrain
199
+ and roof shapes are ignored, so use a DSM for those.
200
+ - `edge="ignore"` treats space outside the raster as unobstructed, so SVF is
201
+ overestimated near the raster border. Pad your area of interest by `max_radius`.
202
+ - The street-canyon summary uses the first building hit on each side. Setbacks,
203
+ gaps and intersections reduce `enclosure` but are not modelled in more detail.
204
+ - Cosine weighting assumes a horizontal receiver and an isotropic sky radiance.
205
+ Anisotropic sky models are out of scope.
206
+
207
+ ## Academic citation
208
+
209
+ ```bibtex
210
+ @software{eminoglu_skyview,
211
+ author = {Eminoğlu, Yusuf},
212
+ title = {skyview},
213
+ year = {2026},
214
+ version = {0.1.0},
215
+ url = {https://github.com/YusufEminoglu/skyview_sdk}
216
+ }
217
+ ```
218
+
219
+ ## License
220
+
221
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,61 @@
1
+ """skyview quick start: one street canyon, three ways.
2
+
3
+ Run from a clone (no data files needed):
4
+
5
+ python examples/quickstart.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+ from skyview import (
13
+ Building,
14
+ BuildingSet,
15
+ canyon_svf,
16
+ rasterize_buildings,
17
+ sky_view_factor,
18
+ street_canyon,
19
+ )
20
+
21
+
22
+ def main() -> int:
23
+ # Two 18 m high blocks along a north-south street, building lines 20 m apart
24
+ # (H/W = 0.9). Coordinates are metres in a projected CRS.
25
+ blocks = BuildingSet(
26
+ [
27
+ Building.box(-40.0, -300.0, -10.0, 300.0, 18.0),
28
+ Building.box(10.0, -300.0, 40.0, 300.0, 18.0),
29
+ ]
30
+ )
31
+ hw = 18.0 / 20.0
32
+
33
+ # 1. Closed form (Oke 1981): centre of an infinite symmetric canyon.
34
+ print(f"analytic cosine SVF : {canyon_svf(hw, 'cosine'):.4f}")
35
+ print(f"analytic isotropic SVF: {canyon_svf(hw, 'isotropic'):.4f}")
36
+
37
+ # 2. Exact vector horizon at the street centre line.
38
+ for w in ("cosine", "isotropic"):
39
+ v = blocks.sky_view_factor(0.0, 0.0, n_directions=360, weighting=w)[0]
40
+ print(f"footprints {w:9s} SVF: {v:.4f}")
41
+
42
+ # 3. Raster: burn the blocks into a 1 m height grid and search 32 directions.
43
+ dsm, transform = rasterize_buildings(blocks, (-60, -100, 60, 100), 1.0)
44
+ svf = sky_view_factor(dsm, 1.0, n_directions=32, max_radius=300.0, edge="nearest")
45
+ # Cell centre (0.5, -0.5), next to the centre line. Surfaces sit at cell
46
+ # centres, so the walls are effectively 21 m apart here: a little more sky.
47
+ row, col = 100, 60
48
+ print(f"raster isotropic SVF: {svf[row, col]:.4f} (grid {dsm.shape}, {transform[1]} m)")
49
+
50
+ # Street-canyon geometry along the centreline.
51
+ c = street_canyon([(0.0, -250.0), (0.0, 250.0)], blocks, spacing=25.0)
52
+ print(
53
+ f"street: W={c.width_mean:.1f} m H={c.height_mean:.1f} m H/W={c.hw_mean:.2f} "
54
+ f"bearing={c.bearing:.0f} deg enclosure={c.enclosure:.0%}"
55
+ )
56
+ print(f"mean SVF of the raster: {np.nanmean(svf):.3f}")
57
+ return 0
58
+
59
+
60
+ if __name__ == "__main__":
61
+ raise SystemExit(main())