seevol 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ tests/* linguist-vendored
2
+ benchmarks/* linguist-vendored
@@ -0,0 +1,8 @@
1
+ uv-cheatsheet.md
2
+ __pycache__
3
+ .venv
4
+ /build
5
+ initial_trials/
6
+ *.npy
7
+ .DS_Store
8
+ benchmarks/
@@ -0,0 +1,33 @@
1
+ cmake_minimum_required(VERSION 3.15)
2
+ project(seevol)
3
+
4
+ # C++ 17 standard
5
+ set(CMAKE_CXX_STANDARD 17)
6
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
7
+
8
+ # require pybind
9
+ find_package(pybind11 REQUIRED)
10
+
11
+ # create module from .cpp
12
+ pybind11_add_module(voxel_occlusion src/seevol/occlusion.cpp)
13
+
14
+ # for multi-threading the hot loop in is_visible:
15
+ # macOS: auto-locate Homebrew's libomp (no-op elsewhere)
16
+ if(APPLE)
17
+ execute_process(COMMAND brew --prefix libomp
18
+ OUTPUT_VARIABLE HOMEBREW_LIBOMP
19
+ OUTPUT_STRIP_TRAILING_WHITESPACE
20
+ ERROR_QUIET)
21
+ if(HOMEBREW_LIBOMP)
22
+ list(APPEND CMAKE_PREFIX_PATH "${HOMEBREW_LIBOMP}")
23
+ endif()
24
+ endif()
25
+ find_package(OpenMP)
26
+ if(OpenMP_CXX_FOUND)
27
+ target_link_libraries(voxel_occlusion PRIVATE OpenMP::OpenMP_CXX)
28
+ else()
29
+ message(WARNING "OpenMP not found: building serial (non-threaded and slower) version. Please install!")
30
+ endif()
31
+
32
+ # once compiled so an .so, add to the seevol package folder
33
+ install(TARGETS voxel_occlusion DESTINATION seevol)
seevol-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 [Archie Benn]
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.
seevol-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: seevol
3
+ Version: 0.1.0
4
+ Summary: All-pairs visibility analysis on voxel volumes (Amanatides & Woo, C++)
5
+ Author-Email: Archie Benn <sj19031@bristol.ac.uk>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: C++
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Project-URL: Repository, https://github.com/archiebenn/seeVol
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: numpy
14
+ Requires-Dist: pyvista
15
+ Description-Content-Type: text/markdown
16
+
17
+ # seeVol
18
+
19
+ <p align="center">
20
+ <img src="images/readme_banner.png" alt="3D visualisation of an artificial ant nest volume coloured by voxel visibility" width="800">
21
+ </p>
22
+
23
+ `seevol` analyses line-of-sight visibility in 3D binary volumes, where 0 represents a solid/opaque voxel, and 1 represents open/transparent voxels.
24
+
25
+ The core functionality is based on the Amanatides & Woo voxel traversal algorithm <sup>1</sup> and is implemented in C++ to compute straight line visibility between all pairs of open (non solid) voxels in a given volume.
26
+
27
+ This tool tests visibility between **all** open pairs throughout the volume, which permits per-voxel visibility analysis metrics to be calculated downstream for every open voxel.
28
+
29
+ > [!WARNING]
30
+ > `seevol` computes all-pairs visibility, which scales as O(open_voxels<sup>2</sup>). This becomes slow or memory-limiting beyond a few hundred thousand open voxels — see [Limitations](#limitations) before running on large volumes. Downsampling the volume is likely to help but will impact results.
31
+
32
+ ## Dependencies
33
+
34
+ A C++17 or greater build toolchain is required for source installation, alongside Python >=3.10.
35
+
36
+ Pyvista is a dependency for visualisations of the visibility metrics output onto the 3D volume itself (see [this visual example](examples/visualiser_example.py) for use).
37
+
38
+ ## Installation
39
+
40
+ To install seevol:
41
+
42
+ ```bash
43
+ pip install seevol
44
+ ```
45
+
46
+ or for uv users:
47
+
48
+ ```bash
49
+ uv pip install seevol
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ The core function `visible_pairs()` accepts a 3D binary (0 = solid, 1 = open) NumPy array as input and outputs two NumPy arrays:
55
+
56
+ - `coords` — shape (N, 3), the open voxel coordinates in the volume
57
+ - `edges` — shape (M, 2), pairs of indices into `coords` for voxels that are mutually visible
58
+
59
+ For example, if `edges[0]` is `[2, 7]`,the visibility edge connects `coords[2]` and `coords[7]` and these two voxels are visible to each other via direct line of sight.
60
+
61
+ Users can load .npy files in Python using `np.load()` and pass the result to `visible_pairs()`.
62
+
63
+ Downstream analyses or visualisations are then possible from this output, such as visualising a 'visibility heatmap' using Pyvista (see below)
64
+
65
+ ## Limitations
66
+
67
+ This tool currently uses all-pair calculation using voxel ray traversal. Because of this, the ray traversal algorithm scales as O(open_voxels<sup>2</sup>) and is therefore quickly unusable for large volumes.
68
+
69
+ A hard limit is imposed in the C++ source file which limits open voxels to 1,000,000. In reality, volumes with over 500,000 open voxels are likely to significant memory issues as very large visible edges will be stored. Methods to reduce this runtime will hopefully be introduced over time.
70
+
71
+ ## Minimal example
72
+
73
+ The [quickstart example](examples/quickstart.py) creates a small volume of open voxels with one solid voxel blocking visibility:
74
+
75
+ ```python
76
+ import numpy as np
77
+ import seevol
78
+
79
+ # 1 = open, 0 = solid
80
+ volume = np.ones((3, 3, 3), dtype=np.uint8)
81
+
82
+ # adds an obstruction
83
+ volume[1, 1, 1] = 0
84
+
85
+ # calculate pairwise voxel visibility
86
+ coords, edges = seevol.visible_pairs(volume)
87
+
88
+ print(f"{len(coords)} open voxels containing {len(edges)} visible pairs")
89
+ ```
90
+
91
+ It prints the number of open voxels and visible pairs:
92
+
93
+ ```
94
+ 26 open voxels containing 214 visible pairs
95
+ ```
96
+
97
+ ### Using .npy files
98
+
99
+ > [!IMPORTANT]
100
+ > NumPy array volumes as .npy files must be 3D and contain binary values, with 0 = solid and 1 = open voxels.
101
+
102
+ To import your own 3D NumPy volume and calculate visibility as with the above example:
103
+
104
+ ```python
105
+ import numpy as np
106
+ import seevol
107
+
108
+ # load your own 3D NumPy volume: 0 = solid, 1 = open
109
+ volume = np.load("my_volume.npy")
110
+
111
+ # calculate pairwise voxel visibility
112
+ coords, edges = seevol.visible_pairs(volume)
113
+
114
+ print(f"{len(coords)} open voxels containing {len(edges)} visible pairs")
115
+ ```
116
+
117
+ ## Visualisation examples on large artificial volumes
118
+
119
+ Below shows an example output of the visibility score per voxel as a heatmap within an intricate 150x150x150 volume, generated using `tunnel_excavator()` to mimic an ant-nest structure and visualised with Pyvista using `visibility_visualiser()`:
120
+
121
+ ![150_cubed](images/realistic_nest.png "150^3 ant excavation example volume")
122
+
123
+ A visibility scored volume using a generated `porespy` volume:
124
+
125
+ ![150_cubed](images/porespy.png "150^3 ant excavation example volume")
126
+
127
+
128
+ ## References
129
+
130
+ > 1. Amanatides, J., & Woo, A. (1987, August). A fast voxel traversal algorithm for ray tracing. In Eurographics (Vol. 87, No. 3, pp. 3-10).
131
+
132
+
seevol-0.1.0/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # seeVol
2
+
3
+ <p align="center">
4
+ <img src="images/readme_banner.png" alt="3D visualisation of an artificial ant nest volume coloured by voxel visibility" width="800">
5
+ </p>
6
+
7
+ `seevol` analyses line-of-sight visibility in 3D binary volumes, where 0 represents a solid/opaque voxel, and 1 represents open/transparent voxels.
8
+
9
+ The core functionality is based on the Amanatides & Woo voxel traversal algorithm <sup>1</sup> and is implemented in C++ to compute straight line visibility between all pairs of open (non solid) voxels in a given volume.
10
+
11
+ This tool tests visibility between **all** open pairs throughout the volume, which permits per-voxel visibility analysis metrics to be calculated downstream for every open voxel.
12
+
13
+ > [!WARNING]
14
+ > `seevol` computes all-pairs visibility, which scales as O(open_voxels<sup>2</sup>). This becomes slow or memory-limiting beyond a few hundred thousand open voxels — see [Limitations](#limitations) before running on large volumes. Downsampling the volume is likely to help but will impact results.
15
+
16
+ ## Dependencies
17
+
18
+ A C++17 or greater build toolchain is required for source installation, alongside Python >=3.10.
19
+
20
+ Pyvista is a dependency for visualisations of the visibility metrics output onto the 3D volume itself (see [this visual example](examples/visualiser_example.py) for use).
21
+
22
+ ## Installation
23
+
24
+ To install seevol:
25
+
26
+ ```bash
27
+ pip install seevol
28
+ ```
29
+
30
+ or for uv users:
31
+
32
+ ```bash
33
+ uv pip install seevol
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ The core function `visible_pairs()` accepts a 3D binary (0 = solid, 1 = open) NumPy array as input and outputs two NumPy arrays:
39
+
40
+ - `coords` — shape (N, 3), the open voxel coordinates in the volume
41
+ - `edges` — shape (M, 2), pairs of indices into `coords` for voxels that are mutually visible
42
+
43
+ For example, if `edges[0]` is `[2, 7]`,the visibility edge connects `coords[2]` and `coords[7]` and these two voxels are visible to each other via direct line of sight.
44
+
45
+ Users can load .npy files in Python using `np.load()` and pass the result to `visible_pairs()`.
46
+
47
+ Downstream analyses or visualisations are then possible from this output, such as visualising a 'visibility heatmap' using Pyvista (see below)
48
+
49
+ ## Limitations
50
+
51
+ This tool currently uses all-pair calculation using voxel ray traversal. Because of this, the ray traversal algorithm scales as O(open_voxels<sup>2</sup>) and is therefore quickly unusable for large volumes.
52
+
53
+ A hard limit is imposed in the C++ source file which limits open voxels to 1,000,000. In reality, volumes with over 500,000 open voxels are likely to significant memory issues as very large visible edges will be stored. Methods to reduce this runtime will hopefully be introduced over time.
54
+
55
+ ## Minimal example
56
+
57
+ The [quickstart example](examples/quickstart.py) creates a small volume of open voxels with one solid voxel blocking visibility:
58
+
59
+ ```python
60
+ import numpy as np
61
+ import seevol
62
+
63
+ # 1 = open, 0 = solid
64
+ volume = np.ones((3, 3, 3), dtype=np.uint8)
65
+
66
+ # adds an obstruction
67
+ volume[1, 1, 1] = 0
68
+
69
+ # calculate pairwise voxel visibility
70
+ coords, edges = seevol.visible_pairs(volume)
71
+
72
+ print(f"{len(coords)} open voxels containing {len(edges)} visible pairs")
73
+ ```
74
+
75
+ It prints the number of open voxels and visible pairs:
76
+
77
+ ```
78
+ 26 open voxels containing 214 visible pairs
79
+ ```
80
+
81
+ ### Using .npy files
82
+
83
+ > [!IMPORTANT]
84
+ > NumPy array volumes as .npy files must be 3D and contain binary values, with 0 = solid and 1 = open voxels.
85
+
86
+ To import your own 3D NumPy volume and calculate visibility as with the above example:
87
+
88
+ ```python
89
+ import numpy as np
90
+ import seevol
91
+
92
+ # load your own 3D NumPy volume: 0 = solid, 1 = open
93
+ volume = np.load("my_volume.npy")
94
+
95
+ # calculate pairwise voxel visibility
96
+ coords, edges = seevol.visible_pairs(volume)
97
+
98
+ print(f"{len(coords)} open voxels containing {len(edges)} visible pairs")
99
+ ```
100
+
101
+ ## Visualisation examples on large artificial volumes
102
+
103
+ Below shows an example output of the visibility score per voxel as a heatmap within an intricate 150x150x150 volume, generated using `tunnel_excavator()` to mimic an ant-nest structure and visualised with Pyvista using `visibility_visualiser()`:
104
+
105
+ ![150_cubed](images/realistic_nest.png "150^3 ant excavation example volume")
106
+
107
+ A visibility scored volume using a generated `porespy` volume:
108
+
109
+ ![150_cubed](images/porespy.png "150^3 ant excavation example volume")
110
+
111
+
112
+ ## References
113
+
114
+ > 1. Amanatides, J., & Woo, A. (1987, August). A fast voxel traversal algorithm for ray tracing. In Eurographics (Vol. 87, No. 3, pp. 3-10).
115
+
116
+
@@ -0,0 +1,17 @@
1
+ # quickstart.py - minimal python example of seevol
2
+ # date: 02-11-2026
3
+ # author: Archie Benn sj19031@bristol.ac.uk
4
+
5
+ import numpy as np
6
+ import seevol
7
+
8
+ # 1 = open, 0 = solid
9
+ volume = np.ones((3, 3, 3), dtype=np.uint8)
10
+
11
+ # adds an obstruction
12
+ volume[1, 1, 1] = 0
13
+
14
+ # calculate pairwise voxel visibility
15
+ coords, edges = seevol.visible_pairs(volume)
16
+
17
+ print(f"{len(coords)} open voxels containing {len(edges)} visible pairs")
@@ -0,0 +1,11 @@
1
+ # example.py - imports functions to view visibility scored in 3D excavated space
2
+ # date: 02-11-2026
3
+ # author: Archie Benn sj19031@bristol.ac.uk
4
+
5
+ import seevol
6
+
7
+ # create example NumPy volume with excavator
8
+ example_nest = seevol.tunnel_excavator(size=180, seed=14, n_steps=18000, branch_prob=1e-5, radius=1)
9
+
10
+ # view visibility in pyvista window
11
+ seevol.visibility_visualiser(example_nest, verbose=True)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core", "pybind11"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "seevol"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Archie Benn", email="sj19031@bristol.ac.uk" },
10
+ ]
11
+ description = "All-pairs visibility analysis on voxel volumes (Amanatides & Woo, C++)"
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ requires-python = ">=3.10"
16
+ classifiers = [
17
+ "Programming Language :: C++",
18
+ "Programming Language :: Python :: 3",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+ dependencies = ["numpy", "pyvista"]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/archiebenn/seeVol"
25
+
26
+ [tool.scikit-build]
27
+ wheel.packages = ["src/seevol"]
28
+ sdist.exclude = ["tests", "development_log.md", "uv-cheatsheet.md"]
29
+
30
+ [tool.pytest.ini_options]
31
+ testpaths = ["tests"]
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pytest>=9.2.0",
36
+ ]
@@ -0,0 +1,11 @@
1
+ from .generator import tunnel_excavator
2
+ from .voxel_occlusion import get_voxel_value, is_visible, visible_pairs
3
+ from .visualiser import visibility_visualiser
4
+
5
+ __all__ = [
6
+ "get_voxel_value",
7
+ "is_visible",
8
+ "tunnel_excavator",
9
+ "visible_pairs",
10
+ "visibility_visualiser",
11
+ ]
@@ -0,0 +1,74 @@
1
+ # generator.py - generates example volumes to simulate ant nest excavations and intricate structures
2
+ # date: 28-10-2026
3
+ # author: Archie Benn sj19031@bristol.ac.uk
4
+
5
+ import numpy as np
6
+ import random
7
+
8
+ def tunnel_excavator(size = 100, n_steps = 20000, branch_prob = 1e-5, radius = 1, seed=0):
9
+ """
10
+ Creates empty grid volume (default volume 100^3 voxels).
11
+
12
+ Produces a volume with fully-connected and randomly carved tunnels within that grid volume.
13
+ """
14
+
15
+ rng = random.Random(seed)
16
+
17
+ # fill size^3 volume with 0s (represents solid/non-open e.g soil within an excavated ant nest)
18
+ vol = np.zeros((size,size,size), dtype=np.uint8)
19
+
20
+ def excavate(position):
21
+ """
22
+ Takes current position and excavates by the radius provided (radius = 1 -> 3x3 excavation).
23
+
24
+ Sets 'excavated' voxel cells to 1.
25
+ """
26
+
27
+ # get current coordinates of 'position'
28
+ x,y,z = position
29
+ # define low and high positions for carving given radius
30
+ low = [max(0, x-radius), max(0, y-radius), max(0, z-radius)]
31
+ high = [min(size, x + radius + 1), min(size, y + radius + 1), min(size, z + radius + 1)]
32
+
33
+ # ****actual excavation step i.e. set carved voxel(s) == 1****
34
+ vol[low[0]:high[0], low[1]:high[1], low[2]:high[2]] = 1
35
+
36
+ # begin 'excavation' with starting position near top in centre (1 down in y plane)
37
+ start = (size //2, size-1, size//2)
38
+ excavate(start)
39
+
40
+ # active walker list (e.g an ant excavating)
41
+ heads = [start]
42
+
43
+ # apply a slight y and x bias for increased chance of moving down/sideways (more ant-like)
44
+ # change these values in each axes to adjust excavation shapes
45
+ moves = (
46
+ [(1,0,0)] * 21 + [(-1,0,0)] * 21 + # x
47
+ [(0,1,0)] * 20 + [(0,-1,0)] * 22 + # y
48
+ [(0,0,1)] * 20 + [(0,0,-1)] * 20 # z
49
+ )
50
+
51
+ for _ in range(n_steps):
52
+
53
+ head_idx = rng.randrange(len(heads))
54
+ x,y,z = heads[head_idx]
55
+
56
+ # movement selector
57
+ dx,dy,dz = rng.choice(moves)
58
+
59
+ # new head coordinates after move
60
+ nx,ny,nz = x + dx, y + dy, z + dz
61
+
62
+ # only excavates if the new coordinates are within size of voluem
63
+ # i.e if new position does not fall outside the defined volume zone 'size'
64
+ if 0 <= nx < size and 0 <= ny < size and 0 <= nz < size:
65
+
66
+ # excavate and move head/ant
67
+ excavate((nx,ny,nz))
68
+ heads[head_idx] = (nx,ny,nz)
69
+
70
+ # occasionally spawn new branch form current position
71
+ if rng.random() < branch_prob:
72
+ heads.append((nx,ny,nz))
73
+
74
+ return vol