isovist 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. isovist-0.1.0/CHANGELOG.md +22 -0
  2. isovist-0.1.0/CITATION.cff +20 -0
  3. isovist-0.1.0/LICENSE +21 -0
  4. isovist-0.1.0/MANIFEST.in +9 -0
  5. isovist-0.1.0/PKG-INFO +259 -0
  6. isovist-0.1.0/README.md +223 -0
  7. isovist-0.1.0/examples/plaza.geojson +104 -0
  8. isovist-0.1.0/examples/plaza_boundary.geojson +38 -0
  9. isovist-0.1.0/examples/quickstart.py +72 -0
  10. isovist-0.1.0/pyproject.toml +109 -0
  11. isovist-0.1.0/setup.cfg +4 -0
  12. isovist-0.1.0/src/isovist/__init__.py +60 -0
  13. isovist-0.1.0/src/isovist/__main__.py +7 -0
  14. isovist-0.1.0/src/isovist/_version.py +1 -0
  15. isovist-0.1.0/src/isovist/cli.py +145 -0
  16. isovist-0.1.0/src/isovist/core.py +507 -0
  17. isovist-0.1.0/src/isovist/environment.py +304 -0
  18. isovist-0.1.0/src/isovist/geometry.py +171 -0
  19. isovist-0.1.0/src/isovist/io.py +182 -0
  20. isovist-0.1.0/src/isovist/measures.py +133 -0
  21. isovist-0.1.0/src/isovist/py.typed +0 -0
  22. isovist-0.1.0/src/isovist/vga.py +200 -0
  23. isovist-0.1.0/src/isovist.egg-info/PKG-INFO +259 -0
  24. isovist-0.1.0/src/isovist.egg-info/SOURCES.txt +33 -0
  25. isovist-0.1.0/src/isovist.egg-info/dependency_links.txt +1 -0
  26. isovist-0.1.0/src/isovist.egg-info/entry_points.txt +2 -0
  27. isovist-0.1.0/src/isovist.egg-info/requires.txt +7 -0
  28. isovist-0.1.0/src/isovist.egg-info/top_level.txt +1 -0
  29. isovist-0.1.0/tests/conftest.py +24 -0
  30. isovist-0.1.0/tests/test_geometry.py +35 -0
  31. isovist-0.1.0/tests/test_io_cli.py +225 -0
  32. isovist-0.1.0/tests/test_isovist.py +295 -0
  33. isovist-0.1.0/tests/test_measures.py +102 -0
  34. isovist-0.1.0/tests/test_vga.py +128 -0
  35. isovist-0.1.0/tests/test_visibility.py +103 -0
