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.
- voxcity/__init__.py +14 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- 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/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {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
|
+
}
|