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.
- skyview-0.1.0/CHANGELOG.md +20 -0
- skyview-0.1.0/CITATION.cff +20 -0
- skyview-0.1.0/LICENSE +21 -0
- skyview-0.1.0/MANIFEST.in +9 -0
- skyview-0.1.0/PKG-INFO +259 -0
- skyview-0.1.0/README.md +221 -0
- skyview-0.1.0/examples/quickstart.py +61 -0
- skyview-0.1.0/pyproject.toml +118 -0
- skyview-0.1.0/setup.cfg +4 -0
- skyview-0.1.0/src/skyview/__init__.py +99 -0
- skyview-0.1.0/src/skyview/__main__.py +7 -0
- skyview-0.1.0/src/skyview/_version.py +1 -0
- skyview-0.1.0/src/skyview/analytic.py +153 -0
- skyview-0.1.0/src/skyview/canyon.py +281 -0
- skyview-0.1.0/src/skyview/cli.py +212 -0
- skyview-0.1.0/src/skyview/io.py +212 -0
- skyview-0.1.0/src/skyview/py.typed +0 -0
- skyview-0.1.0/src/skyview/raster.py +269 -0
- skyview-0.1.0/src/skyview/vector.py +381 -0
- skyview-0.1.0/src/skyview.egg-info/PKG-INFO +259 -0
- skyview-0.1.0/src/skyview.egg-info/SOURCES.txt +28 -0
- skyview-0.1.0/src/skyview.egg-info/dependency_links.txt +1 -0
- skyview-0.1.0/src/skyview.egg-info/entry_points.txt +2 -0
- skyview-0.1.0/src/skyview.egg-info/requires.txt +10 -0
- skyview-0.1.0/src/skyview.egg-info/top_level.txt +1 -0
- skyview-0.1.0/tests/test_analytic.py +90 -0
- skyview-0.1.0/tests/test_canyon.py +137 -0
- skyview-0.1.0/tests/test_io_cli.py +179 -0
- skyview-0.1.0/tests/test_raster.py +169 -0
- skyview-0.1.0/tests/test_vector.py +190 -0
|
@@ -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.
|
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
|
+
[](https://pypi.org/project/skyview/)
|
|
42
|
+
[](https://pypi.org/project/skyview/)
|
|
43
|
+
[](https://github.com/YusufEminoglu/skyview_sdk/actions/workflows/ci.yml)
|
|
44
|
+
[](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).
|
skyview-0.1.0/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# skyview
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/skyview/)
|
|
4
|
+
[](https://pypi.org/project/skyview/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/skyview_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](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())
|