@@ -0,0 +1,22 @@
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
+ - `Environment` of obstacle polygons (with holes, overlaps allowed) and an optional
10
+ enclosing boundary; exact point location with a data-scaled tolerance.
11
+ - Exact isovist polygon by an angular sweep over critical directions (edge endpoints,
12
+ polygon crossings, view-distance circle crossings, field-of-view limits) with exact
13
+ ray/edge-line intersections; optional `max_radius`, `fov` and `direction`.
14
+ - Isovist measures with closed-form integrals: area, perimeter, compactness,
15
+ jaggedness, occlusivity and number of occluding edges, drift (distance and angle),
16
+ min/max/mean/std radial length.
17
+ - Exact point-to-point intervisibility (corner grazing allowed, interior crossings
18
+ blocked) and a vectorised intervisibility matrix.
19
+ - Visibility graph analysis on grids or explicit points: connectivity, clustering
20
+ coefficient, mean depth, relative asymmetry and Hillier-Hanson integration.
21
+ - GeoJSON input/output for projected coordinates; `isovist` command line tool with
22
+ `compute`, `vga` and `visible` sub-commands.
@@ -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: "isovist"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/isovist_sdk"
13
+ url: "https://pypi.org/project/isovist/"
14
+ abstract: "Exact 2D isovists from a viewpoint among obstacle polygons (angular sweep with exact edge intersections, optional view distance and field of view), isovist shape measures (area, perimeter, compactness, occlusivity, drift, radial statistics, jaggedness), exact point-to-point intervisibility and visibility graph analysis (connectivity, clustering coefficient, mean depth, integration), with GeoJSON input/output and a command-line tool."
15
+ keywords:
16
+ - isovist
17
+ - visibility graph analysis
18
+ - space syntax
19
+ - urban design
20
+ - gis
isovist-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/isovist/py.typed
7
+ recursive-include src/isovist *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py *.geojson
isovist-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: isovist
3
+ Version: 0.1.0
4
+ Summary: Exact 2D isovists, isovist shape measures, point-to-point intervisibility and visibility graph analysis (VGA) for urban design, with GeoJSON in/out and a CLI.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/isovist_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/isovist_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/isovist_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/isovist_sdk/blob/main/CHANGELOG.md
11
+ Keywords: isovist,visibility,visibility-graph-analysis,vga,space-syntax,urban-design,architecture,intervisibility,viewshed,geojson,gis,computational-geometry
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 :: Mathematics
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: dev
31
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
34
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # isovist
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/isovist.svg)](https://pypi.org/project/isovist/)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/isovist.svg)](https://pypi.org/project/isovist/)
41
+ [![CI](https://github.com/YusufEminoglu/isovist_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/isovist_sdk/actions/workflows/ci.yml)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/isovist_sdk/blob/main/LICENSE)
43
+
44
+ **Exact 2D isovists and visibility graph analysis for urban design.**
45
+
46
+ `isovist` computes what can be seen from a point in a plan of buildings, walls
47
+ and columns, and describes the shape of that visible space with the classic
48
+ isovist measures. It also runs visibility graph analysis (VGA) on a grid of
49
+ points and answers point-to-point "can A see B?" questions. The only dependency
50
+ is NumPy.
51
+
52
+ - **Exact isovist polygon** from an angular sweep with exact ray/edge
53
+ intersections, not a fan of sampled rays. Optional maximum view distance and
54
+ field of view (angle and direction).
55
+ - **Isovist measures**: area, perimeter, compactness, jaggedness, occlusivity,
56
+ number of occluding edges, drift (distance and direction) and min/max/mean/std
57
+ radial length. All of them come from closed-form integrals.
58
+ - **Intervisibility**: exact; a sight line may graze a corner or run along a
59
+ wall, but it may not cross an obstacle.
60
+ - **Visibility graph analysis**: connectivity, clustering coefficient, mean
61
+ depth, relative asymmetry and integration on grids or on your own points.
62
+ An optional visual radius limits the links.
63
+ - **GeoJSON in/out** in projected coordinates, plus an `isovist` command-line tool.
64
+
65
+ ```bash
66
+ pip install isovist
67
+ ```
68
+
69
+ Requires Python 3.9 or newer.
70
+
71
+ ---
72
+
73
+ ## Quick start (Python)
74
+
75
+ ```python
76
+ from isovist import Environment
77
+
78
+ room = [(0, 0), (10, 0), (10, 10), (0, 10)]
79
+ pillar = [(4, 4), (6, 4), (6, 6), (4, 6)]
80
+ env = Environment(obstacles=[pillar], boundary=room)
81
+
82
+ iso = env.isovist((2, 5))
83
+ iso.area # 70.0 (100 - pillar 4 - shadow 26)
84
+ iso.occlusivity # 13.416 (two occluding edges of length sqrt(45))
85
+ iso.polygon # [(x, y), ...] counter-clockwise ring
86
+
87
+ m = iso.measures()
88
+ m.compactness, m.drift, m.mean_radial, m.max_radial
89
+
90
+ env.isovist((2, 5), fov=90, direction=0, max_radius=6) # 90 deg view cone, 6 m reach
91
+ env.is_visible((3, 5), (7, 5)) # False: the pillar is in the way
92
+
93
+ from isovist import visibility_graph_analysis
94
+ vga = visibility_graph_analysis(env, spacing=1.0)
95
+ vga.connectivity, vga.clustering, vga.mean_depth, vga.integration
96
+ ```
97
+
98
+ Obstacles and the boundary are `isovist.Polygon` objects (holes allowed) or plain
99
+ vertex lists. Obstacles may overlap each other and may stick out of the boundary.
100
+ Holes in the boundary act as obstacles. The viewpoint must lie strictly in free
101
+ space.
102
+
103
+ ## Quick start (command line)
104
+
105
+ ```bash
106
+ # isovists with all measures for two viewpoints and for every Point in a file
107
+ isovist compute --obstacles buildings.geojson --boundary site.geojson \
108
+ --at 512340.5,4512200 --points viewpoints.geojson -o isovists.geojson
109
+
110
+ # 120 degree view cone towards north-east, 80 m reach
111
+ isovist compute --obstacles buildings.geojson --at 512340.5,4512200 \
112
+ --fov 120 --direction 45 --max-radius 80
113
+
114
+ # VGA on a 2 m grid (writes grid points with the measures)
115
+ isovist vga --obstacles buildings.geojson --boundary site.geojson --spacing 2 -o vga.geojson
116
+
117
+ # intervisibility; exit code 0 = visible, 1 = not visible
118
+ isovist visible --obstacles buildings.geojson --from 10,20 --to 80,45
119
+ ```
120
+
121
+ Write negative coordinates as `--at=-12.5,40` so they are not read as options.
122
+ Without `--boundary`, open-space isovists need `--max-radius`.
123
+
124
+ See `examples/quickstart.py` for a small plaza with two pavilions and a column.
125
+
126
+ ## Coordinates and GeoJSON
127
+
128
+ All geometry is planar, so coordinates must be **projected** (metres or feet),
129
+ for example UTM. RFC 7946 GeoJSON is nominally longitude/latitude. `isovist`
130
+ reads and writes the same structure with projected coordinates, as produced by
131
+ `ogr2ogr -t_srs EPSG:32635 -lco RFC7946=NO out.geojson in.gpkg`. A legacy
132
+ `crs` member in the input is copied to the output. `load_environment` warns when
133
+ the coordinates look like degrees.
134
+
135
+ Outputs: `isovist compute` writes Polygon features with the viewpoint
136
+ (`vp_x`, `vp_y`), the input point properties and every measure. `isovist vga`
137
+ writes Point features with `connectivity`, `clustering`, `mean_depth`,
138
+ `relative_asymmetry`, `integration` and `node_count`. Undefined values
139
+ (`nan`, `inf`) become `null`.
140
+
141
+ ## API overview
142
+
143
+ | Object / function | Purpose |
144
+ |---|---|
145
+ | `Environment(obstacles, boundary=None, tol=None)` | the plan; `.isovist()`, `.is_visible()`, `.visibility_matrix()`, `.locate()` |
146
+ | `Environment.isovist(vp, max_radius=None, fov=None, direction=0, arc_step=1)` | exact `Isovist` |
147
+ | `Isovist` | `.polygon`, `.area`, `.perimeter`, `.centroid`, `.occlusivity`, `.occluding_edges`, `.radial(theta)`, `.contains(pt)`, `.measures()`, `.to_geojson()` |
148
+ | `IsovistMeasures` | area, perimeter, compactness, jaggedness, occlusivity, occluding_edges, drift, drift_angle, min/max/mean/std_radial |
149
+ | `visibility_graph_analysis(env, points=None, spacing=None, max_radius=None)` | `VGAResult` with per-node arrays and `.records()` |
150
+ | `grid_points(env, spacing, bounds=None, origin=None)` | cell-centre grid in free space |
151
+ | `graph_measures(adjacency)` | VGA measures for any undirected graph |
152
+ | `load_environment`, `polygons_from_geojson`, `points_from_geojson`, `feature_collection`, `write_geojson` | GeoJSON I/O |
153
+
154
+ Angles are in degrees, counted counter-clockwise from east (+x), in both the
155
+ API and the CLI. `Isovist.radial` takes radians.
156
+
157
+ ## Method and sources
158
+
159
+ **Isovist.** An isovist is the set of points visible from a vantage point
160
+ (Benedikt 1979). Every edge end point, every crossing between overlapping
161
+ polygons, every point where an edge meets the view-distance circle and each
162
+ field-of-view limit gives a *critical direction*. Between two consecutive
163
+ critical directions the nearest edge cannot change, so one ray at the mid-angle
164
+ finds it. The isovist boundary on that interval is then the exact intersection
165
+ of the two bounding rays with that edge's line, or a circular arc of radius
166
+ `max_radius`. Edges face the viewer only when the viewpoint lies on their open
167
+ side, so back faces are culled. Only arcs are discretised, and only for the
168
+ output polygon (`arc_step`). Area, perimeter, centroid and radial statistics use
169
+ exact sector formulas.
170
+
171
+ **Measures.** With `A` the area, `P` the perimeter and `r(θ)` the radial length:
172
+
173
+ | Measure | Definition | Source |
174
+ |---|---|---|
175
+ | area, perimeter | `A`, `P` | Benedikt 1979 |
176
+ | occlusivity | total length of the radial occluding edges (boundary that is not a surface). View-cone sides and view-distance arcs are not counted. | Benedikt 1979 |
177
+ | compactness | `4πA / P²` (1 for a circle) | circularity form used in isovist studies after Benedikt 1979; Batty 2001 |
178
+ | jaggedness | `P² / A` (4π for a circle) | Wiener et al. 2007 |
179
+ | drift, drift_angle | distance and direction from the viewpoint to the isovist centroid | Turner et al. 2001; Batty 2001 |
180
+ | min/max/mean/std radial | moments of `r(θ)` over the visible angle, as continuous integrals | Benedikt 1979 (radial moments); Batty 2001 |
181
+
182
+ For a straight piece at perpendicular distance `h`, with tangential coordinate
183
+ `τ`: `∫ r dθ = h ln((r₁+τ₁)/(r₀+τ₀))` and `∫ r² dθ = h (τ₁ − τ₀)`.
184
+
185
+ **VGA** (Turner et al. 2001). Nodes are grid points in free space. Two nodes
186
+ are linked when they can see each other (both ways, by the same exact rule as
187
+ `is_visible`). For each node:
188
+
189
+ - *connectivity* is the number of linked nodes.
190
+ - *clustering coefficient* is the number of links among the node's neighbours
191
+ divided by `k(k−1)/2` (Watts & Strogatz 1998).
192
+ - *mean depth* is `MD = Σ depth / (k−1)` over the node's connected component of
193
+ `k` nodes, from breadth-first search.
194
+ - *relative asymmetry* is `RA = 2(MD−1)/(k−2)`.
195
+ - *integration* is `D_k / RA`, with `D_k = 2{k[log₂((k+2)/3) − 1] + 1} / ((k−1)(k−2))`
196
+ (Hillier & Hanson 1984).
197
+
198
+ Each row of the visibility matrix is computed from one exact isovist, so VGA
199
+ costs one isovist per grid point plus an `n × n` BFS in matrix form.
200
+
201
+ ### References
202
+
203
+ - Batty, M. (2001). Exploring isovist fields: space and shape in architectural and urban morphology. *Environment and Planning B*, 28(1), 123–150. https://doi.org/10.1068/b2725
204
+ - Benedikt, M. L. (1979). To take hold of space: isovists and isovist fields. *Environment and Planning B*, 6(1), 47–65. https://doi.org/10.1068/b060047
205
+ - Hillier, B., & Hanson, J. (1984). *The Social Logic of Space*. Cambridge University Press.
206
+ - Turner, A., Doxa, M., O'Sullivan, D., & Penn, A. (2001). From isovists to visibility graphs: a methodology for the analysis of architectural space. *Environment and Planning B*, 28(1), 103–121. https://doi.org/10.1068/b2684
207
+ - Watts, D. J., & Strogatz, S. H. (1998). Collective dynamics of "small-world" networks. *Nature*, 393, 440–442. https://doi.org/10.1038/30918
208
+ - Wiener, J. M., Franz, G., Rossmanith, N., Reichelt, A., Mallot, H. A., & Bülthoff, H. H. (2007). Isovist analysis captures properties of space relevant for locomotion and experience. *Perception*, 36(7), 1066–1083. https://doi.org/10.1068/p5587
209
+
210
+ ## Limitations
211
+
212
+ - 2D only: no terrain, no building heights, no eye level. Every obstacle blocks
213
+ the view completely.
214
+ - Planar coordinates only. Reproject geographic data first.
215
+ - Isovist cost grows with (front-facing edges)², so a single isovist among a
216
+ few thousand edges takes milliseconds to tenths of a second. VGA needs one
217
+ isovist per grid point plus an `n × n` matrix: a few thousand points is
218
+ practical, but tens of thousands is not. Use a coarser grid, a sub-area or
219
+ `max_radius`.
220
+ - Without a boundary, every viewpoint needs `max_radius` (open-space views
221
+ never close).
222
+ - Measure names differ between tools such as depthmapX. Check the definitions
223
+ above before you compare numbers. Integration is computed per connected
224
+ component; nodes that see every other node have `RA = 0` and integration `inf`
225
+ (written as `null`).
226
+ - Geometric tolerance defaults to `1e-9 ×` the extent of the data. Pass
227
+ `Environment(tol=...)` for unusual data.
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ pip install -e ".[dev]"
233
+ python -m pytest tests -q
234
+ python -m ruff check . && python -m ruff format --check src tests
235
+ python -m mypy src
236
+ ```
237
+
238
+ The tests check results against hand-computed geometries: a convex room, a room
239
+ with a pillar, an L-shaped corridor, view cones, view-distance circles clipped
240
+ by walls and small graphs with known depths. They also cross-check against
241
+ independent sampled rays.
242
+
243
+ ## Academic citation
244
+
245
+ If you use `isovist` in research, please cite it (see also `CITATION.cff`):
246
+
247
+ ```bibtex
248
+ @software{eminoglu_isovist,
249
+ author = {Eminoğlu, Yusuf},
250
+ title = {isovist},
251
+ year = {2026},
252
+ version = {0.1.0},
253
+ url = {https://github.com/YusufEminoglu/isovist_sdk}
254
+ }
255
+ ```
256
+
257
+ ## License
258
+
259
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,223 @@
1
+ # isovist
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/isovist.svg)](https://pypi.org/project/isovist/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/isovist.svg)](https://pypi.org/project/isovist/)
5
+ [![CI](https://github.com/YusufEminoglu/isovist_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/isovist_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/isovist_sdk/blob/main/LICENSE)
7
+
8
+ **Exact 2D isovists and visibility graph analysis for urban design.**
9
+
10
+ `isovist` computes what can be seen from a point in a plan of buildings, walls
11
+ and columns, and describes the shape of that visible space with the classic
12
+ isovist measures. It also runs visibility graph analysis (VGA) on a grid of
13
+ points and answers point-to-point "can A see B?" questions. The only dependency
14
+ is NumPy.
15
+
16
+ - **Exact isovist polygon** from an angular sweep with exact ray/edge
17
+ intersections, not a fan of sampled rays. Optional maximum view distance and
18
+ field of view (angle and direction).
19
+ - **Isovist measures**: area, perimeter, compactness, jaggedness, occlusivity,
20
+ number of occluding edges, drift (distance and direction) and min/max/mean/std
21
+ radial length. All of them come from closed-form integrals.
22
+ - **Intervisibility**: exact; a sight line may graze a corner or run along a
23
+ wall, but it may not cross an obstacle.
24
+ - **Visibility graph analysis**: connectivity, clustering coefficient, mean
25
+ depth, relative asymmetry and integration on grids or on your own points.
26
+ An optional visual radius limits the links.
27
+ - **GeoJSON in/out** in projected coordinates, plus an `isovist` command-line tool.
28
+
29
+ ```bash
30
+ pip install isovist
31
+ ```
32
+
33
+ Requires Python 3.9 or newer.
34
+
35
+ ---
36
+
37
+ ## Quick start (Python)
38
+
39
+ ```python
40
+ from isovist import Environment
41
+
42
+ room = [(0, 0), (10, 0), (10, 10), (0, 10)]
43
+ pillar = [(4, 4), (6, 4), (6, 6), (4, 6)]
44
+ env = Environment(obstacles=[pillar], boundary=room)
45
+
46
+ iso = env.isovist((2, 5))
47
+ iso.area # 70.0 (100 - pillar 4 - shadow 26)
48
+ iso.occlusivity # 13.416 (two occluding edges of length sqrt(45))
49
+ iso.polygon # [(x, y), ...] counter-clockwise ring
50
+
51
+ m = iso.measures()
52
+ m.compactness, m.drift, m.mean_radial, m.max_radial
53
+
54
+ env.isovist((2, 5), fov=90, direction=0, max_radius=6) # 90 deg view cone, 6 m reach
55
+ env.is_visible((3, 5), (7, 5)) # False: the pillar is in the way
56
+
57
+ from isovist import visibility_graph_analysis
58
+ vga = visibility_graph_analysis(env, spacing=1.0)
59
+ vga.connectivity, vga.clustering, vga.mean_depth, vga.integration
60
+ ```
61
+
62
+ Obstacles and the boundary are `isovist.Polygon` objects (holes allowed) or plain
63
+ vertex lists. Obstacles may overlap each other and may stick out of the boundary.
64
+ Holes in the boundary act as obstacles. The viewpoint must lie strictly in free
65
+ space.
66
+
67
+ ## Quick start (command line)
68
+
69
+ ```bash
70
+ # isovists with all measures for two viewpoints and for every Point in a file
71
+ isovist compute --obstacles buildings.geojson --boundary site.geojson \
72
+ --at 512340.5,4512200 --points viewpoints.geojson -o isovists.geojson
73
+
74
+ # 120 degree view cone towards north-east, 80 m reach
75
+ isovist compute --obstacles buildings.geojson --at 512340.5,4512200 \
76
+ --fov 120 --direction 45 --max-radius 80
77
+
78
+ # VGA on a 2 m grid (writes grid points with the measures)
79
+ isovist vga --obstacles buildings.geojson --boundary site.geojson --spacing 2 -o vga.geojson
80
+
81
+ # intervisibility; exit code 0 = visible, 1 = not visible
82
+ isovist visible --obstacles buildings.geojson --from 10,20 --to 80,45
83
+ ```
84
+
85
+ Write negative coordinates as `--at=-12.5,40` so they are not read as options.
86
+ Without `--boundary`, open-space isovists need `--max-radius`.
87
+
88
+ See `examples/quickstart.py` for a small plaza with two pavilions and a column.
89
+
90
+ ## Coordinates and GeoJSON
91
+
92
+ All geometry is planar, so coordinates must be **projected** (metres or feet),
93
+ for example UTM. RFC 7946 GeoJSON is nominally longitude/latitude. `isovist`
94
+ reads and writes the same structure with projected coordinates, as produced by
95
+ `ogr2ogr -t_srs EPSG:32635 -lco RFC7946=NO out.geojson in.gpkg`. A legacy
96
+ `crs` member in the input is copied to the output. `load_environment` warns when
97
+ the coordinates look like degrees.
98
+
99
+ Outputs: `isovist compute` writes Polygon features with the viewpoint
100
+ (`vp_x`, `vp_y`), the input point properties and every measure. `isovist vga`
101
+ writes Point features with `connectivity`, `clustering`, `mean_depth`,
102
+ `relative_asymmetry`, `integration` and `node_count`. Undefined values
103
+ (`nan`, `inf`) become `null`.
104
+
105
+ ## API overview
106
+
107
+ | Object / function | Purpose |
108
+ |---|---|
109
+ | `Environment(obstacles, boundary=None, tol=None)` | the plan; `.isovist()`, `.is_visible()`, `.visibility_matrix()`, `.locate()` |
110
+ | `Environment.isovist(vp, max_radius=None, fov=None, direction=0, arc_step=1)` | exact `Isovist` |
111
+ | `Isovist` | `.polygon`, `.area`, `.perimeter`, `.centroid`, `.occlusivity`, `.occluding_edges`, `.radial(theta)`, `.contains(pt)`, `.measures()`, `.to_geojson()` |
112
+ | `IsovistMeasures` | area, perimeter, compactness, jaggedness, occlusivity, occluding_edges, drift, drift_angle, min/max/mean/std_radial |
113
+ | `visibility_graph_analysis(env, points=None, spacing=None, max_radius=None)` | `VGAResult` with per-node arrays and `.records()` |
114
+ | `grid_points(env, spacing, bounds=None, origin=None)` | cell-centre grid in free space |
115
+ | `graph_measures(adjacency)` | VGA measures for any undirected graph |
116
+ | `load_environment`, `polygons_from_geojson`, `points_from_geojson`, `feature_collection`, `write_geojson` | GeoJSON I/O |
117
+
118
+ Angles are in degrees, counted counter-clockwise from east (+x), in both the
119
+ API and the CLI. `Isovist.radial` takes radians.
120
+
121
+ ## Method and sources
122
+
123
+ **Isovist.** An isovist is the set of points visible from a vantage point
124
+ (Benedikt 1979). Every edge end point, every crossing between overlapping
125
+ polygons, every point where an edge meets the view-distance circle and each
126
+ field-of-view limit gives a *critical direction*. Between two consecutive
127
+ critical directions the nearest edge cannot change, so one ray at the mid-angle
128
+ finds it. The isovist boundary on that interval is then the exact intersection
129
+ of the two bounding rays with that edge's line, or a circular arc of radius
130
+ `max_radius`. Edges face the viewer only when the viewpoint lies on their open
131
+ side, so back faces are culled. Only arcs are discretised, and only for the
132
+ output polygon (`arc_step`). Area, perimeter, centroid and radial statistics use
133
+ exact sector formulas.
134
+
135
+ **Measures.** With `A` the area, `P` the perimeter and `r(θ)` the radial length:
136
+
137
+ | Measure | Definition | Source |
138
+ |---|---|---|
139
+ | area, perimeter | `A`, `P` | Benedikt 1979 |
140
+ | occlusivity | total length of the radial occluding edges (boundary that is not a surface). View-cone sides and view-distance arcs are not counted. | Benedikt 1979 |
141
+ | compactness | `4πA / P²` (1 for a circle) | circularity form used in isovist studies after Benedikt 1979; Batty 2001 |
142
+ | jaggedness | `P² / A` (4π for a circle) | Wiener et al. 2007 |
143
+ | drift, drift_angle | distance and direction from the viewpoint to the isovist centroid | Turner et al. 2001; Batty 2001 |
144
+ | min/max/mean/std radial | moments of `r(θ)` over the visible angle, as continuous integrals | Benedikt 1979 (radial moments); Batty 2001 |
145
+
146
+ For a straight piece at perpendicular distance `h`, with tangential coordinate
147
+ `τ`: `∫ r dθ = h ln((r₁+τ₁)/(r₀+τ₀))` and `∫ r² dθ = h (τ₁ − τ₀)`.
148
+
149
+ **VGA** (Turner et al. 2001). Nodes are grid points in free space. Two nodes
150
+ are linked when they can see each other (both ways, by the same exact rule as
151
+ `is_visible`). For each node:
152
+
153
+ - *connectivity* is the number of linked nodes.
154
+ - *clustering coefficient* is the number of links among the node's neighbours
155
+ divided by `k(k−1)/2` (Watts & Strogatz 1998).
156
+ - *mean depth* is `MD = Σ depth / (k−1)` over the node's connected component of
157
+ `k` nodes, from breadth-first search.
158
+ - *relative asymmetry* is `RA = 2(MD−1)/(k−2)`.
159
+ - *integration* is `D_k / RA`, with `D_k = 2{k[log₂((k+2)/3) − 1] + 1} / ((k−1)(k−2))`
160
+ (Hillier & Hanson 1984).
161
+
162
+ Each row of the visibility matrix is computed from one exact isovist, so VGA
163
+ costs one isovist per grid point plus an `n × n` BFS in matrix form.
164
+
165
+ ### References
166
+
167
+ - Batty, M. (2001). Exploring isovist fields: space and shape in architectural and urban morphology. *Environment and Planning B*, 28(1), 123–150. https://doi.org/10.1068/b2725
168
+ - Benedikt, M. L. (1979). To take hold of space: isovists and isovist fields. *Environment and Planning B*, 6(1), 47–65. https://doi.org/10.1068/b060047
169
+ - Hillier, B., & Hanson, J. (1984). *The Social Logic of Space*. Cambridge University Press.
170
+ - Turner, A., Doxa, M., O'Sullivan, D., & Penn, A. (2001). From isovists to visibility graphs: a methodology for the analysis of architectural space. *Environment and Planning B*, 28(1), 103–121. https://doi.org/10.1068/b2684
171
+ - Watts, D. J., & Strogatz, S. H. (1998). Collective dynamics of "small-world" networks. *Nature*, 393, 440–442. https://doi.org/10.1038/30918
172
+ - Wiener, J. M., Franz, G., Rossmanith, N., Reichelt, A., Mallot, H. A., & Bülthoff, H. H. (2007). Isovist analysis captures properties of space relevant for locomotion and experience. *Perception*, 36(7), 1066–1083. https://doi.org/10.1068/p5587
173
+
174
+ ## Limitations
175
+
176
+ - 2D only: no terrain, no building heights, no eye level. Every obstacle blocks
177
+ the view completely.
178
+ - Planar coordinates only. Reproject geographic data first.
179
+ - Isovist cost grows with (front-facing edges)², so a single isovist among a
180
+ few thousand edges takes milliseconds to tenths of a second. VGA needs one
181
+ isovist per grid point plus an `n × n` matrix: a few thousand points is
182
+ practical, but tens of thousands is not. Use a coarser grid, a sub-area or
183
+ `max_radius`.
184
+ - Without a boundary, every viewpoint needs `max_radius` (open-space views
185
+ never close).
186
+ - Measure names differ between tools such as depthmapX. Check the definitions
187
+ above before you compare numbers. Integration is computed per connected
188
+ component; nodes that see every other node have `RA = 0` and integration `inf`
189
+ (written as `null`).
190
+ - Geometric tolerance defaults to `1e-9 ×` the extent of the data. Pass
191
+ `Environment(tol=...)` for unusual data.
192
+
193
+ ## Development
194
+
195
+ ```bash
196
+ pip install -e ".[dev]"
197
+ python -m pytest tests -q
198
+ python -m ruff check . && python -m ruff format --check src tests
199
+ python -m mypy src
200
+ ```
201
+
202
+ The tests check results against hand-computed geometries: a convex room, a room
203
+ with a pillar, an L-shaped corridor, view cones, view-distance circles clipped
204
+ by walls and small graphs with known depths. They also cross-check against
205
+ independent sampled rays.
206
+
207
+ ## Academic citation
208
+
209
+ If you use `isovist` in research, please cite it (see also `CITATION.cff`):
210
+
211
+ ```bibtex
212
+ @software{eminoglu_isovist,
213
+ author = {Eminoğlu, Yusuf},
214
+ title = {isovist},
215
+ year = {2026},
216
+ version = {0.1.0},
217
+ url = {https://github.com/YusufEminoglu/isovist_sdk}
218
+ }
219
+ ```
220
+
221
+ ## License
222
+
223
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,104 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "features": [
4
+ {
5
+ "type": "Feature",
6
+ "properties": {
7
+ "name": "pavilion west"
8
+ },
9
+ "geometry": {
10
+ "type": "Polygon",
11
+ "coordinates": [
12
+ [
13
+ [
14
+ 15,
15
+ 8
16
+ ],
17
+ [
18
+ 27,
19
+ 8
20
+ ],
21
+ [
22
+ 27,
23
+ 16
24
+ ],
25
+ [
26
+ 15,
27
+ 16
28
+ ],
29
+ [
30
+ 15,
31
+ 8
32
+ ]
33
+ ]
34
+ ]
35
+ }
36
+ },
37
+ {
38
+ "type": "Feature",
39
+ "properties": {
40
+ "name": "pavilion east"
41
+ },
42
+ "geometry": {
43
+ "type": "Polygon",
44
+ "coordinates": [
45
+ [
46
+ [
47
+ 34,
48
+ 24
49
+ ],
50
+ [
51
+ 46,
52
+ 24
53
+ ],
54
+ [
55
+ 46,
56
+ 33
57
+ ],
58
+ [
59
+ 34,
60
+ 33
61
+ ],
62
+ [
63
+ 34,
64
+ 24
65
+ ]
66
+ ]
67
+ ]
68
+ }
69
+ },
70
+ {
71
+ "type": "Feature",
72
+ "properties": {
73
+ "name": "column"
74
+ },
75
+ "geometry": {
76
+ "type": "Polygon",
77
+ "coordinates": [
78
+ [
79
+ [
80
+ 30,
81
+ 12
82
+ ],
83
+ [
84
+ 31,
85
+ 12
86
+ ],
87
+ [
88
+ 31,
89
+ 13
90
+ ],
91
+ [
92
+ 30,
93
+ 13
94
+ ],
95
+ [
96
+ 30,
97
+ 12
98
+ ]
99
+ ]
100
+ ]
101
+ }
102
+ }
103
+ ]
104
+ }