voxcity 0.7.0__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.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,421 @@
1
+ """
2
+ EPW (EnergyPlus Weather) File Processing for palm_solar.
3
+
4
+ This module provides utilities for reading EPW weather files and extracting
5
+ solar radiation data for cumulative irradiance simulations.
6
+
7
+ EPW files contain hourly weather data including:
8
+ - Direct Normal Irradiance (DNI)
9
+ - Diffuse Horizontal Irradiance (DHI)
10
+ - Global Horizontal Irradiance (GHI)
11
+ - Location metadata (latitude, longitude, timezone, elevation)
12
+
13
+ References:
14
+ - EnergyPlus Weather File Format: https://energyplus.net/weather
15
+ - EPW Data Dictionary: https://bigladdersoftware.com/epx/docs/8-3/auxiliary-programs/energyplus-weather-file-epw-data-dictionary.html
16
+ """
17
+
18
+ import numpy as np
19
+ from pathlib import Path
20
+ from typing import Tuple, Union, Optional, List
21
+ from dataclasses import dataclass
22
+ import pandas as pd
23
+
24
+
25
+ @dataclass
26
+ class EPWLocation:
27
+ """
28
+ Location metadata from EPW file header.
29
+
30
+ Attributes:
31
+ city: City name
32
+ state_province: State or province
33
+ country: Country code
34
+ data_source: Weather data source
35
+ wmo_station: WMO station identifier
36
+ latitude: Latitude in degrees (positive = North)
37
+ longitude: Longitude in degrees (positive = East)
38
+ timezone: UTC offset in hours (e.g., -5 for EST)
39
+ elevation: Elevation above sea level in meters
40
+ """
41
+ city: str
42
+ state_province: str
43
+ country: str
44
+ data_source: str
45
+ wmo_station: str
46
+ latitude: float
47
+ longitude: float
48
+ timezone: float
49
+ elevation: float
50
+
51
+
52
+ @dataclass
53
+ class EPWSolarData:
54
+ """
55
+ Solar radiation data extracted from EPW file for simulation.
56
+
57
+ Attributes:
58
+ timestamps: Array of datetime-like indices (hourly)
59
+ dni: Direct Normal Irradiance (W/m²)
60
+ dhi: Diffuse Horizontal Irradiance (W/m²)
61
+ ghi: Global Horizontal Irradiance (W/m²), optional
62
+ location: EPWLocation with site metadata
63
+ day_of_year: Array of day numbers (1-365)
64
+ hour: Array of hour values (0-23)
65
+ year: Year from EPW file
66
+ """
67
+ timestamps: np.ndarray
68
+ dni: np.ndarray
69
+ dhi: np.ndarray
70
+ ghi: Optional[np.ndarray]
71
+ location: EPWLocation
72
+ day_of_year: np.ndarray
73
+ hour: np.ndarray
74
+ year: int
75
+
76
+
77
+ def read_epw_header(epw_path: Union[str, Path]) -> EPWLocation:
78
+ """
79
+ Read location metadata from EPW file header.
80
+
81
+ Args:
82
+ epw_path: Path to EPW file
83
+
84
+ Returns:
85
+ EPWLocation with site metadata
86
+
87
+ Raises:
88
+ FileNotFoundError: If EPW file doesn't exist
89
+ ValueError: If LOCATION line cannot be parsed
90
+ """
91
+ epw_path_obj = Path(epw_path)
92
+ if not epw_path_obj.exists():
93
+ raise FileNotFoundError(f"EPW file not found: {epw_path}")
94
+
95
+ with open(epw_path_obj, 'r', encoding='utf-8', errors='replace') as f:
96
+ for line in f:
97
+ if line.startswith("LOCATION"):
98
+ parts = line.strip().split(',')
99
+ if len(parts) < 10:
100
+ raise ValueError(f"Invalid LOCATION line in EPW file: {line}")
101
+
102
+ return EPWLocation(
103
+ city=parts[1].strip(),
104
+ state_province=parts[2].strip(),
105
+ country=parts[3].strip(),
106
+ data_source=parts[4].strip(),
107
+ wmo_station=parts[5].strip(),
108
+ latitude=float(parts[6]),
109
+ longitude=float(parts[7]),
110
+ timezone=float(parts[8]),
111
+ elevation=float(parts[9])
112
+ )
113
+
114
+ raise ValueError("Could not find LOCATION line in EPW file")
115
+
116
+
117
+ def read_epw_solar_data(
118
+ epw_path: Union[str, Path],
119
+ start_month: Optional[int] = None,
120
+ end_month: Optional[int] = None,
121
+ start_day: Optional[int] = None,
122
+ end_day: Optional[int] = None
123
+ ) -> EPWSolarData:
124
+ """
125
+ Read solar radiation data from EPW file.
126
+
127
+ This function extracts DNI, DHI, and GHI values along with temporal
128
+ information needed for solar position calculations.
129
+
130
+ Args:
131
+ epw_path: Path to EPW file
132
+ start_month: Filter to start from this month (1-12)
133
+ end_month: Filter to end at this month (1-12)
134
+ start_day: Filter to start from this day (1-31)
135
+ end_day: Filter to end at this day (1-31)
136
+
137
+ Returns:
138
+ EPWSolarData containing radiation data and location metadata
139
+
140
+ Example:
141
+ >>> solar_data = read_epw_solar_data("weather.epw", start_month=6, end_month=6)
142
+ >>> print(f"June DNI max: {solar_data.dni.max():.1f} W/m²")
143
+ """
144
+ epw_path_obj = Path(epw_path)
145
+ if not epw_path_obj.exists():
146
+ raise FileNotFoundError(f"EPW file not found: {epw_path}")
147
+
148
+ # Read location header
149
+ location = read_epw_header(epw_path)
150
+
151
+ # Read weather data (starts at line 9, 0-indexed line 8)
152
+ with open(epw_path_obj, 'r', encoding='utf-8', errors='replace') as f:
153
+ lines = f.readlines()
154
+
155
+ # Find data start (after 8 header lines)
156
+ data_start_index = 8
157
+
158
+ # Parse weather data
159
+ data_rows = []
160
+ for line in lines[data_start_index:]:
161
+ parts = line.strip().split(',')
162
+ if len(parts) < 22: # Need at least 22 columns for radiation data
163
+ continue
164
+
165
+ try:
166
+ year = int(parts[0])
167
+ month = int(parts[1])
168
+ day = int(parts[2])
169
+ hour = int(parts[3]) - 1 # EPW hours are 1-24, convert to 0-23
170
+
171
+ # Apply date filters
172
+ if start_month is not None and month < start_month:
173
+ continue
174
+ if end_month is not None and month > end_month:
175
+ continue
176
+ if start_month is not None and end_month is not None:
177
+ if start_month == end_month:
178
+ if start_day is not None and day < start_day:
179
+ continue
180
+ if end_day is not None and day > end_day:
181
+ continue
182
+
183
+ # Radiation columns (from EPW specification):
184
+ # Column 11 (index 10): Global Horizontal Radiation (Wh/m²)
185
+ # Column 14 (index 13): Direct Normal Radiation (Wh/m²)
186
+ # Column 15 (index 14): Diffuse Horizontal Radiation (Wh/m²)
187
+ ghi = float(parts[13]) # Global Horizontal Radiation
188
+ dni = float(parts[14]) # Direct Normal Radiation
189
+ dhi = float(parts[15]) # Diffuse Horizontal Radiation
190
+
191
+ data_rows.append({
192
+ 'year': year,
193
+ 'month': month,
194
+ 'day': day,
195
+ 'hour': hour,
196
+ 'dni': dni,
197
+ 'dhi': dhi,
198
+ 'ghi': ghi
199
+ })
200
+ except (ValueError, IndexError):
201
+ continue
202
+
203
+ if not data_rows:
204
+ raise ValueError("No valid weather data found in EPW file")
205
+
206
+ # Convert to arrays
207
+ n = len(data_rows)
208
+ timestamps = np.empty(n, dtype='datetime64[h]')
209
+ dni = np.zeros(n, dtype=np.float64)
210
+ dhi = np.zeros(n, dtype=np.float64)
211
+ ghi = np.zeros(n, dtype=np.float64)
212
+ day_of_year = np.zeros(n, dtype=np.int32)
213
+ hour = np.zeros(n, dtype=np.int32)
214
+
215
+ for i, row in enumerate(data_rows):
216
+ # Create timestamp
217
+ timestamps[i] = np.datetime64(
218
+ f"{row['year']:04d}-{row['month']:02d}-{row['day']:02d}T{row['hour']:02d}"
219
+ )
220
+ dni[i] = row['dni']
221
+ dhi[i] = row['dhi']
222
+ ghi[i] = row['ghi']
223
+
224
+ # Calculate day of year
225
+ import datetime
226
+ dt = datetime.date(row['year'], row['month'], row['day'])
227
+ day_of_year[i] = dt.timetuple().tm_yday
228
+ hour[i] = row['hour']
229
+
230
+ return EPWSolarData(
231
+ timestamps=timestamps,
232
+ dni=dni,
233
+ dhi=dhi,
234
+ ghi=ghi,
235
+ location=location,
236
+ day_of_year=day_of_year,
237
+ hour=hour,
238
+ year=data_rows[0]['year'] if data_rows else 2020
239
+ )
240
+
241
+
242
+ def prepare_cumulative_simulation_input(
243
+ epw_path: Union[str, Path],
244
+ start_month: Optional[int] = None,
245
+ end_month: Optional[int] = None,
246
+ start_day: Optional[int] = None,
247
+ end_day: Optional[int] = None,
248
+ filter_daytime: bool = True,
249
+ min_elevation_deg: float = 5.0
250
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, EPWLocation]:
251
+ """
252
+ Prepare solar simulation input data from EPW file.
253
+
254
+ This function reads EPW data and calculates solar positions for each
255
+ timestep, filtering out nighttime hours. Returns data ready for sky
256
+ patch binning and cumulative irradiance calculation.
257
+
258
+ Args:
259
+ epw_path: Path to EPW file
260
+ start_month: Filter start month (1-12)
261
+ end_month: Filter end month (1-12)
262
+ start_day: Filter start day (1-31)
263
+ end_day: Filter end day (1-31)
264
+ filter_daytime: If True, filter out hours with sun below horizon
265
+ min_elevation_deg: Minimum solar elevation to include (degrees)
266
+
267
+ Returns:
268
+ Tuple of:
269
+ - azimuth: Solar azimuth angles (degrees, 0=North, clockwise)
270
+ - elevation: Solar elevation angles (degrees, 0=horizon, 90=zenith)
271
+ - dni: Direct Normal Irradiance (W/m²)
272
+ - dhi: Diffuse Horizontal Irradiance (W/m²)
273
+ - location: EPWLocation with site metadata
274
+
275
+ Example:
276
+ >>> az, el, dni, dhi, loc = prepare_cumulative_simulation_input("weather.epw")
277
+ >>> print(f"Location: {loc.city}, {loc.country}")
278
+ >>> print(f"Daytime hours: {len(az)}")
279
+ """
280
+ from .solar import calc_zenith
281
+
282
+ # Read EPW data
283
+ solar_data = read_epw_solar_data(
284
+ epw_path, start_month, end_month, start_day, end_day
285
+ )
286
+
287
+ location = solar_data.location
288
+
289
+ # Calculate solar positions for each timestep
290
+ n = len(solar_data.dni)
291
+ azimuth = np.zeros(n, dtype=np.float64)
292
+ elevation = np.zeros(n, dtype=np.float64)
293
+
294
+ for i in range(n):
295
+ doy = solar_data.day_of_year[i]
296
+ hr = solar_data.hour[i]
297
+
298
+ # Convert local hour to UTC seconds
299
+ # EPW data is in local standard time, need to convert to UTC
300
+ utc_hour = hr - location.timezone
301
+ second_of_day = utc_hour * 3600.0
302
+
303
+ # Handle day wraparound
304
+ if second_of_day < 0:
305
+ second_of_day += 86400
306
+ elif second_of_day >= 86400:
307
+ second_of_day -= 86400
308
+
309
+ # Calculate solar position
310
+ pos = calc_zenith(doy, second_of_day, location.latitude, location.longitude)
311
+ azimuth[i] = pos.azimuth_angle
312
+ elevation[i] = pos.elevation_angle
313
+
314
+ # Filter daytime hours if requested
315
+ if filter_daytime:
316
+ mask = elevation >= min_elevation_deg
317
+ azimuth = azimuth[mask]
318
+ elevation = elevation[mask]
319
+ dni = solar_data.dni[mask]
320
+ dhi = solar_data.dhi[mask]
321
+ else:
322
+ dni = solar_data.dni
323
+ dhi = solar_data.dhi
324
+
325
+ return azimuth, elevation, dni, dhi, location
326
+
327
+
328
+ def get_typical_days(
329
+ epw_path: Union[str, Path],
330
+ months: Optional[List[int]] = None
331
+ ) -> pd.DataFrame:
332
+ """
333
+ Extract typical day profiles from EPW file for each month.
334
+
335
+ Calculates average hourly DNI and DHI for each month, useful for
336
+ quick annual simulations using representative days.
337
+
338
+ Args:
339
+ epw_path: Path to EPW file
340
+ months: List of months to process (default: all 12)
341
+
342
+ Returns:
343
+ DataFrame with columns: month, hour, dni_avg, dhi_avg, ghi_avg
344
+
345
+ Example:
346
+ >>> typical = get_typical_days("weather.epw", months=[6, 12])
347
+ >>> june = typical[typical.month == 6]
348
+ """
349
+ solar_data = read_epw_solar_data(epw_path)
350
+
351
+ if months is None:
352
+ months = list(range(1, 13))
353
+
354
+ results = []
355
+
356
+ for month in months:
357
+ for hour in range(24):
358
+ # Find matching hours
359
+ mask = (
360
+ (np.array([int(str(t)[5:7]) for t in solar_data.timestamps]) == month) &
361
+ (solar_data.hour == hour)
362
+ )
363
+
364
+ if np.any(mask):
365
+ results.append({
366
+ 'month': month,
367
+ 'hour': hour,
368
+ 'dni_avg': np.mean(solar_data.dni[mask]),
369
+ 'dhi_avg': np.mean(solar_data.dhi[mask]),
370
+ 'ghi_avg': np.mean(solar_data.ghi[mask]) if solar_data.ghi is not None else np.nan
371
+ })
372
+
373
+ return pd.DataFrame(results)
374
+
375
+
376
+ def estimate_annual_irradiance(epw_path: Union[str, Path]) -> dict:
377
+ """
378
+ Estimate annual solar irradiance statistics from EPW file.
379
+
380
+ Provides quick overview of solar resource without full simulation.
381
+
382
+ Args:
383
+ epw_path: Path to EPW file
384
+
385
+ Returns:
386
+ Dictionary with annual statistics:
387
+ - total_dni_kwh_m2: Annual cumulative DNI (kWh/m²)
388
+ - total_dhi_kwh_m2: Annual cumulative DHI (kWh/m²)
389
+ - total_ghi_kwh_m2: Annual cumulative GHI (kWh/m²)
390
+ - peak_dni: Maximum hourly DNI (W/m²)
391
+ - peak_ghi: Maximum hourly GHI (W/m²)
392
+ - sunshine_hours: Hours with DNI > 120 W/m²
393
+ - location: EPWLocation metadata
394
+
395
+ Example:
396
+ >>> stats = estimate_annual_irradiance("weather.epw")
397
+ >>> print(f"Annual GHI: {stats['total_ghi_kwh_m2']:.0f} kWh/m²")
398
+ """
399
+ solar_data = read_epw_solar_data(epw_path)
400
+
401
+ # Sum up Wh values and convert to kWh
402
+ total_dni = np.sum(solar_data.dni) / 1000.0
403
+ total_dhi = np.sum(solar_data.dhi) / 1000.0
404
+ total_ghi = np.sum(solar_data.ghi) / 1000.0 if solar_data.ghi is not None else np.nan
405
+
406
+ # Peak values
407
+ peak_dni = np.max(solar_data.dni)
408
+ peak_ghi = np.max(solar_data.ghi) if solar_data.ghi is not None else np.nan
409
+
410
+ # Sunshine hours (typical threshold: 120 W/m² DNI)
411
+ sunshine_hours = np.sum(solar_data.dni > 120)
412
+
413
+ return {
414
+ 'total_dni_kwh_m2': total_dni,
415
+ 'total_dhi_kwh_m2': total_dhi,
416
+ 'total_ghi_kwh_m2': total_ghi,
417
+ 'peak_dni': peak_dni,
418
+ 'peak_ghi': peak_ghi,
419
+ 'sunshine_hours': int(sunshine_hours),
420
+ 'location': solar_data.location
421
+ }