urbanshadow 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.
- urbanshadow-0.1.0/CHANGELOG.md +24 -0
- urbanshadow-0.1.0/CITATION.cff +21 -0
- urbanshadow-0.1.0/LICENSE +21 -0
- urbanshadow-0.1.0/MANIFEST.in +9 -0
- urbanshadow-0.1.0/PKG-INFO +293 -0
- urbanshadow-0.1.0/README.md +252 -0
- urbanshadow-0.1.0/examples/quickstart.py +41 -0
- urbanshadow-0.1.0/pyproject.toml +121 -0
- urbanshadow-0.1.0/setup.cfg +4 -0
- urbanshadow-0.1.0/src/urbanshadow/__init__.py +88 -0
- urbanshadow-0.1.0/src/urbanshadow/__main__.py +7 -0
- urbanshadow-0.1.0/src/urbanshadow/_version.py +1 -0
- urbanshadow-0.1.0/src/urbanshadow/buildings.py +367 -0
- urbanshadow-0.1.0/src/urbanshadow/cli.py +280 -0
- urbanshadow-0.1.0/src/urbanshadow/geometry.py +310 -0
- urbanshadow-0.1.0/src/urbanshadow/metrics.py +283 -0
- urbanshadow-0.1.0/src/urbanshadow/py.typed +0 -0
- urbanshadow-0.1.0/src/urbanshadow/scene.py +256 -0
- urbanshadow-0.1.0/src/urbanshadow/shadow.py +442 -0
- urbanshadow-0.1.0/src/urbanshadow/solar.py +353 -0
- urbanshadow-0.1.0/src/urbanshadow/sunhours.py +396 -0
- urbanshadow-0.1.0/src/urbanshadow/timeutil.py +132 -0
- urbanshadow-0.1.0/src/urbanshadow.egg-info/PKG-INFO +293 -0
- urbanshadow-0.1.0/src/urbanshadow.egg-info/SOURCES.txt +31 -0
- urbanshadow-0.1.0/src/urbanshadow.egg-info/dependency_links.txt +1 -0
- urbanshadow-0.1.0/src/urbanshadow.egg-info/entry_points.txt +2 -0
- urbanshadow-0.1.0/src/urbanshadow.egg-info/requires.txt +16 -0
- urbanshadow-0.1.0/src/urbanshadow.egg-info/top_level.txt +1 -0
- urbanshadow-0.1.0/tests/test_io_cli.py +307 -0
- urbanshadow-0.1.0/tests/test_metrics.py +141 -0
- urbanshadow-0.1.0/tests/test_shadow.py +251 -0
- urbanshadow-0.1.0/tests/test_solar.py +186 -0
- urbanshadow-0.1.0/tests/test_sunhours.py +177 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- Solar position after the NOAA Solar Calculator (Meeus low-accuracy solar coordinates,
|
|
10
|
+
equation of time, NOAA refraction model): azimuth, elevation, declination, hour angle;
|
|
11
|
+
sunrise, solar noon and sunset with polar day/night handling. Tested against values from
|
|
12
|
+
the NOAA calculator code and Meeus worked examples.
|
|
13
|
+
- `Building` prisms (footprint with holes, height, optional raised base) and a GeoJSON reader
|
|
14
|
+
with height / storey fields, lon/lat auto-detection and a WGS84 local tangent-plane
|
|
15
|
+
projection.
|
|
16
|
+
- Shadow polygons per building and time (footprint + translated footprint + edge sweeps;
|
|
17
|
+
convex hull for convex footprints), dissolved with shapely when installed.
|
|
18
|
+
- Point-in-shadow queries in pure Python and vectorised with numpy, for any receptor height.
|
|
19
|
+
- Sun hours at points, on ground grids (GeoJSON / CSV export) and on facades, for one day,
|
|
20
|
+
a date range or a local time window.
|
|
21
|
+
- Planning metrics: share of an open space in shadow at a time (exact with shapely or by
|
|
22
|
+
sampling), share of an area with at least N hours of sun, and the BRE 209 (2022)
|
|
23
|
+
two-hours-on-21-March garden test with the 0.8 x former-value check.
|
|
24
|
+
- `urbanshadow` command line: `sunpos`, `shadows`, `sunhours`, `bre`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "geospacephilo@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "urbanshadow"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/urbanshadow_sdk"
|
|
13
|
+
url: "https://pypi.org/project/urbanshadow/"
|
|
14
|
+
abstract: "Building shadow casting and sun-hours analysis from 2D footprints and heights: NOAA/Meeus solar position, exact shadow polygons of extruded footprints, vectorised point-in-shadow queries with a shapely-free fallback, sun-hours grids and facade sun, open-space shadow shares and the BRE 209 two-hours-of-sun-on-21-March test."
|
|
15
|
+
keywords:
|
|
16
|
+
- shadow analysis
|
|
17
|
+
- sun hours
|
|
18
|
+
- solar position
|
|
19
|
+
- urban planning
|
|
20
|
+
- BRE 209
|
|
21
|
+
- gis
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urbanshadow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Building shadow casting and sun-hours analysis from 2D footprints and heights: NOAA solar position, shadow polygons, sun-hours grids, facade sun and the BRE 209 two-hours-on-21-March test.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/urbanshadow_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/urbanshadow_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/urbanshadow_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: shadow,shadow-analysis,sun-hours,solar-position,noaa,daylight,sunlight,bre-209,urban-planning,buildings,geojson,gis,overshadowing
|
|
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: shapely
|
|
31
|
+
Requires-Dist: shapely>=2.0; extra == "shapely"
|
|
32
|
+
Provides-Extra: tz
|
|
33
|
+
Requires-Dist: tzdata; platform_system == "Windows" and extra == "tz"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: tzdata; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# urbanshadow
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/urbanshadow/)
|
|
45
|
+
[](https://pypi.org/project/urbanshadow/)
|
|
46
|
+
[](https://github.com/YusufEminoglu/urbanshadow_sdk/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/LICENSE)
|
|
48
|
+
|
|
49
|
+
**Building shadows and sun hours from 2D footprints and heights.**
|
|
50
|
+
|
|
51
|
+
`urbanshadow` answers the overshadowing questions that come up in urban
|
|
52
|
+
planning and design review, working from building footprints (GeoJSON) and
|
|
53
|
+
heights:
|
|
54
|
+
|
|
55
|
+
- **Where the sun is.** Azimuth, elevation, sunrise, solar noon and sunset
|
|
56
|
+
with the NOAA Solar Calculator algorithm (Meeus). The tests check it
|
|
57
|
+
against values from the NOAA calculator code.
|
|
58
|
+
- **Where the shadows fall.** One exact shadow polygon per building and time
|
|
59
|
+
step, and the union of all shadows at a time step.
|
|
60
|
+
- **How long a spot gets sun.** Hours of direct sun at points, on a ground
|
|
61
|
+
grid or on facades, for one day, a date range or a time window, with every
|
|
62
|
+
building acting as an obstacle.
|
|
63
|
+
- **Planning checks.** The share of an open space in shadow at given times.
|
|
64
|
+
The BRE 209 test: does at least half of a garden get at least
|
|
65
|
+
2 hours of sun on 21 March? If not, is the sunlit area below 0.8 times its
|
|
66
|
+
former value?
|
|
67
|
+
- **A command line tool** that turns a GeoJSON of buildings into GeoJSON
|
|
68
|
+
shadows, sun-hours grids (GeoJSON or CSV) or a BRE verdict.
|
|
69
|
+
|
|
70
|
+
numpy is the only required dependency. shapely is optional: with it you get
|
|
71
|
+
dissolved shadow polygons and exact area overlays. Without it, point-in-shadow
|
|
72
|
+
queries, sun hours and the area metrics still run, through vectorised numpy
|
|
73
|
+
or pure Python, so the package also works where shapely is hard to install.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install urbanshadow # numpy only
|
|
77
|
+
pip install "urbanshadow[shapely]" # + dissolved polygons, exact areas
|
|
78
|
+
pip install "urbanshadow[tz]" # + IANA time-zone data on Windows
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Requires Python 3.9 or newer. (The PyPI name `shadowcast` was already taken.)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Quick start (Python)
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from datetime import date, datetime
|
|
89
|
+
from urbanshadow import Building, Scene
|
|
90
|
+
|
|
91
|
+
buildings = [
|
|
92
|
+
Building.box(0, 0, 40, 12, 20, id="slab"), # x, y, width, depth, height (m)
|
|
93
|
+
Building.box(50, 0, 15, 30, 9, id="east"),
|
|
94
|
+
]
|
|
95
|
+
garden = [(0, 15), (40, 15), (40, 45), (0, 45)] # planar metres
|
|
96
|
+
|
|
97
|
+
scene = Scene(buildings, latitude=41.0082, longitude=28.9784, tz=3) # Istanbul, UTC+3
|
|
98
|
+
|
|
99
|
+
sun = scene.sun(datetime(2026, 3, 21, 12, 0))
|
|
100
|
+
print(sun.azimuth, sun.elevation) # 153.76, 46.23
|
|
101
|
+
|
|
102
|
+
for s in scene.shadows(datetime(2026, 3, 21, 12, 0)):
|
|
103
|
+
print(s.building.id, round(s.length, 1), round(s.area)) # slab 19.2 1269 ...
|
|
104
|
+
|
|
105
|
+
scene.shadow_fraction(garden, datetime(2026, 3, 21, 12, 0)) # share of garden in shadow
|
|
106
|
+
scene.sun_hours([20.0], [20.0], date(2026, 3, 21)) # array([3.0])
|
|
107
|
+
print(scene.bre_check(garden, year=2026).summary())
|
|
108
|
+
# 94.6% of the area receives >= 2 h of sun on 2026-03-21 (BR 209 target 50%): PASS
|
|
109
|
+
|
|
110
|
+
grid = scene.sun_hours_grid(date(2026, 3, 21), cell_size=2.0, margin=20.0)
|
|
111
|
+
grid.to_csv("sunhours.csv")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
From GeoJSON (lon/lat or a projected metric CRS):
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
scene = Scene.from_geojson("buildings.geojson", tz="Europe/Istanbul") # lon/lat input
|
|
118
|
+
scene = Scene.from_geojson("buildings_utm35n.geojson", latitude=41.01, longitude=28.98,
|
|
119
|
+
tz=3, crs="projected") # metric input
|
|
120
|
+
fc = scene.shadows_geojson([datetime(2026, 6, 21, h) for h in range(8, 19)])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
See [`examples/quickstart.py`](https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/examples/quickstart.py) for a runnable script.
|
|
124
|
+
|
|
125
|
+
## Quick start (command line)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Solar position and sun times
|
|
129
|
+
urbanshadow sunpos --lat 41.0082 --lon 28.9784 --time 2026-03-21T12:00 --tz 3
|
|
130
|
+
|
|
131
|
+
# Hourly shadow polygons, 08:00-18:00
|
|
132
|
+
urbanshadow shadows buildings.geojson --date 2026-03-21 --tz Europe/Istanbul \
|
|
133
|
+
--start 08:00 --end 18:00 --step 60 -o shadows.geojson
|
|
134
|
+
|
|
135
|
+
# One dissolved feature per time step (needs shapely)
|
|
136
|
+
urbanshadow shadows buildings.geojson --date 2026-12-21 --time 12:00 --dissolve -o noon.geojson
|
|
137
|
+
|
|
138
|
+
# Sun hours on a 2 m grid (GeoJSON squares, or .csv), mean of a date range
|
|
139
|
+
urbanshadow sunhours buildings.geojson --date 2026-06-01 --last-date 2026-06-30 --average \
|
|
140
|
+
--cell 2 --tz 3 -o june_sunhours.geojson
|
|
141
|
+
|
|
142
|
+
# BRE 209 garden test, proposed vs existing (exit code 1 if the area fails)
|
|
143
|
+
urbanshadow bre proposed.geojson --area garden.geojson --baseline existing.geojson --tz 0
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Common options: `--height-field`, `--levels-field` with `--level-height`
|
|
147
|
+
(default 3 m per storey), `--default-height`, `--crs auto|geographic|projected`,
|
|
148
|
+
`--lat/--lon` (required for projected input) and `--grid-rotation`.
|
|
149
|
+
|
|
150
|
+
## API overview
|
|
151
|
+
|
|
152
|
+
| Function / class | What it does |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `solar_position(when, lat, lon, tz=None, refraction=True)` | NOAA azimuth / elevation / declination / equation of time |
|
|
155
|
+
| `sun_times(day, lat, lon, tz)` | sunrise, solar noon, sunset (polar day/night aware) |
|
|
156
|
+
| `Building(exterior, height, holes=[], base=0)`, `Building.box(...)` | flat-roofed prism in planar metres |
|
|
157
|
+
| `buildings_from_geojson(src, ...)`, `read_buildings(path)` | GeoJSON to buildings (height / storeys fields, lon/lat detection) |
|
|
158
|
+
| `LocalProjection(lon0, lat0)` | WGS84 local tangent-plane projection (forward / inverse) |
|
|
159
|
+
| `building_shadow(b, az, el, z=0)` → `BuildingShadow` | exact shadow: `.parts`, `.contains()`, `.area`, `.to_shapely()`, `.geojson_geometry()` |
|
|
160
|
+
| `cast_shadows(...)`, `union_shadows(shadows)` | all shadows for one sun position; dissolved union (shapely) |
|
|
161
|
+
| `point_in_shadow(x, y, buildings, az, el, z=0)` | pure-Python point query |
|
|
162
|
+
| `shadow_mask(xs, ys, buildings, az, el, z=0)` | vectorised numpy point query |
|
|
163
|
+
| `sun_hours(xs, ys, buildings, lat, lon, day, last_day=None, ...)` | hours of direct sun at points |
|
|
164
|
+
| `sun_hours_grid(...)` → `SunHoursGrid` | ground grid; `.to_geojson()`, `.to_csv()`, `.fraction_at_least(h)` |
|
|
165
|
+
| `facade_sun_hours(...)` → `[FacadePoint]` | sun hours on walls (own building and neighbours as obstacles) |
|
|
166
|
+
| `shadow_fraction(area, buildings, az, el)` | share of an open space in shadow |
|
|
167
|
+
| `sunlit_area_fraction(area, buildings, lat, lon, day, min_hours=2)` | share of an area with at least N hours of sun |
|
|
168
|
+
| `bre_sun_on_ground(area, buildings, lat, lon, baseline_buildings=...)` → `BREResult` | BR 209 test |
|
|
169
|
+
| `Scene` | bundles buildings, location, time zone and map rotation behind the calls above |
|
|
170
|
+
|
|
171
|
+
Conventions: azimuth is measured in degrees clockwise from true north.
|
|
172
|
+
Elevation is the apparent elevation above the horizon, with refraction
|
|
173
|
+
included. Planar coordinates are metres, with x pointing east and y north.
|
|
174
|
+
A naive `datetime` is read in the scene's or the call's `tz`; an aware
|
|
175
|
+
`datetime` is used as it is. `tz` can be an hour offset (`3`, `-5.5`), an
|
|
176
|
+
offset string (`"+03:00"`) or an IANA name (`"Europe/London"`).
|
|
177
|
+
|
|
178
|
+
## Method and sources
|
|
179
|
+
|
|
180
|
+
**Solar position.** The package uses the equations of the NOAA Global
|
|
181
|
+
Monitoring Laboratory Solar Calculator
|
|
182
|
+
(<https://gml.noaa.gov/grad/solcalc/>, details at
|
|
183
|
+
<https://gml.noaa.gov/grad/solcalc/calcdetails.html>). That calculator
|
|
184
|
+
implements the low-accuracy solar coordinates from Jean Meeus,
|
|
185
|
+
*Astronomical Algorithms*, 2nd ed. (Willmann-Bell, 1998): the Julian Day
|
|
186
|
+
(ch. 7), the obliquity (ch. 22), the solar coordinates (ch. 25) and the
|
|
187
|
+
equation of time (ch. 28). Refraction follows NOAA's piecewise model, and
|
|
188
|
+
sunrise and sunset use a 0.833° solar depression. NOAA describes the method
|
|
189
|
+
as very accurate for 1800–2100 and sunrise/sunset as accurate to about one
|
|
190
|
+
minute within ±72° latitude.
|
|
191
|
+
|
|
192
|
+
The tests check six cases (both hemispheres, solstices, equinoxes, low sun)
|
|
193
|
+
against the functions of the NOAA calculator's own JavaScript (`main.js`), to
|
|
194
|
+
1e-6°. Sunrise and sunset agree to under 0.01 s. The tests also reproduce
|
|
195
|
+
Meeus Examples 7.a and 25.a. Solar noon differs from NOAA's by about 10 s,
|
|
196
|
+
because NOAA evaluates its second equation-of-time pass 12 hours early and
|
|
197
|
+
urbanshadow evaluates it at noon. Like the NOAA calculator, urbanshadow
|
|
198
|
+
ignores ΔT (TT − UT), which costs less than 0.01°.
|
|
199
|
+
|
|
200
|
+
**Shadow geometry.** A building is a vertical prism over footprint *P*,
|
|
201
|
+
running from height `base` to `height`, on flat ground. *w* is the unit
|
|
202
|
+
vector pointing away from the sun. A receptor at height *z* is shaded when
|
|
203
|
+
*p − s·w* lies inside *P* for some *s* between *L₁* and *L₂*, where
|
|
204
|
+
*L₁ = max(base − z, 0)/tan E* and *L₂ = (height − z)/tan E*. The shadow
|
|
205
|
+
polygon is therefore the Minkowski sum of *P* with the segment
|
|
206
|
+
[*L₁w*, *L₂w*]. That sum is the union of the two translated footprints and
|
|
207
|
+
the quadrilateral swept by each edge (holes included, so courtyards come out
|
|
208
|
+
right). For a convex footprint it reduces to the convex hull of the two
|
|
209
|
+
translated footprints. Point queries test the segment from *p − L₁w* to
|
|
210
|
+
*p − L₂w* against *P*: an end point inside *P*, or a proper crossing of an
|
|
211
|
+
edge. This needs no polygon union, so it runs in pure Python or vectorised
|
|
212
|
+
numpy. The raster approach of Ratti & Richens (2004, *Environment and
|
|
213
|
+
Planning B* 31(5), 743–757) answers the same question on a DEM.
|
|
214
|
+
|
|
215
|
+
**Sun hours.** Each day, or each local time window, is split into
|
|
216
|
+
`step_minutes` intervals (10 minutes by default) and integrated with the
|
|
217
|
+
midpoint rule. An interval counts as sunlit when the apparent sun is above
|
|
218
|
+
`min_elevation` and no prism blocks the ray. The tests compare the result
|
|
219
|
+
with a closed form for the equinox. On that day the sun's profile angle in
|
|
220
|
+
the north–south plane is a constant 90° − φ, so a point at distance *D* north
|
|
221
|
+
of a long east–west wall of height *H* is shaded all day when *D < H·tan φ*.
|
|
222
|
+
|
|
223
|
+
**Facades.** Receptors are placed along every wall and pushed 5 cm outwards.
|
|
224
|
+
A receptor is sunlit when the sun is in front of the wall and no building,
|
|
225
|
+
its own included, blocks the ray. Including the receptor's own building
|
|
226
|
+
handles L- and U-shaped blocks.
|
|
227
|
+
|
|
228
|
+
**BRE 209.** The test follows P. J. Littlefair, *Site layout planning for
|
|
229
|
+
daylight and sunlight: a guide to good practice* (BR 209, 3rd ed., BRE
|
|
230
|
+
Press, 2022, section 3.3, "Gardens and open spaces"). For a garden or
|
|
231
|
+
amenity area to appear adequately sunlit throughout the year, at least half
|
|
232
|
+
of it should receive at least two hours of sunlight on 21 March. If new
|
|
233
|
+
development leaves an existing area failing this, and the area that can get
|
|
234
|
+
two hours of sun on 21 March falls below 0.8 times its former value, the loss
|
|
235
|
+
of sunlight is likely to be noticeable. The 2 h, 50 % and 0.8 values are
|
|
236
|
+
parameters (`BRE_REQUIRED_HOURS`, `BRE_REQUIRED_FRACTION`,
|
|
237
|
+
`BRE_RATIO_THRESHOLD`) and can be overridden. BR 209 is advisory guidance,
|
|
238
|
+
not a regulation, and planning authorities apply it flexibly.
|
|
239
|
+
|
|
240
|
+
**Lon/lat input.** Coordinates are projected to a local tangent plane at the
|
|
241
|
+
layer centre, using the WGS84 meridional and prime-vertical radii of
|
|
242
|
+
curvature (Snyder, *Map Projections: A Working Manual*, USGS PP 1395,
|
|
243
|
+
1987). In this plane grid north is true north. The scale error is about
|
|
244
|
+
0.01–0.02 % at 1 km, which suits district-scale studies. Results are written
|
|
245
|
+
back in lon/lat.
|
|
246
|
+
|
|
247
|
+
## Limitations
|
|
248
|
+
|
|
249
|
+
- Flat ground, flat roofs and vertical walls (LOD1 blocks). Terrain, pitched
|
|
250
|
+
roofs, trees and other non-building obstacles are not modelled.
|
|
251
|
+
Translucent obstacles are treated as opaque.
|
|
252
|
+
- Only direct sun is computed. Diffuse daylight, VSC, APSH and the BRE
|
|
253
|
+
window tests are out of scope.
|
|
254
|
+
- In a projected CRS, map north can differ from true north (meridian
|
|
255
|
+
convergence, up to a few degrees in UTM). Pass `grid_rotation`, the
|
|
256
|
+
clockwise angle from grid north to true north, to correct for it.
|
|
257
|
+
- Without shapely, non-convex shadow polygons are exported as a MultiPolygon
|
|
258
|
+
of overlapping parts. Their union is exact, but they are not dissolved, and
|
|
259
|
+
`include_footprint=False` only affects point queries. Shadow areas of
|
|
260
|
+
non-convex footprints are then estimated on a 200 × 200 grid.
|
|
261
|
+
- Area shares and the BRE test are computed by sampling. Points inside
|
|
262
|
+
buildings are left out. The default spacing gives about 10 000 samples
|
|
263
|
+
per area.
|
|
264
|
+
- Sun hours use wall-clock steps. On a daylight-saving change day the
|
|
265
|
+
intervals follow local clock time.
|
|
266
|
+
- `crs="auto"` treats any layer whose coordinates fit lon/lat ranges and span
|
|
267
|
+
less than 5° as geographic. Pass `crs="projected"` for small local
|
|
268
|
+
engineering grids.
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
python -m pip install -e ".[dev,shapely]"
|
|
274
|
+
python -m pytest
|
|
275
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
276
|
+
python -m mypy src
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Academic citation
|
|
280
|
+
|
|
281
|
+
```bibtex
|
|
282
|
+
@software{eminoglu_urbanshadow,
|
|
283
|
+
author = {Eminoğlu, Yusuf},
|
|
284
|
+
title = {urbanshadow},
|
|
285
|
+
year = {2026},
|
|
286
|
+
version = {0.1.0},
|
|
287
|
+
url = {https://github.com/YusufEminoglu/urbanshadow_sdk}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT © 2026 Yusuf Eminoğlu. See [LICENSE](https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/LICENSE).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# urbanshadow
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/urbanshadow/)
|
|
4
|
+
[](https://pypi.org/project/urbanshadow/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/urbanshadow_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Building shadows and sun hours from 2D footprints and heights.**
|
|
9
|
+
|
|
10
|
+
`urbanshadow` answers the overshadowing questions that come up in urban
|
|
11
|
+
planning and design review, working from building footprints (GeoJSON) and
|
|
12
|
+
heights:
|
|
13
|
+
|
|
14
|
+
- **Where the sun is.** Azimuth, elevation, sunrise, solar noon and sunset
|
|
15
|
+
with the NOAA Solar Calculator algorithm (Meeus). The tests check it
|
|
16
|
+
against values from the NOAA calculator code.
|
|
17
|
+
- **Where the shadows fall.** One exact shadow polygon per building and time
|
|
18
|
+
step, and the union of all shadows at a time step.
|
|
19
|
+
- **How long a spot gets sun.** Hours of direct sun at points, on a ground
|
|
20
|
+
grid or on facades, for one day, a date range or a time window, with every
|
|
21
|
+
building acting as an obstacle.
|
|
22
|
+
- **Planning checks.** The share of an open space in shadow at given times.
|
|
23
|
+
The BRE 209 test: does at least half of a garden get at least
|
|
24
|
+
2 hours of sun on 21 March? If not, is the sunlit area below 0.8 times its
|
|
25
|
+
former value?
|
|
26
|
+
- **A command line tool** that turns a GeoJSON of buildings into GeoJSON
|
|
27
|
+
shadows, sun-hours grids (GeoJSON or CSV) or a BRE verdict.
|
|
28
|
+
|
|
29
|
+
numpy is the only required dependency. shapely is optional: with it you get
|
|
30
|
+
dissolved shadow polygons and exact area overlays. Without it, point-in-shadow
|
|
31
|
+
queries, sun hours and the area metrics still run, through vectorised numpy
|
|
32
|
+
or pure Python, so the package also works where shapely is hard to install.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install urbanshadow # numpy only
|
|
36
|
+
pip install "urbanshadow[shapely]" # + dissolved polygons, exact areas
|
|
37
|
+
pip install "urbanshadow[tz]" # + IANA time-zone data on Windows
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.9 or newer. (The PyPI name `shadowcast` was already taken.)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick start (Python)
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from datetime import date, datetime
|
|
48
|
+
from urbanshadow import Building, Scene
|
|
49
|
+
|
|
50
|
+
buildings = [
|
|
51
|
+
Building.box(0, 0, 40, 12, 20, id="slab"), # x, y, width, depth, height (m)
|
|
52
|
+
Building.box(50, 0, 15, 30, 9, id="east"),
|
|
53
|
+
]
|
|
54
|
+
garden = [(0, 15), (40, 15), (40, 45), (0, 45)] # planar metres
|
|
55
|
+
|
|
56
|
+
scene = Scene(buildings, latitude=41.0082, longitude=28.9784, tz=3) # Istanbul, UTC+3
|
|
57
|
+
|
|
58
|
+
sun = scene.sun(datetime(2026, 3, 21, 12, 0))
|
|
59
|
+
print(sun.azimuth, sun.elevation) # 153.76, 46.23
|
|
60
|
+
|
|
61
|
+
for s in scene.shadows(datetime(2026, 3, 21, 12, 0)):
|
|
62
|
+
print(s.building.id, round(s.length, 1), round(s.area)) # slab 19.2 1269 ...
|
|
63
|
+
|
|
64
|
+
scene.shadow_fraction(garden, datetime(2026, 3, 21, 12, 0)) # share of garden in shadow
|
|
65
|
+
scene.sun_hours([20.0], [20.0], date(2026, 3, 21)) # array([3.0])
|
|
66
|
+
print(scene.bre_check(garden, year=2026).summary())
|
|
67
|
+
# 94.6% of the area receives >= 2 h of sun on 2026-03-21 (BR 209 target 50%): PASS
|
|
68
|
+
|
|
69
|
+
grid = scene.sun_hours_grid(date(2026, 3, 21), cell_size=2.0, margin=20.0)
|
|
70
|
+
grid.to_csv("sunhours.csv")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
From GeoJSON (lon/lat or a projected metric CRS):
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
scene = Scene.from_geojson("buildings.geojson", tz="Europe/Istanbul") # lon/lat input
|
|
77
|
+
scene = Scene.from_geojson("buildings_utm35n.geojson", latitude=41.01, longitude=28.98,
|
|
78
|
+
tz=3, crs="projected") # metric input
|
|
79
|
+
fc = scene.shadows_geojson([datetime(2026, 6, 21, h) for h in range(8, 19)])
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
See [`examples/quickstart.py`](https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/examples/quickstart.py) for a runnable script.
|
|
83
|
+
|
|
84
|
+
## Quick start (command line)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Solar position and sun times
|
|
88
|
+
urbanshadow sunpos --lat 41.0082 --lon 28.9784 --time 2026-03-21T12:00 --tz 3
|
|
89
|
+
|
|
90
|
+
# Hourly shadow polygons, 08:00-18:00
|
|
91
|
+
urbanshadow shadows buildings.geojson --date 2026-03-21 --tz Europe/Istanbul \
|
|
92
|
+
--start 08:00 --end 18:00 --step 60 -o shadows.geojson
|
|
93
|
+
|
|
94
|
+
# One dissolved feature per time step (needs shapely)
|
|
95
|
+
urbanshadow shadows buildings.geojson --date 2026-12-21 --time 12:00 --dissolve -o noon.geojson
|
|
96
|
+
|
|
97
|
+
# Sun hours on a 2 m grid (GeoJSON squares, or .csv), mean of a date range
|
|
98
|
+
urbanshadow sunhours buildings.geojson --date 2026-06-01 --last-date 2026-06-30 --average \
|
|
99
|
+
--cell 2 --tz 3 -o june_sunhours.geojson
|
|
100
|
+
|
|
101
|
+
# BRE 209 garden test, proposed vs existing (exit code 1 if the area fails)
|
|
102
|
+
urbanshadow bre proposed.geojson --area garden.geojson --baseline existing.geojson --tz 0
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Common options: `--height-field`, `--levels-field` with `--level-height`
|
|
106
|
+
(default 3 m per storey), `--default-height`, `--crs auto|geographic|projected`,
|
|
107
|
+
`--lat/--lon` (required for projected input) and `--grid-rotation`.
|
|
108
|
+
|
|
109
|
+
## API overview
|
|
110
|
+
|
|
111
|
+
| Function / class | What it does |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `solar_position(when, lat, lon, tz=None, refraction=True)` | NOAA azimuth / elevation / declination / equation of time |
|
|
114
|
+
| `sun_times(day, lat, lon, tz)` | sunrise, solar noon, sunset (polar day/night aware) |
|
|
115
|
+
| `Building(exterior, height, holes=[], base=0)`, `Building.box(...)` | flat-roofed prism in planar metres |
|
|
116
|
+
| `buildings_from_geojson(src, ...)`, `read_buildings(path)` | GeoJSON to buildings (height / storeys fields, lon/lat detection) |
|
|
117
|
+
| `LocalProjection(lon0, lat0)` | WGS84 local tangent-plane projection (forward / inverse) |
|
|
118
|
+
| `building_shadow(b, az, el, z=0)` → `BuildingShadow` | exact shadow: `.parts`, `.contains()`, `.area`, `.to_shapely()`, `.geojson_geometry()` |
|
|
119
|
+
| `cast_shadows(...)`, `union_shadows(shadows)` | all shadows for one sun position; dissolved union (shapely) |
|
|
120
|
+
| `point_in_shadow(x, y, buildings, az, el, z=0)` | pure-Python point query |
|
|
121
|
+
| `shadow_mask(xs, ys, buildings, az, el, z=0)` | vectorised numpy point query |
|
|
122
|
+
| `sun_hours(xs, ys, buildings, lat, lon, day, last_day=None, ...)` | hours of direct sun at points |
|
|
123
|
+
| `sun_hours_grid(...)` → `SunHoursGrid` | ground grid; `.to_geojson()`, `.to_csv()`, `.fraction_at_least(h)` |
|
|
124
|
+
| `facade_sun_hours(...)` → `[FacadePoint]` | sun hours on walls (own building and neighbours as obstacles) |
|
|
125
|
+
| `shadow_fraction(area, buildings, az, el)` | share of an open space in shadow |
|
|
126
|
+
| `sunlit_area_fraction(area, buildings, lat, lon, day, min_hours=2)` | share of an area with at least N hours of sun |
|
|
127
|
+
| `bre_sun_on_ground(area, buildings, lat, lon, baseline_buildings=...)` → `BREResult` | BR 209 test |
|
|
128
|
+
| `Scene` | bundles buildings, location, time zone and map rotation behind the calls above |
|
|
129
|
+
|
|
130
|
+
Conventions: azimuth is measured in degrees clockwise from true north.
|
|
131
|
+
Elevation is the apparent elevation above the horizon, with refraction
|
|
132
|
+
included. Planar coordinates are metres, with x pointing east and y north.
|
|
133
|
+
A naive `datetime` is read in the scene's or the call's `tz`; an aware
|
|
134
|
+
`datetime` is used as it is. `tz` can be an hour offset (`3`, `-5.5`), an
|
|
135
|
+
offset string (`"+03:00"`) or an IANA name (`"Europe/London"`).
|
|
136
|
+
|
|
137
|
+
## Method and sources
|
|
138
|
+
|
|
139
|
+
**Solar position.** The package uses the equations of the NOAA Global
|
|
140
|
+
Monitoring Laboratory Solar Calculator
|
|
141
|
+
(<https://gml.noaa.gov/grad/solcalc/>, details at
|
|
142
|
+
<https://gml.noaa.gov/grad/solcalc/calcdetails.html>). That calculator
|
|
143
|
+
implements the low-accuracy solar coordinates from Jean Meeus,
|
|
144
|
+
*Astronomical Algorithms*, 2nd ed. (Willmann-Bell, 1998): the Julian Day
|
|
145
|
+
(ch. 7), the obliquity (ch. 22), the solar coordinates (ch. 25) and the
|
|
146
|
+
equation of time (ch. 28). Refraction follows NOAA's piecewise model, and
|
|
147
|
+
sunrise and sunset use a 0.833° solar depression. NOAA describes the method
|
|
148
|
+
as very accurate for 1800–2100 and sunrise/sunset as accurate to about one
|
|
149
|
+
minute within ±72° latitude.
|
|
150
|
+
|
|
151
|
+
The tests check six cases (both hemispheres, solstices, equinoxes, low sun)
|
|
152
|
+
against the functions of the NOAA calculator's own JavaScript (`main.js`), to
|
|
153
|
+
1e-6°. Sunrise and sunset agree to under 0.01 s. The tests also reproduce
|
|
154
|
+
Meeus Examples 7.a and 25.a. Solar noon differs from NOAA's by about 10 s,
|
|
155
|
+
because NOAA evaluates its second equation-of-time pass 12 hours early and
|
|
156
|
+
urbanshadow evaluates it at noon. Like the NOAA calculator, urbanshadow
|
|
157
|
+
ignores ΔT (TT − UT), which costs less than 0.01°.
|
|
158
|
+
|
|
159
|
+
**Shadow geometry.** A building is a vertical prism over footprint *P*,
|
|
160
|
+
running from height `base` to `height`, on flat ground. *w* is the unit
|
|
161
|
+
vector pointing away from the sun. A receptor at height *z* is shaded when
|
|
162
|
+
*p − s·w* lies inside *P* for some *s* between *L₁* and *L₂*, where
|
|
163
|
+
*L₁ = max(base − z, 0)/tan E* and *L₂ = (height − z)/tan E*. The shadow
|
|
164
|
+
polygon is therefore the Minkowski sum of *P* with the segment
|
|
165
|
+
[*L₁w*, *L₂w*]. That sum is the union of the two translated footprints and
|
|
166
|
+
the quadrilateral swept by each edge (holes included, so courtyards come out
|
|
167
|
+
right). For a convex footprint it reduces to the convex hull of the two
|
|
168
|
+
translated footprints. Point queries test the segment from *p − L₁w* to
|
|
169
|
+
*p − L₂w* against *P*: an end point inside *P*, or a proper crossing of an
|
|
170
|
+
edge. This needs no polygon union, so it runs in pure Python or vectorised
|
|
171
|
+
numpy. The raster approach of Ratti & Richens (2004, *Environment and
|
|
172
|
+
Planning B* 31(5), 743–757) answers the same question on a DEM.
|
|
173
|
+
|
|
174
|
+
**Sun hours.** Each day, or each local time window, is split into
|
|
175
|
+
`step_minutes` intervals (10 minutes by default) and integrated with the
|
|
176
|
+
midpoint rule. An interval counts as sunlit when the apparent sun is above
|
|
177
|
+
`min_elevation` and no prism blocks the ray. The tests compare the result
|
|
178
|
+
with a closed form for the equinox. On that day the sun's profile angle in
|
|
179
|
+
the north–south plane is a constant 90° − φ, so a point at distance *D* north
|
|
180
|
+
of a long east–west wall of height *H* is shaded all day when *D < H·tan φ*.
|
|
181
|
+
|
|
182
|
+
**Facades.** Receptors are placed along every wall and pushed 5 cm outwards.
|
|
183
|
+
A receptor is sunlit when the sun is in front of the wall and no building,
|
|
184
|
+
its own included, blocks the ray. Including the receptor's own building
|
|
185
|
+
handles L- and U-shaped blocks.
|
|
186
|
+
|
|
187
|
+
**BRE 209.** The test follows P. J. Littlefair, *Site layout planning for
|
|
188
|
+
daylight and sunlight: a guide to good practice* (BR 209, 3rd ed., BRE
|
|
189
|
+
Press, 2022, section 3.3, "Gardens and open spaces"). For a garden or
|
|
190
|
+
amenity area to appear adequately sunlit throughout the year, at least half
|
|
191
|
+
of it should receive at least two hours of sunlight on 21 March. If new
|
|
192
|
+
development leaves an existing area failing this, and the area that can get
|
|
193
|
+
two hours of sun on 21 March falls below 0.8 times its former value, the loss
|
|
194
|
+
of sunlight is likely to be noticeable. The 2 h, 50 % and 0.8 values are
|
|
195
|
+
parameters (`BRE_REQUIRED_HOURS`, `BRE_REQUIRED_FRACTION`,
|
|
196
|
+
`BRE_RATIO_THRESHOLD`) and can be overridden. BR 209 is advisory guidance,
|
|
197
|
+
not a regulation, and planning authorities apply it flexibly.
|
|
198
|
+
|
|
199
|
+
**Lon/lat input.** Coordinates are projected to a local tangent plane at the
|
|
200
|
+
layer centre, using the WGS84 meridional and prime-vertical radii of
|
|
201
|
+
curvature (Snyder, *Map Projections: A Working Manual*, USGS PP 1395,
|
|
202
|
+
1987). In this plane grid north is true north. The scale error is about
|
|
203
|
+
0.01–0.02 % at 1 km, which suits district-scale studies. Results are written
|
|
204
|
+
back in lon/lat.
|
|
205
|
+
|
|
206
|
+
## Limitations
|
|
207
|
+
|
|
208
|
+
- Flat ground, flat roofs and vertical walls (LOD1 blocks). Terrain, pitched
|
|
209
|
+
roofs, trees and other non-building obstacles are not modelled.
|
|
210
|
+
Translucent obstacles are treated as opaque.
|
|
211
|
+
- Only direct sun is computed. Diffuse daylight, VSC, APSH and the BRE
|
|
212
|
+
window tests are out of scope.
|
|
213
|
+
- In a projected CRS, map north can differ from true north (meridian
|
|
214
|
+
convergence, up to a few degrees in UTM). Pass `grid_rotation`, the
|
|
215
|
+
clockwise angle from grid north to true north, to correct for it.
|
|
216
|
+
- Without shapely, non-convex shadow polygons are exported as a MultiPolygon
|
|
217
|
+
of overlapping parts. Their union is exact, but they are not dissolved, and
|
|
218
|
+
`include_footprint=False` only affects point queries. Shadow areas of
|
|
219
|
+
non-convex footprints are then estimated on a 200 × 200 grid.
|
|
220
|
+
- Area shares and the BRE test are computed by sampling. Points inside
|
|
221
|
+
buildings are left out. The default spacing gives about 10 000 samples
|
|
222
|
+
per area.
|
|
223
|
+
- Sun hours use wall-clock steps. On a daylight-saving change day the
|
|
224
|
+
intervals follow local clock time.
|
|
225
|
+
- `crs="auto"` treats any layer whose coordinates fit lon/lat ranges and span
|
|
226
|
+
less than 5° as geographic. Pass `crs="projected"` for small local
|
|
227
|
+
engineering grids.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
python -m pip install -e ".[dev,shapely]"
|
|
233
|
+
python -m pytest
|
|
234
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
235
|
+
python -m mypy src
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Academic citation
|
|
239
|
+
|
|
240
|
+
```bibtex
|
|
241
|
+
@software{eminoglu_urbanshadow,
|
|
242
|
+
author = {Eminoğlu, Yusuf},
|
|
243
|
+
title = {urbanshadow},
|
|
244
|
+
year = {2026},
|
|
245
|
+
version = {0.1.0},
|
|
246
|
+
url = {https://github.com/YusufEminoglu/urbanshadow_sdk}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT © 2026 Yusuf Eminoğlu. See [LICENSE](https://github.com/YusufEminoglu/urbanshadow_sdk/blob/main/LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""urbanshadow quick start: a small block in Istanbul on the March equinox.
|
|
2
|
+
|
|
3
|
+
Run from a clone: python examples/quickstart.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import date, datetime
|
|
9
|
+
|
|
10
|
+
from urbanshadow import Building, Scene
|
|
11
|
+
|
|
12
|
+
# Planar coordinates in metres (x east, y north); a 20 m slab south of a courtyard
|
|
13
|
+
# garden and a lower block to the east.
|
|
14
|
+
buildings = [
|
|
15
|
+
Building.box(0, 0, 40, 12, 20, id="slab"),
|
|
16
|
+
Building.box(50, 0, 15, 30, 9, id="east"),
|
|
17
|
+
]
|
|
18
|
+
garden = [(0, 15), (40, 15), (40, 45), (0, 45)]
|
|
19
|
+
|
|
20
|
+
scene = Scene(buildings, latitude=41.0082, longitude=28.9784, tz=3) # Istanbul, UTC+3
|
|
21
|
+
|
|
22
|
+
when = datetime(2026, 3, 21, 12, 0)
|
|
23
|
+
sun = scene.sun(when)
|
|
24
|
+
print(f"Sun at {when:%Y-%m-%d %H:%M}: azimuth {sun.azimuth:.2f}, elevation {sun.elevation:.2f}")
|
|
25
|
+
times = scene.sun_times(date(2026, 3, 21))
|
|
26
|
+
print(f"Sunrise {times.sunrise:%H:%M}, solar noon {times.solar_noon:%H:%M}, sunset {times.sunset:%H:%M}")
|
|
27
|
+
|
|
28
|
+
for s in scene.shadows(when):
|
|
29
|
+
print(f" {s.building.id}: shadow length {s.length:.1f} m, area {s.area:.0f} m2")
|
|
30
|
+
|
|
31
|
+
share = scene.shadow_fraction(garden, when, method="grid", resolution=0.5)
|
|
32
|
+
print(f"Garden in shadow at noon: {share:.0%}")
|
|
33
|
+
|
|
34
|
+
hours = scene.sun_hours([20.0, 20.0], [20.0, 40.0], date(2026, 3, 21))
|
|
35
|
+
print(f"Sun hours on 21 March at (20, 20): {hours[0]:.1f} h, at (20, 40): {hours[1]:.1f} h")
|
|
36
|
+
|
|
37
|
+
result = scene.bre_check(garden, year=2026, resolution=1.0)
|
|
38
|
+
print(result.summary())
|
|
39
|
+
|
|
40
|
+
grid = scene.sun_hours_grid(date(2026, 3, 21), cell_size=2.0, margin=20.0)
|
|
41
|
+
print(f"Grid {grid.hours.shape}: {grid.fraction_at_least(2.0):.0%} of open cells get >= 2 h")
|