voxcity 1.0.2__py3-none-any.whl → 1.0.13__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/downloader/ocean.py +559 -0
- voxcity/generator/api.py +6 -0
- voxcity/generator/grids.py +45 -32
- voxcity/generator/pipeline.py +327 -27
- voxcity/geoprocessor/draw.py +14 -8
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/landcover.py +173 -49
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/models.py +2 -0
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/visualizer/renderer.py +2 -1
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/METADATA +16 -53
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/RECORD +50 -15
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solar position calculation for palm-solar.
|
|
3
|
+
|
|
4
|
+
Based on PALM's calc_zenith subroutine (radiation_model_mod.f90 lines 7965-8012).
|
|
5
|
+
Computes solar declination, hour angle, zenith angle, and direction vector.
|
|
6
|
+
|
|
7
|
+
PALM Alignment:
|
|
8
|
+
- Declination formula: ASIN(decl_1 * SIN(decl_2 * day_of_year - decl_3))
|
|
9
|
+
- Hour angle formula: 2π * (second_of_day / 86400) + longitude - π
|
|
10
|
+
- cos_zenith: sin(lat)*sin(decl) + cos(lat)*cos(decl)*cos(hour_angle)
|
|
11
|
+
- Sun direction: Computed from declination and hour angle
|
|
12
|
+
|
|
13
|
+
All constants match PALM exactly:
|
|
14
|
+
- decl_1 = sin(23.45°) = 0.39794968147687266
|
|
15
|
+
- decl_2 = 2π/365 = 0.017214206321039962
|
|
16
|
+
- decl_3 = decl_2 * 81 = 1.3943507120042368 (vernal equinox offset)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import taichi as ti
|
|
20
|
+
import math
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Tuple, Optional
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from .core import Vector3, Point3, DEG_TO_RAD, RAD_TO_DEG, PI, TWO_PI
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Constants for solar declination calculation (matching PALM exactly)
|
|
29
|
+
# PALM: decl_1 = SIN( 23.45_wp * pi / 180.0_wp )
|
|
30
|
+
# PALM: decl_2 = 2.0_wp * pi / 365.0_wp
|
|
31
|
+
# PALM: decl_3 = decl_2 * 81.0_wp (offset for vernal equinox ~March 21)
|
|
32
|
+
DECL_1 = 0.39794968147687266 # sin(23.45 * pi / 180)
|
|
33
|
+
DECL_2 = 0.017214206321039962 # 2 * pi / 365
|
|
34
|
+
DECL_3 = 1.3943507120042368 # DECL_2 * 81
|
|
35
|
+
|
|
36
|
+
# Seconds per day
|
|
37
|
+
SECONDS_PER_DAY = 86400.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SolarPosition:
|
|
42
|
+
"""
|
|
43
|
+
Solar position data.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
cos_zenith: Cosine of solar zenith angle (0 at horizon, 1 at zenith)
|
|
47
|
+
zenith_angle: Solar zenith angle in degrees
|
|
48
|
+
azimuth_angle: Solar azimuth angle in degrees (0 = North, 90 = East)
|
|
49
|
+
elevation_angle: Solar elevation angle in degrees (0 = horizon, 90 = zenith)
|
|
50
|
+
direction: Unit vector pointing towards the sun (x, y, z)
|
|
51
|
+
sun_up: True if sun is above horizon
|
|
52
|
+
"""
|
|
53
|
+
cos_zenith: float
|
|
54
|
+
zenith_angle: float
|
|
55
|
+
azimuth_angle: float
|
|
56
|
+
elevation_angle: float
|
|
57
|
+
direction: Tuple[float, float, float]
|
|
58
|
+
sun_up: bool
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def calc_zenith(
|
|
62
|
+
day_of_year: int,
|
|
63
|
+
second_of_day: float,
|
|
64
|
+
latitude: float,
|
|
65
|
+
longitude: float
|
|
66
|
+
) -> SolarPosition:
|
|
67
|
+
"""
|
|
68
|
+
Calculate solar position.
|
|
69
|
+
|
|
70
|
+
Based on PALM's calc_zenith subroutine.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
day_of_year: Day number (1-365)
|
|
74
|
+
second_of_day: Seconds since midnight UTC
|
|
75
|
+
latitude: Latitude in degrees (-90 to 90)
|
|
76
|
+
longitude: Longitude in degrees (-180 to 180)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
SolarPosition with all solar geometry data
|
|
80
|
+
"""
|
|
81
|
+
# Convert to radians
|
|
82
|
+
lat = latitude * DEG_TO_RAD
|
|
83
|
+
lon = longitude * DEG_TO_RAD
|
|
84
|
+
|
|
85
|
+
# Solar declination angle
|
|
86
|
+
declination = math.asin(DECL_1 * math.sin(DECL_2 * day_of_year - DECL_3))
|
|
87
|
+
|
|
88
|
+
# Hour angle (solar noon at lon=0 is at 12:00 UTC)
|
|
89
|
+
hour_angle = TWO_PI * (second_of_day / SECONDS_PER_DAY) + lon - PI
|
|
90
|
+
|
|
91
|
+
# Cosine of zenith angle
|
|
92
|
+
cos_zenith = (math.sin(lat) * math.sin(declination) +
|
|
93
|
+
math.cos(lat) * math.cos(declination) * math.cos(hour_angle))
|
|
94
|
+
cos_zenith = max(0.0, cos_zenith)
|
|
95
|
+
|
|
96
|
+
# Zenith and elevation angles
|
|
97
|
+
zenith_angle = math.acos(min(1.0, cos_zenith)) * RAD_TO_DEG
|
|
98
|
+
elevation_angle = 90.0 - zenith_angle
|
|
99
|
+
|
|
100
|
+
# Solar direction vector (x=east, y=north, z=up)
|
|
101
|
+
# Direction in longitudes = sin(solar_azimuth) * sin(zenith)
|
|
102
|
+
sun_dir_lon = -math.sin(hour_angle) * math.cos(declination)
|
|
103
|
+
|
|
104
|
+
# Direction in latitudes = cos(solar_azimuth) * sin(zenith)
|
|
105
|
+
sun_dir_lat = (math.sin(declination) * math.cos(lat) -
|
|
106
|
+
math.cos(hour_angle) * math.cos(declination) * math.sin(lat))
|
|
107
|
+
|
|
108
|
+
# Normalize to get unit vector pointing toward sun
|
|
109
|
+
sin_zenith = math.sqrt(1.0 - cos_zenith**2) if cos_zenith < 1.0 else 0.0
|
|
110
|
+
|
|
111
|
+
if sin_zenith > 1e-10:
|
|
112
|
+
# Horizontal components
|
|
113
|
+
sun_x = sun_dir_lon # East component
|
|
114
|
+
sun_y = sun_dir_lat # North component
|
|
115
|
+
sun_z = cos_zenith # Up component
|
|
116
|
+
|
|
117
|
+
# Normalize
|
|
118
|
+
length = math.sqrt(sun_x**2 + sun_y**2 + sun_z**2)
|
|
119
|
+
if length > 1e-10:
|
|
120
|
+
sun_x /= length
|
|
121
|
+
sun_y /= length
|
|
122
|
+
sun_z /= length
|
|
123
|
+
else:
|
|
124
|
+
# Sun at zenith
|
|
125
|
+
sun_x = 0.0
|
|
126
|
+
sun_y = 0.0
|
|
127
|
+
sun_z = 1.0
|
|
128
|
+
|
|
129
|
+
# Azimuth angle (0 = North, 90 = East)
|
|
130
|
+
azimuth_angle = math.atan2(sun_x, sun_y) * RAD_TO_DEG
|
|
131
|
+
if azimuth_angle < 0:
|
|
132
|
+
azimuth_angle += 360.0
|
|
133
|
+
|
|
134
|
+
sun_up = cos_zenith > 0.0
|
|
135
|
+
|
|
136
|
+
return SolarPosition(
|
|
137
|
+
cos_zenith=cos_zenith,
|
|
138
|
+
zenith_angle=zenith_angle,
|
|
139
|
+
azimuth_angle=azimuth_angle,
|
|
140
|
+
elevation_angle=elevation_angle,
|
|
141
|
+
direction=(sun_x, sun_y, sun_z),
|
|
142
|
+
sun_up=sun_up
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def calc_solar_position_datetime(
|
|
147
|
+
dt: datetime,
|
|
148
|
+
latitude: float,
|
|
149
|
+
longitude: float
|
|
150
|
+
) -> SolarPosition:
|
|
151
|
+
"""
|
|
152
|
+
Calculate solar position from datetime.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
dt: Datetime (should be in UTC or timezone-aware)
|
|
156
|
+
latitude: Latitude in degrees
|
|
157
|
+
longitude: Longitude in degrees
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
SolarPosition
|
|
161
|
+
"""
|
|
162
|
+
# Convert to UTC if timezone-aware
|
|
163
|
+
if dt.tzinfo is not None:
|
|
164
|
+
dt = dt.astimezone(timezone.utc)
|
|
165
|
+
|
|
166
|
+
# Day of year (1-365)
|
|
167
|
+
day_of_year = dt.timetuple().tm_yday
|
|
168
|
+
|
|
169
|
+
# Seconds since midnight UTC
|
|
170
|
+
second_of_day = dt.hour * 3600.0 + dt.minute * 60.0 + dt.second + dt.microsecond / 1e6
|
|
171
|
+
|
|
172
|
+
return calc_zenith(day_of_year, second_of_day, latitude, longitude)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_day_of_year(year: int, month: int, day: int) -> int:
|
|
176
|
+
"""Get day of year from date."""
|
|
177
|
+
return datetime(year, month, day).timetuple().tm_yday
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@ti.func
|
|
181
|
+
def calc_zenith_ti(
|
|
182
|
+
day_of_year: ti.i32,
|
|
183
|
+
second_of_day: ti.f32,
|
|
184
|
+
lat_rad: ti.f32,
|
|
185
|
+
lon_rad: ti.f32
|
|
186
|
+
) -> ti.math.vec4:
|
|
187
|
+
"""
|
|
188
|
+
Taichi function to calculate solar position.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
day_of_year: Day number (1-365)
|
|
192
|
+
second_of_day: Seconds since midnight UTC
|
|
193
|
+
lat_rad: Latitude in radians
|
|
194
|
+
lon_rad: Longitude in radians
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
vec4(cos_zenith, sun_x, sun_y, sun_z)
|
|
198
|
+
"""
|
|
199
|
+
# Solar declination
|
|
200
|
+
declination = ti.asin(0.409093 * ti.sin(0.0172028 * day_of_year - 1.39012))
|
|
201
|
+
|
|
202
|
+
# Hour angle
|
|
203
|
+
hour_angle = 6.283185 * (second_of_day / 86400.0) + lon_rad - 3.141593
|
|
204
|
+
|
|
205
|
+
# Cosine of zenith
|
|
206
|
+
cos_zenith = (ti.sin(lat_rad) * ti.sin(declination) +
|
|
207
|
+
ti.cos(lat_rad) * ti.cos(declination) * ti.cos(hour_angle))
|
|
208
|
+
cos_zenith = ti.max(0.0, cos_zenith)
|
|
209
|
+
|
|
210
|
+
# Direction components
|
|
211
|
+
sun_dir_lon = -ti.sin(hour_angle) * ti.cos(declination)
|
|
212
|
+
sun_dir_lat = (ti.sin(declination) * ti.cos(lat_rad) -
|
|
213
|
+
ti.cos(hour_angle) * ti.cos(declination) * ti.sin(lat_rad))
|
|
214
|
+
|
|
215
|
+
# Normalize
|
|
216
|
+
sun_x = sun_dir_lon
|
|
217
|
+
sun_y = sun_dir_lat
|
|
218
|
+
sun_z = cos_zenith
|
|
219
|
+
length = ti.sqrt(sun_x**2 + sun_y**2 + sun_z**2)
|
|
220
|
+
|
|
221
|
+
if length > 1e-10:
|
|
222
|
+
sun_x /= length
|
|
223
|
+
sun_y /= length
|
|
224
|
+
sun_z /= length
|
|
225
|
+
else:
|
|
226
|
+
sun_x = 0.0
|
|
227
|
+
sun_y = 0.0
|
|
228
|
+
sun_z = 1.0
|
|
229
|
+
|
|
230
|
+
return ti.math.vec4(cos_zenith, sun_x, sun_y, sun_z)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@ti.data_oriented
|
|
234
|
+
class SolarCalculator:
|
|
235
|
+
"""
|
|
236
|
+
GPU-accelerated solar position calculator.
|
|
237
|
+
|
|
238
|
+
Pre-computes solar positions for all time steps.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(self, latitude: float, longitude: float):
|
|
242
|
+
"""
|
|
243
|
+
Initialize solar calculator.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
latitude: Site latitude in degrees
|
|
247
|
+
longitude: Site longitude in degrees
|
|
248
|
+
"""
|
|
249
|
+
self.latitude = latitude
|
|
250
|
+
self.longitude = longitude
|
|
251
|
+
self.lat_rad = latitude * DEG_TO_RAD
|
|
252
|
+
self.lon_rad = longitude * DEG_TO_RAD
|
|
253
|
+
|
|
254
|
+
# Current solar position (stored as fields for GPU access)
|
|
255
|
+
self.cos_zenith = ti.field(dtype=ti.f32, shape=())
|
|
256
|
+
self.sun_direction = ti.Vector.field(3, dtype=ti.f32, shape=())
|
|
257
|
+
self.sun_up = ti.field(dtype=ti.i32, shape=())
|
|
258
|
+
|
|
259
|
+
# Initialize
|
|
260
|
+
self.cos_zenith[None] = 0.0
|
|
261
|
+
self.sun_direction[None] = Vector3(0.0, 0.0, 1.0)
|
|
262
|
+
self.sun_up[None] = 0
|
|
263
|
+
|
|
264
|
+
def update(self, day_of_year: int, second_of_day: float):
|
|
265
|
+
"""Update solar position for given time."""
|
|
266
|
+
pos = calc_zenith(day_of_year, second_of_day, self.latitude, self.longitude)
|
|
267
|
+
self.cos_zenith[None] = pos.cos_zenith
|
|
268
|
+
self.sun_direction[None] = Vector3(*pos.direction)
|
|
269
|
+
self.sun_up[None] = 1 if pos.sun_up else 0
|
|
270
|
+
|
|
271
|
+
def update_datetime(self, dt: datetime):
|
|
272
|
+
"""Update solar position for given datetime."""
|
|
273
|
+
pos = calc_solar_position_datetime(dt, self.latitude, self.longitude)
|
|
274
|
+
self.cos_zenith[None] = pos.cos_zenith
|
|
275
|
+
self.sun_direction[None] = Vector3(*pos.direction)
|
|
276
|
+
self.sun_up[None] = 1 if pos.sun_up else 0
|
|
277
|
+
|
|
278
|
+
@ti.func
|
|
279
|
+
def get_cos_zenith(self) -> ti.f32:
|
|
280
|
+
"""Get current cosine of zenith angle."""
|
|
281
|
+
return self.cos_zenith[None]
|
|
282
|
+
|
|
283
|
+
@ti.func
|
|
284
|
+
def get_sun_direction(self) -> Vector3:
|
|
285
|
+
"""Get current sun direction unit vector."""
|
|
286
|
+
return self.sun_direction[None]
|
|
287
|
+
|
|
288
|
+
@ti.func
|
|
289
|
+
def is_sun_up(self) -> ti.i32:
|
|
290
|
+
"""Check if sun is above horizon."""
|
|
291
|
+
return self.sun_up[None]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def discretize_sky_directions(
|
|
295
|
+
n_azimuth: int = 80,
|
|
296
|
+
n_elevation: int = 40
|
|
297
|
+
) -> Tuple[list, list]:
|
|
298
|
+
"""
|
|
299
|
+
Generate discretized sky directions for SVF/ray tracing.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
n_azimuth: Number of azimuthal divisions
|
|
303
|
+
n_elevation: Number of elevation divisions (full hemisphere)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Tuple of (directions, solid_angles) where:
|
|
307
|
+
- directions: List of (x, y, z) unit vectors
|
|
308
|
+
- solid_angles: List of solid angle weights
|
|
309
|
+
"""
|
|
310
|
+
directions = []
|
|
311
|
+
solid_angles = []
|
|
312
|
+
|
|
313
|
+
# Only upper hemisphere (elevation from 0 to 90)
|
|
314
|
+
for i_elev in range(n_elevation // 2):
|
|
315
|
+
# Center of elevation band
|
|
316
|
+
elev_low = (i_elev / n_elevation) * PI
|
|
317
|
+
elev_high = ((i_elev + 1) / n_elevation) * PI
|
|
318
|
+
elevation = (elev_low + elev_high) / 2
|
|
319
|
+
|
|
320
|
+
# Solid angle for this band
|
|
321
|
+
d_omega = (2 * PI / n_azimuth) * (math.cos(elev_low) - math.cos(elev_high))
|
|
322
|
+
|
|
323
|
+
for i_azim in range(n_azimuth):
|
|
324
|
+
# Center of azimuth band
|
|
325
|
+
azimuth = (i_azim + 0.5) * (2 * PI / n_azimuth)
|
|
326
|
+
|
|
327
|
+
# Convert to Cartesian
|
|
328
|
+
cos_elev = math.cos(elevation)
|
|
329
|
+
sin_elev = math.sin(elevation)
|
|
330
|
+
x = sin_elev * math.sin(azimuth)
|
|
331
|
+
y = sin_elev * math.cos(azimuth)
|
|
332
|
+
z = cos_elev
|
|
333
|
+
|
|
334
|
+
directions.append((x, y, z))
|
|
335
|
+
solid_angles.append(d_omega)
|
|
336
|
+
|
|
337
|
+
return directions, solid_angles
|