voxcity 1.0.13__py3-none-any.whl → 1.0.15__py3-none-any.whl
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.
- voxcity/simulator/solar/__init__.py +13 -0
- voxcity/simulator_gpu/__init__.py +73 -98
- voxcity/simulator_gpu/domain.py +30 -256
- voxcity/simulator_gpu/raytracing.py +153 -0
- voxcity/simulator_gpu/solar/__init__.py +45 -1
- voxcity/simulator_gpu/solar/domain.py +57 -0
- voxcity/simulator_gpu/solar/integration.py +1622 -253
- voxcity/simulator_gpu/solar/mask.py +459 -0
- voxcity/simulator_gpu/solar/raytracing.py +28 -532
- voxcity/simulator_gpu/solar/volumetric.py +962 -14
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/METADATA +1 -1
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/RECORD +15 -25
- voxcity/simulator_gpu/common/__init__.py +0 -9
- voxcity/simulator_gpu/common/geometry.py +0 -11
- voxcity/simulator_gpu/environment.yml +0 -11
- voxcity/simulator_gpu/integration.py +0 -15
- voxcity/simulator_gpu/kernels.py +0 -56
- voxcity/simulator_gpu/radiation.py +0 -28
- voxcity/simulator_gpu/sky.py +0 -9
- voxcity/simulator_gpu/solar/voxcity.py +0 -2953
- voxcity/simulator_gpu/temporal.py +0 -13
- voxcity/simulator_gpu/utils.py +0 -25
- voxcity/simulator_gpu/view.py +0 -32
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-1.0.13.dist-info → voxcity-1.0.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -64,3 +64,16 @@ from .integration import ( # noqa: F401
|
|
|
64
64
|
save_irradiance_mesh,
|
|
65
65
|
load_irradiance_mesh,
|
|
66
66
|
)
|
|
67
|
+
|
|
68
|
+
# Computation mask utilities (re-export from simulator_gpu for convenience)
|
|
69
|
+
try:
|
|
70
|
+
from voxcity.simulator_gpu.solar.mask import ( # noqa: F401
|
|
71
|
+
create_computation_mask,
|
|
72
|
+
draw_computation_mask,
|
|
73
|
+
get_mask_from_drawing,
|
|
74
|
+
visualize_computation_mask,
|
|
75
|
+
get_mask_info,
|
|
76
|
+
)
|
|
77
|
+
except ImportError:
|
|
78
|
+
# simulator_gpu may not be installed
|
|
79
|
+
pass
|
|
@@ -1,115 +1,90 @@
|
|
|
1
|
-
"""simulator_gpu: GPU-accelerated simulation
|
|
1
|
+
"""simulator_gpu: GPU-accelerated urban simulation using Taichi.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This package provides GPU-accelerated implementations for:
|
|
4
|
+
- Solar radiation simulation (direct, diffuse, cumulative)
|
|
5
|
+
- View analysis (green view index, sky view factor)
|
|
6
|
+
- Landmark visibility analysis
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Submodules:
|
|
9
|
+
solar: Solar radiation calculations
|
|
10
|
+
visibility: View and visibility analysis
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Example:
|
|
13
|
+
from voxcity.simulator_gpu import solar, visibility
|
|
14
|
+
|
|
15
|
+
# Solar radiation
|
|
16
|
+
irradiance = solar.get_global_solar_irradiance_using_epw(voxcity, ...)
|
|
17
|
+
|
|
18
|
+
# View analysis
|
|
19
|
+
gvi = visibility.get_view_index(voxcity, mode='green')
|
|
10
20
|
"""
|
|
11
21
|
|
|
12
22
|
import os
|
|
13
23
|
|
|
14
|
-
# Disable Numba caching to prevent stale cache issues
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
os.environ.setdefault("NUMBA_CACHE_DIR", "") # Disable disk caching
|
|
18
|
-
os.environ.setdefault("NUMBA_DISABLE_JIT", "0") # Keep JIT enabled for performance
|
|
19
|
-
|
|
20
|
-
# Import Taichi initialization utilities first
|
|
21
|
-
from .init_taichi import ( # noqa: F401
|
|
22
|
-
init_taichi,
|
|
23
|
-
ensure_initialized,
|
|
24
|
-
is_initialized,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# Check if Taichi is available
|
|
28
|
-
try:
|
|
29
|
-
import taichi as ti
|
|
30
|
-
_TAICHI_AVAILABLE = True
|
|
31
|
-
except ImportError:
|
|
32
|
-
_TAICHI_AVAILABLE = False
|
|
24
|
+
# Disable Numba caching to prevent stale cache issues
|
|
25
|
+
os.environ.setdefault("NUMBA_CACHE_DIR", "")
|
|
26
|
+
os.environ.setdefault("NUMBA_DISABLE_JIT", "0")
|
|
33
27
|
|
|
34
|
-
#
|
|
35
|
-
from .
|
|
36
|
-
from .solar import * # noqa: F401,F403
|
|
37
|
-
from .utils import * # noqa: F401,F403
|
|
28
|
+
# Taichi initialization
|
|
29
|
+
from .init_taichi import init_taichi, ensure_initialized, is_initialized
|
|
38
30
|
|
|
39
|
-
#
|
|
40
|
-
from . import
|
|
41
|
-
from . import visibility # noqa: F401
|
|
42
|
-
from . import view # noqa: F401
|
|
43
|
-
from . import utils # noqa: F401
|
|
44
|
-
from . import common # noqa: F401
|
|
45
|
-
|
|
46
|
-
# VoxCity-flattened module names that some code expects to exist on the toplevel
|
|
47
|
-
from . import sky # noqa: F401
|
|
48
|
-
from . import kernels # noqa: F401
|
|
49
|
-
from . import radiation # noqa: F401
|
|
50
|
-
from . import temporal # noqa: F401
|
|
51
|
-
from . import integration # noqa: F401
|
|
52
|
-
|
|
53
|
-
# Commonly re-exported VoxCity solar helpers
|
|
54
|
-
from .kernels import compute_direct_solar_irradiance_map_binary # noqa: F401
|
|
55
|
-
from .radiation import compute_solar_irradiance_for_all_faces # noqa: F401
|
|
56
|
-
|
|
57
|
-
# Backward compatibility: some code treats `simulator.view` as `simulator.visibility`
|
|
58
|
-
# (VoxCity provides `view.py` wrapper; we also provide that module).
|
|
59
|
-
|
|
60
|
-
# Export shared modules (kept; extra symbols are fine)
|
|
61
|
-
from .core import ( # noqa: F401
|
|
31
|
+
# Core utilities
|
|
32
|
+
from .core import (
|
|
62
33
|
Vector3, Point3,
|
|
63
34
|
PI, TWO_PI, DEG_TO_RAD, RAD_TO_DEG,
|
|
64
35
|
SOLAR_CONSTANT, EXT_COEF,
|
|
65
36
|
)
|
|
66
|
-
from .domain import Domain, IUP, IDOWN, INORTH, ISOUTH, IEAST, IWEST # noqa: F401
|
|
67
|
-
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for pattern in ["**/*.nbc", "**/*.nbi"]:
|
|
86
|
-
for cache_file in package_dir.glob(pattern):
|
|
87
|
-
try:
|
|
88
|
-
cache_file.unlink()
|
|
89
|
-
cleared.append(str(cache_file))
|
|
90
|
-
except Exception:
|
|
91
|
-
pass
|
|
92
|
-
|
|
93
|
-
# Clear __pycache__ directories
|
|
94
|
-
for pycache in package_dir.glob("**/__pycache__"):
|
|
95
|
-
try:
|
|
96
|
-
shutil.rmtree(pycache)
|
|
97
|
-
cleared.append(str(pycache))
|
|
98
|
-
except Exception:
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
# Try to clear user's .numba_cache if it exists
|
|
102
|
-
home = Path.home()
|
|
103
|
-
numba_cache = home / ".numba_cache"
|
|
104
|
-
if numba_cache.exists():
|
|
105
|
-
try:
|
|
106
|
-
shutil.rmtree(numba_cache)
|
|
107
|
-
cleared.append(str(numba_cache))
|
|
108
|
-
except Exception:
|
|
109
|
-
pass
|
|
110
|
-
|
|
111
|
-
print(f"Cleared {len(cleared)} cache items. Please restart your Python kernel.")
|
|
112
|
-
return cleared
|
|
38
|
+
# Domain (shared between solar and visibility)
|
|
39
|
+
from .domain import Domain, Surfaces, extract_surfaces_from_domain
|
|
40
|
+
from .domain import IUP, IDOWN, INORTH, ISOUTH, IEAST, IWEST
|
|
41
|
+
|
|
42
|
+
# Submodules
|
|
43
|
+
from . import solar
|
|
44
|
+
from . import visibility
|
|
45
|
+
|
|
46
|
+
# Convenience imports from solar
|
|
47
|
+
from .solar import (
|
|
48
|
+
get_global_solar_irradiance_using_epw,
|
|
49
|
+
get_building_global_solar_irradiance_using_epw,
|
|
50
|
+
get_direct_solar_irradiance_map,
|
|
51
|
+
get_diffuse_solar_irradiance_map,
|
|
52
|
+
get_global_solar_irradiance_map,
|
|
53
|
+
)
|
|
113
54
|
|
|
55
|
+
# Convenience imports from visibility
|
|
56
|
+
from .visibility import (
|
|
57
|
+
get_view_index,
|
|
58
|
+
get_sky_view_factor_map,
|
|
59
|
+
get_surface_view_factor,
|
|
60
|
+
get_landmark_visibility_map,
|
|
61
|
+
get_surface_landmark_visibility,
|
|
62
|
+
)
|
|
114
63
|
|
|
115
64
|
__version__ = "0.1.0"
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
# Initialization
|
|
68
|
+
'init_taichi', 'ensure_initialized', 'is_initialized',
|
|
69
|
+
# Core
|
|
70
|
+
'Vector3', 'Point3',
|
|
71
|
+
'PI', 'TWO_PI', 'DEG_TO_RAD', 'RAD_TO_DEG',
|
|
72
|
+
'SOLAR_CONSTANT', 'EXT_COEF',
|
|
73
|
+
# Domain
|
|
74
|
+
'Domain', 'Surfaces', 'extract_surfaces_from_domain',
|
|
75
|
+
'IUP', 'IDOWN', 'INORTH', 'ISOUTH', 'IEAST', 'IWEST',
|
|
76
|
+
# Submodules
|
|
77
|
+
'solar', 'visibility',
|
|
78
|
+
# Solar (convenience)
|
|
79
|
+
'get_global_solar_irradiance_using_epw',
|
|
80
|
+
'get_building_global_solar_irradiance_using_epw',
|
|
81
|
+
'get_direct_solar_irradiance_map',
|
|
82
|
+
'get_diffuse_solar_irradiance_map',
|
|
83
|
+
'get_global_solar_irradiance_map',
|
|
84
|
+
# Visibility (convenience)
|
|
85
|
+
'get_view_index',
|
|
86
|
+
'get_sky_view_factor_map',
|
|
87
|
+
'get_surface_view_factor',
|
|
88
|
+
'get_landmark_visibility_map',
|
|
89
|
+
'get_surface_landmark_visibility',
|
|
90
|
+
]
|
voxcity/simulator_gpu/domain.py
CHANGED
|
@@ -1,262 +1,36 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Shared domain definition for simulator_gpu.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- Tree mask for view analysis
|
|
4
|
+
This module re-exports the Domain class from solar.domain for backward compatibility.
|
|
5
|
+
The main implementation is in simulator_gpu.solar.domain which includes:
|
|
6
|
+
- Domain class with full grid, terrain, building, and vegetation support
|
|
7
|
+
- Surfaces class for radiation calculations
|
|
8
|
+
- Surface extraction utilities
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
# Re-export from solar.domain (the main implementation)
|
|
12
|
+
from .solar.domain import (
|
|
13
|
+
Domain,
|
|
14
|
+
Surfaces,
|
|
15
|
+
extract_surfaces_from_domain,
|
|
16
|
+
IUP,
|
|
17
|
+
IDOWN,
|
|
18
|
+
INORTH,
|
|
19
|
+
ISOUTH,
|
|
20
|
+
IEAST,
|
|
21
|
+
IWEST,
|
|
22
|
+
DIR_NORMALS,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
DIR_NORMALS
|
|
29
|
-
|
|
30
|
-
IDOWN: (0.0, 0.0, -1.0),
|
|
31
|
-
INORTH: (0.0, 1.0, 0.0),
|
|
32
|
-
ISOUTH: (0.0, -1.0, 0.0),
|
|
33
|
-
IEAST: (1.0, 0.0, 0.0),
|
|
34
|
-
IWEST: (-1.0, 0.0, 0.0),
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@ti.data_oriented
|
|
39
|
-
class Domain:
|
|
40
|
-
"""
|
|
41
|
-
3D computational domain for simulation.
|
|
42
|
-
|
|
43
|
-
The domain uses a regular grid with:
|
|
44
|
-
- x: West to East
|
|
45
|
-
- y: South to North
|
|
46
|
-
- z: Ground to Sky
|
|
47
|
-
|
|
48
|
-
Attributes:
|
|
49
|
-
nx, ny, nz: Number of grid cells in each direction
|
|
50
|
-
dx, dy, dz: Grid spacing in meters
|
|
51
|
-
origin: (x, y, z) coordinates of domain origin
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
nx: int,
|
|
57
|
-
ny: int,
|
|
58
|
-
nz: int,
|
|
59
|
-
dx: float = 1.0,
|
|
60
|
-
dy: float = 1.0,
|
|
61
|
-
dz: float = 1.0,
|
|
62
|
-
origin: Tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
63
|
-
origin_lat: Optional[float] = None,
|
|
64
|
-
origin_lon: Optional[float] = None
|
|
65
|
-
):
|
|
66
|
-
"""
|
|
67
|
-
Initialize the domain.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
nx, ny, nz: Grid dimensions
|
|
71
|
-
dx, dy, dz: Grid spacing (m)
|
|
72
|
-
origin: Domain origin coordinates
|
|
73
|
-
origin_lat: Latitude for solar calculations (degrees)
|
|
74
|
-
origin_lon: Longitude for solar calculations (degrees)
|
|
75
|
-
"""
|
|
76
|
-
# Ensure Taichi is initialized before creating any fields
|
|
77
|
-
ensure_initialized()
|
|
78
|
-
|
|
79
|
-
self.nx = nx
|
|
80
|
-
self.ny = ny
|
|
81
|
-
self.nz = nz
|
|
82
|
-
self.dx = dx
|
|
83
|
-
self.dy = dy
|
|
84
|
-
self.dz = dz
|
|
85
|
-
self.origin = origin
|
|
86
|
-
self.origin_lat = origin_lat if origin_lat is not None else 0.0
|
|
87
|
-
self.origin_lon = origin_lon if origin_lon is not None else 0.0
|
|
88
|
-
|
|
89
|
-
# Domain bounds
|
|
90
|
-
self.x_min = origin[0]
|
|
91
|
-
self.x_max = origin[0] + nx * dx
|
|
92
|
-
self.y_min = origin[1]
|
|
93
|
-
self.y_max = origin[1] + ny * dy
|
|
94
|
-
self.z_min = origin[2]
|
|
95
|
-
self.z_max = origin[2] + nz * dz
|
|
96
|
-
|
|
97
|
-
# Grid cell volume
|
|
98
|
-
self.cell_volume = dx * dy * dz
|
|
99
|
-
|
|
100
|
-
# Topography: terrain height at each (i, j) column
|
|
101
|
-
self.topo_top = ti.field(dtype=ti.i32, shape=(nx, ny))
|
|
102
|
-
|
|
103
|
-
# Building mask: 1 if cell is solid (building), 0 if air
|
|
104
|
-
self.is_solid = ti.field(dtype=ti.i32, shape=(nx, ny, nz))
|
|
105
|
-
|
|
106
|
-
# Tree mask: 1 if cell is tree canopy, 0 otherwise
|
|
107
|
-
self.is_tree = ti.field(dtype=ti.i32, shape=(nx, ny, nz))
|
|
108
|
-
|
|
109
|
-
# Leaf Area Density (m^2/m^3) for plant canopy
|
|
110
|
-
self.lad = ti.field(dtype=ti.f32, shape=(nx, ny, nz))
|
|
111
|
-
|
|
112
|
-
# Plant canopy top index for each column
|
|
113
|
-
self.plant_top = ti.field(dtype=ti.i32, shape=(nx, ny))
|
|
114
|
-
|
|
115
|
-
# Surface count
|
|
116
|
-
self.n_surfaces = ti.field(dtype=ti.i32, shape=())
|
|
117
|
-
|
|
118
|
-
# Initialize arrays
|
|
119
|
-
self._init_arrays()
|
|
120
|
-
|
|
121
|
-
@ti.kernel
|
|
122
|
-
def _init_arrays(self):
|
|
123
|
-
"""Initialize all arrays to default values."""
|
|
124
|
-
for i, j in self.topo_top:
|
|
125
|
-
self.topo_top[i, j] = 0
|
|
126
|
-
self.plant_top[i, j] = 0
|
|
127
|
-
|
|
128
|
-
for i, j, k in self.is_solid:
|
|
129
|
-
self.is_solid[i, j, k] = 0
|
|
130
|
-
self.is_tree[i, j, k] = 0
|
|
131
|
-
self.lad[i, j, k] = 0.0
|
|
132
|
-
|
|
133
|
-
def set_flat_terrain(self, height: float = 0.0):
|
|
134
|
-
"""Set flat terrain at given height."""
|
|
135
|
-
k_top = int(height / self.dz)
|
|
136
|
-
self._set_flat_terrain_kernel(k_top)
|
|
137
|
-
|
|
138
|
-
def initialize_terrain(self, height: float = 0.0):
|
|
139
|
-
"""Alias for set_flat_terrain."""
|
|
140
|
-
self.set_flat_terrain(height)
|
|
141
|
-
|
|
142
|
-
@ti.kernel
|
|
143
|
-
def _set_flat_terrain_kernel(self, k_top: ti.i32):
|
|
144
|
-
for i, j in self.topo_top:
|
|
145
|
-
self.topo_top[i, j] = k_top
|
|
146
|
-
for k in range(k_top + 1):
|
|
147
|
-
self.is_solid[i, j, k] = 1
|
|
148
|
-
|
|
149
|
-
def set_terrain_from_array(self, terrain_height: np.ndarray):
|
|
150
|
-
"""
|
|
151
|
-
Set terrain from 2D numpy array of heights.
|
|
152
|
-
|
|
153
|
-
Args:
|
|
154
|
-
terrain_height: 2D array (nx, ny) of terrain heights in meters
|
|
155
|
-
"""
|
|
156
|
-
terrain_k = (terrain_height / self.dz).astype(np.int32)
|
|
157
|
-
self._set_terrain_kernel(terrain_k)
|
|
158
|
-
|
|
159
|
-
@ti.kernel
|
|
160
|
-
def _set_terrain_kernel(self, terrain_k: ti.types.ndarray()):
|
|
161
|
-
for i, j in self.topo_top:
|
|
162
|
-
k_top = terrain_k[i, j]
|
|
163
|
-
self.topo_top[i, j] = k_top
|
|
164
|
-
for k in range(self.nz):
|
|
165
|
-
if k <= k_top:
|
|
166
|
-
self.is_solid[i, j, k] = 1
|
|
167
|
-
else:
|
|
168
|
-
self.is_solid[i, j, k] = 0
|
|
169
|
-
|
|
170
|
-
def add_building(
|
|
171
|
-
self,
|
|
172
|
-
x_range: Optional[Tuple[int, int]] = None,
|
|
173
|
-
y_range: Optional[Tuple[int, int]] = None,
|
|
174
|
-
z_range: Optional[Tuple[int, int]] = None,
|
|
175
|
-
*,
|
|
176
|
-
x_start: Optional[int] = None,
|
|
177
|
-
x_end: Optional[int] = None,
|
|
178
|
-
y_start: Optional[int] = None,
|
|
179
|
-
y_end: Optional[int] = None,
|
|
180
|
-
height: Optional[float] = None
|
|
181
|
-
):
|
|
182
|
-
"""
|
|
183
|
-
Add a rectangular building to the domain.
|
|
184
|
-
"""
|
|
185
|
-
# Handle convenience parameters
|
|
186
|
-
if x_start is not None and x_end is not None:
|
|
187
|
-
x_range = (x_start, x_end)
|
|
188
|
-
if y_start is not None and y_end is not None:
|
|
189
|
-
y_range = (y_start, y_end)
|
|
190
|
-
if height is not None and z_range is None:
|
|
191
|
-
k_top = int(height / self.dz) + 1
|
|
192
|
-
z_range = (0, k_top)
|
|
193
|
-
|
|
194
|
-
if x_range is None or y_range is None or z_range is None:
|
|
195
|
-
raise ValueError("Must provide either range tuples or individual parameters")
|
|
196
|
-
|
|
197
|
-
self._add_building_kernel(x_range[0], x_range[1], y_range[0], y_range[1], z_range[0], z_range[1])
|
|
198
|
-
|
|
199
|
-
@ti.kernel
|
|
200
|
-
def _add_building_kernel(self, i_min: ti.i32, i_max: ti.i32, j_min: ti.i32, j_max: ti.i32, k_min: ti.i32, k_max: ti.i32):
|
|
201
|
-
for i, j, k in ti.ndrange((i_min, i_max), (j_min, j_max), (k_min, k_max)):
|
|
202
|
-
self.is_solid[i, j, k] = 1
|
|
203
|
-
|
|
204
|
-
def add_tree(
|
|
205
|
-
self,
|
|
206
|
-
x_range: Tuple[int, int],
|
|
207
|
-
y_range: Tuple[int, int],
|
|
208
|
-
z_range: Tuple[int, int],
|
|
209
|
-
lad_value: float = 1.0
|
|
210
|
-
):
|
|
211
|
-
"""
|
|
212
|
-
Add a tree canopy region to the domain.
|
|
213
|
-
|
|
214
|
-
Args:
|
|
215
|
-
x_range, y_range, z_range: Grid index ranges
|
|
216
|
-
lad_value: Leaf Area Density value
|
|
217
|
-
"""
|
|
218
|
-
self._add_tree_kernel(x_range[0], x_range[1], y_range[0], y_range[1], z_range[0], z_range[1], lad_value)
|
|
219
|
-
|
|
220
|
-
@ti.kernel
|
|
221
|
-
def _add_tree_kernel(self, i_min: ti.i32, i_max: ti.i32, j_min: ti.i32, j_max: ti.i32, k_min: ti.i32, k_max: ti.i32, lad: ti.f32):
|
|
222
|
-
for i, j, k in ti.ndrange((i_min, i_max), (j_min, j_max), (k_min, k_max)):
|
|
223
|
-
self.is_tree[i, j, k] = 1
|
|
224
|
-
self.lad[i, j, k] = lad
|
|
225
|
-
|
|
226
|
-
def set_from_voxel_data(self, voxel_data: np.ndarray, tree_code: int = -2, solid_codes: Optional[list] = None):
|
|
227
|
-
"""
|
|
228
|
-
Set domain from a 3D voxel data array.
|
|
229
|
-
|
|
230
|
-
Args:
|
|
231
|
-
voxel_data: 3D numpy array with voxel class codes
|
|
232
|
-
tree_code: Class code for trees (default -2)
|
|
233
|
-
solid_codes: List of codes that are solid (default: all non-zero except tree_code)
|
|
234
|
-
"""
|
|
235
|
-
if solid_codes is None:
|
|
236
|
-
# All non-zero codes except tree are solid
|
|
237
|
-
solid_codes = []
|
|
238
|
-
|
|
239
|
-
self._set_from_voxel_data_kernel(voxel_data, tree_code)
|
|
240
|
-
|
|
241
|
-
@ti.kernel
|
|
242
|
-
def _set_from_voxel_data_kernel(self, voxel_data: ti.types.ndarray(), tree_code: ti.i32):
|
|
243
|
-
for i, j, k in ti.ndrange(self.nx, self.ny, self.nz):
|
|
244
|
-
val = voxel_data[i, j, k]
|
|
245
|
-
if val == tree_code:
|
|
246
|
-
self.is_tree[i, j, k] = 1
|
|
247
|
-
self.is_solid[i, j, k] = 0
|
|
248
|
-
elif val != 0:
|
|
249
|
-
self.is_solid[i, j, k] = 1
|
|
250
|
-
self.is_tree[i, j, k] = 0
|
|
251
|
-
else:
|
|
252
|
-
self.is_solid[i, j, k] = 0
|
|
253
|
-
self.is_tree[i, j, k] = 0
|
|
254
|
-
|
|
255
|
-
def get_max_dist(self) -> float:
|
|
256
|
-
"""Get maximum ray distance (domain diagonal)."""
|
|
257
|
-
import math
|
|
258
|
-
return math.sqrt(
|
|
259
|
-
(self.nx * self.dx)**2 +
|
|
260
|
-
(self.ny * self.dy)**2 +
|
|
261
|
-
(self.nz * self.dz)**2
|
|
262
|
-
)
|
|
25
|
+
__all__ = [
|
|
26
|
+
'Domain',
|
|
27
|
+
'Surfaces',
|
|
28
|
+
'extract_surfaces_from_domain',
|
|
29
|
+
'IUP',
|
|
30
|
+
'IDOWN',
|
|
31
|
+
'INORTH',
|
|
32
|
+
'ISOUTH',
|
|
33
|
+
'IEAST',
|
|
34
|
+
'IWEST',
|
|
35
|
+
'DIR_NORMALS',
|
|
36
|
+
]
|
|
@@ -466,6 +466,159 @@ def ray_canopy_absorption(
|
|
|
466
466
|
return transmissivity, total_lad_path
|
|
467
467
|
|
|
468
468
|
|
|
469
|
+
@ti.func
|
|
470
|
+
def ray_point_to_point_transmissivity(
|
|
471
|
+
pos_from: Vector3,
|
|
472
|
+
pos_to: Vector3,
|
|
473
|
+
lad: ti.template(),
|
|
474
|
+
is_solid: ti.template(),
|
|
475
|
+
nx: ti.i32,
|
|
476
|
+
ny: ti.i32,
|
|
477
|
+
nz: ti.i32,
|
|
478
|
+
dx: ti.f32,
|
|
479
|
+
dy: ti.f32,
|
|
480
|
+
dz: ti.f32,
|
|
481
|
+
ext_coef: ti.f32
|
|
482
|
+
):
|
|
483
|
+
"""
|
|
484
|
+
Compute transmissivity of radiation between two points through canopy.
|
|
485
|
+
|
|
486
|
+
This is used for surface-to-surface reflections where reflected radiation
|
|
487
|
+
must pass through any intervening vegetation.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
pos_from: Start position (emitting surface center)
|
|
491
|
+
pos_to: End position (receiving surface center)
|
|
492
|
+
lad: 3D field of Leaf Area Density
|
|
493
|
+
is_solid: 3D field of solid cells (buildings/terrain)
|
|
494
|
+
nx, ny, nz: Grid dimensions
|
|
495
|
+
dx, dy, dz: Cell sizes
|
|
496
|
+
ext_coef: Extinction coefficient
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Tuple of (transmissivity, blocked_by_solid)
|
|
500
|
+
- transmissivity: 0-1 fraction of radiation that gets through
|
|
501
|
+
- blocked_by_solid: 1 if ray hits a solid cell, 0 otherwise
|
|
502
|
+
"""
|
|
503
|
+
# Compute ray direction and distance
|
|
504
|
+
diff = pos_to - pos_from
|
|
505
|
+
dist = diff.norm()
|
|
506
|
+
|
|
507
|
+
transmissivity = 1.0
|
|
508
|
+
blocked = 0
|
|
509
|
+
|
|
510
|
+
# Only trace if distance is significant
|
|
511
|
+
if dist >= 0.01:
|
|
512
|
+
ray_dir = diff / dist
|
|
513
|
+
|
|
514
|
+
# Starting voxel
|
|
515
|
+
pos = pos_from + ray_dir * 0.01 # Slight offset to avoid self-intersection
|
|
516
|
+
|
|
517
|
+
ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
|
|
518
|
+
iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
|
|
519
|
+
iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
|
|
520
|
+
|
|
521
|
+
# Clamp to valid range
|
|
522
|
+
ix = ti.max(0, ti.min(nx - 1, ix))
|
|
523
|
+
iy = ti.max(0, ti.min(ny - 1, iy))
|
|
524
|
+
iz = ti.max(0, ti.min(nz - 1, iz))
|
|
525
|
+
|
|
526
|
+
# Step directions
|
|
527
|
+
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
528
|
+
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
529
|
+
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
530
|
+
|
|
531
|
+
# Initialize DDA variables
|
|
532
|
+
t_max_x = 1e30
|
|
533
|
+
t_max_y = 1e30
|
|
534
|
+
t_max_z = 1e30
|
|
535
|
+
t_delta_x = 1e30
|
|
536
|
+
t_delta_y = 1e30
|
|
537
|
+
t_delta_z = 1e30
|
|
538
|
+
|
|
539
|
+
t = 0.01 # Start offset
|
|
540
|
+
|
|
541
|
+
if ti.abs(ray_dir[0]) > 1e-10:
|
|
542
|
+
if step_x > 0:
|
|
543
|
+
t_max_x = ((ix + 1) * dx - pos_from[0]) / ray_dir[0]
|
|
544
|
+
else:
|
|
545
|
+
t_max_x = (ix * dx - pos_from[0]) / ray_dir[0]
|
|
546
|
+
t_delta_x = ti.abs(dx / ray_dir[0])
|
|
547
|
+
|
|
548
|
+
if ti.abs(ray_dir[1]) > 1e-10:
|
|
549
|
+
if step_y > 0:
|
|
550
|
+
t_max_y = ((iy + 1) * dy - pos_from[1]) / ray_dir[1]
|
|
551
|
+
else:
|
|
552
|
+
t_max_y = (iy * dy - pos_from[1]) / ray_dir[1]
|
|
553
|
+
t_delta_y = ti.abs(dy / ray_dir[1])
|
|
554
|
+
|
|
555
|
+
if ti.abs(ray_dir[2]) > 1e-10:
|
|
556
|
+
if step_z > 0:
|
|
557
|
+
t_max_z = ((iz + 1) * dz - pos_from[2]) / ray_dir[2]
|
|
558
|
+
else:
|
|
559
|
+
t_max_z = (iz * dz - pos_from[2]) / ray_dir[2]
|
|
560
|
+
t_delta_z = ti.abs(dz / ray_dir[2])
|
|
561
|
+
|
|
562
|
+
t_prev = t
|
|
563
|
+
max_steps = nx + ny + nz
|
|
564
|
+
done = 0
|
|
565
|
+
|
|
566
|
+
for _ in range(max_steps):
|
|
567
|
+
if done == 1:
|
|
568
|
+
continue # Skip remaining iterations
|
|
569
|
+
|
|
570
|
+
if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
|
|
571
|
+
done = 1
|
|
572
|
+
continue
|
|
573
|
+
if t > dist: # Reached target
|
|
574
|
+
done = 1
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
# Check for solid obstruction (but skip first and last cell as they're the surfaces)
|
|
578
|
+
if is_solid[ix, iy, iz] == 1 and t > 0.1 and t < dist - 0.1:
|
|
579
|
+
blocked = 1
|
|
580
|
+
transmissivity = 0.0
|
|
581
|
+
done = 1
|
|
582
|
+
continue
|
|
583
|
+
|
|
584
|
+
# Get step distance
|
|
585
|
+
t_next = t_max_x
|
|
586
|
+
if t_max_y < t_next:
|
|
587
|
+
t_next = t_max_y
|
|
588
|
+
if t_max_z < t_next:
|
|
589
|
+
t_next = t_max_z
|
|
590
|
+
|
|
591
|
+
# Limit to target distance
|
|
592
|
+
t_next = ti.min(t_next, dist)
|
|
593
|
+
|
|
594
|
+
# Path length through this cell
|
|
595
|
+
path_len = t_next - t_prev
|
|
596
|
+
|
|
597
|
+
# Accumulate absorption from LAD
|
|
598
|
+
cell_lad = lad[ix, iy, iz]
|
|
599
|
+
if cell_lad > 0.0:
|
|
600
|
+
# Beer-Lambert: T = exp(-ext_coef * LAD * path)
|
|
601
|
+
transmissivity *= ti.exp(-ext_coef * cell_lad * path_len)
|
|
602
|
+
|
|
603
|
+
t_prev = t_next
|
|
604
|
+
|
|
605
|
+
# Step to next voxel
|
|
606
|
+
if t_max_x < t_max_y and t_max_x < t_max_z:
|
|
607
|
+
t = t_max_x
|
|
608
|
+
ix += step_x
|
|
609
|
+
t_max_x += t_delta_x
|
|
610
|
+
elif t_max_y < t_max_z:
|
|
611
|
+
t = t_max_y
|
|
612
|
+
iy += step_y
|
|
613
|
+
t_max_y += t_delta_y
|
|
614
|
+
else:
|
|
615
|
+
t = t_max_z
|
|
616
|
+
iz += step_z
|
|
617
|
+
t_max_z += t_delta_z
|
|
618
|
+
|
|
619
|
+
return transmissivity, blocked
|
|
620
|
+
|
|
621
|
+
|
|
469
622
|
@ti.func
|
|
470
623
|
def ray_trace_to_target(
|
|
471
624
|
origin: Vector3,
|