voxcity.simulator_gpu.solar.integration.utils ============================================= .. py:module:: voxcity.simulator_gpu.solar.integration.utils .. autoapi-nested-parse:: Common utility functions for VoxCity solar integration module. This module contains shared helper functions used across ground, building, and volumetric solar irradiance calculations to reduce code duplication. Attributes ---------- .. autoapisummary:: voxcity.simulator_gpu.solar.integration.utils.VOXCITY_GROUND_CODE voxcity.simulator_gpu.solar.integration.utils.VOXCITY_TREE_CODE voxcity.simulator_gpu.solar.integration.utils.VOXCITY_BUILDING_CODE Classes ------- .. toctree:: :hidden: /autoapi/voxcity/simulator_gpu/solar/integration/utils/ArrayWithMetadata .. autoapisummary:: voxcity.simulator_gpu.solar.integration.utils.ArrayWithMetadata Functions --------- .. autoapisummary:: voxcity.simulator_gpu.solar.integration.utils.get_location_from_voxcity voxcity.simulator_gpu.solar.integration.utils.convert_voxel_data_to_arrays voxcity.simulator_gpu.solar.integration.utils.compute_valid_ground_vectorized voxcity.simulator_gpu.solar.integration.utils.compute_ground_k_from_voxels voxcity.simulator_gpu.solar.integration.utils.compute_sun_direction voxcity.simulator_gpu.solar.integration.utils.parse_time_period voxcity.simulator_gpu.solar.integration.utils.filter_df_to_period voxcity.simulator_gpu.solar.integration.utils.get_hour_range_from_period voxcity.simulator_gpu.solar.integration.utils.get_timezone_offset_from_location voxcity.simulator_gpu.solar.integration.utils.generate_annual_hourly_dataframe voxcity.simulator_gpu.solar.integration.utils.load_epw_data voxcity.simulator_gpu.solar.integration.utils.get_solar_positions_astral voxcity.simulator_gpu.solar.integration.utils.extract_terrain_following_slice voxcity.simulator_gpu.solar.integration.utils.accumulate_terrain_following_slice voxcity.simulator_gpu.solar.integration.utils.add_metadata_to_array voxcity.simulator_gpu.solar.integration.utils.compute_boundary_vertical_mask voxcity.simulator_gpu.solar.integration.utils.apply_computation_mask_to_faces Module Contents --------------- .. py:data:: VOXCITY_GROUND_CODE :value: -1 .. py:data:: VOXCITY_TREE_CODE :value: -2 .. py:data:: VOXCITY_BUILDING_CODE :value: -3 .. py:function:: get_location_from_voxcity(voxcity, default_lat: float = 1.35, default_lon: float = 103.82) -> Tuple[float, float] Extract latitude/longitude from VoxCity object or return defaults. :param voxcity: VoxCity object with extras containing rectangle_vertices :param default_lat: Default latitude if not found (Singapore) :param default_lon: Default longitude if not found (Singapore) :returns: Tuple of (origin_lat, origin_lon) .. py:function:: convert_voxel_data_to_arrays(voxel_data: numpy.ndarray, default_lad: float = 1.0) -> Tuple[numpy.ndarray, numpy.ndarray] Convert VoxCity voxel codes to is_solid and LAD arrays using vectorized operations. This is 10-100x faster than triple-nested Python loops for large grids. :param voxel_data: 3D array of VoxCity voxel class codes :param default_lad: Default Leaf Area Density for tree voxels (m²/m³) :returns: Tuple of (is_solid, lad) numpy arrays with same shape as voxel_data .. py:function:: compute_valid_ground_vectorized(voxel_data: numpy.ndarray) -> Tuple[numpy.ndarray, numpy.ndarray] Compute valid ground mask and ground k-levels using vectorized operations. Valid ground cells are those where: - The transition from solid to air/tree occurs - The solid below is not water (7,8,9) or building/underground (negative codes) :param voxel_data: 3D array of VoxCity voxel class codes (ni, nj, nk) :returns: Tuple of (valid_ground 2D bool array, ground_k 2D int array) ground_k[i,j] = -1 means no valid ground found .. py:function:: compute_ground_k_from_voxels(voxel_data: numpy.ndarray) -> numpy.ndarray Compute ground surface k-level for each (i,j) cell from voxel data. This finds the terrain top - the highest k where the cell below the first air cell is solid ground (not building). This is used for terrain-following height extraction in volumetric calculations. :param voxel_data: 3D array of voxel class codes :returns: 2D array of ground k-levels (ni, nj). -1 means no valid ground found. .. py:function:: compute_sun_direction(azimuth_degrees_ori: float, elevation_degrees: float, rotation_angle: float = 0) -> Tuple[float, float, float, float] Compute sun direction vector from azimuth and elevation angles. :param azimuth_degrees_ori: Solar azimuth in VoxCity convention (0=North, clockwise) :param elevation_degrees: Solar elevation in degrees above horizon :param rotation_angle: Grid rotation angle in degrees (clockwise, from voxcity.extras) :returns: Tuple of (sun_dir_x, sun_dir_y, sun_dir_z, cos_zenith) .. py:function:: parse_time_period(start_time: str, end_time: str) -> Tuple[datetime.datetime, datetime.datetime] Parse start and end time strings. :param start_time: Start time in format 'MM-DD HH:MM:SS' :param end_time: End time in format 'MM-DD HH:MM:SS' :returns: Tuple of (start_dt, end_dt) datetime objects :raises ValueError: If time format is invalid .. py:function:: filter_df_to_period(df, start_time: str, end_time: str, tz: float) Filter weather DataFrame to specified time period and convert to UTC. :param df: pandas DataFrame with datetime index :param start_time: Start time in format 'MM-DD HH:MM:SS' :param end_time: End time in format 'MM-DD HH:MM:SS' :param tz: Timezone offset in hours :returns: Tuple of (df_period_utc, df with hour_of_year column) :raises ValueError: If time format is invalid or no data in period .. py:function:: get_hour_range_from_period(start_time: str, end_time: str) -> Tuple[int, int] Get hour-of-year range from time period strings. :param start_time: Start time in format 'MM-DD HH:MM:SS' :param end_time: End time in format 'MM-DD HH:MM:SS' :returns: Tuple of (start_hour, end_hour) as hour-of-year values .. py:function:: get_timezone_offset_from_location(lon: float, lat: float) -> float Get UTC timezone offset (in hours) from longitude/latitude using timezonefinder. Falls back to a simple longitude-based estimate if timezonefinder is not available. :param lon: Longitude in degrees :param lat: Latitude in degrees :returns: Timezone offset in hours (e.g. 9.0 for JST, -5.0 for EST) .. py:function:: generate_annual_hourly_dataframe(year: int = 2020) Generate a pandas DataFrame with hourly timestamps for a full year. The DataFrame has a datetime index (timezone-naive) and no weather columns, suitable for DSH (Direct Sun Hours) calculations that only need solar position data and do not require weather/EPW data. :param year: The year to generate timestamps for (default: 2020, a non-leap year is fine since solar geometry varies negligibly between years) :returns: pandas DataFrame with hourly datetime index spanning the full year .. py:function:: load_epw_data(epw_file_path: Optional[str] = None, download_nearest_epw: bool = False, voxcity=None, **kwargs) -> Tuple Load EPW weather data, optionally downloading the nearest file. :param epw_file_path: Path to EPW file (required if download_nearest_epw=False) :param download_nearest_epw: If True, download nearest EPW based on location :param voxcity: VoxCity object (needed for location when downloading) :param \*\*kwargs: Additional parameters (output_dir, max_distance, rectangle_vertices) :returns: Tuple of (df, lon, lat, tz) where df is the weather DataFrame :raises ValueError: If EPW file not provided and download_nearest_epw=False :raises ImportError: If required modules not available .. py:function:: get_solar_positions_astral(times, lon: float, lat: float) Compute solar azimuth and elevation for given times and location using Astral. :param times: Pandas DatetimeIndex of times (should be timezone-aware, preferably UTC) :param lon: Longitude in degrees :param lat: Latitude in degrees :returns: DataFrame indexed by times with columns ['azimuth', 'elevation'] in degrees .. py:function:: extract_terrain_following_slice(flux_3d: numpy.ndarray, ground_k: numpy.ndarray, height_offset_k: int, is_solid: numpy.ndarray) -> numpy.ndarray Extract a terrain-following 2D slice from a 3D flux field (vectorized). For each (i,j), extracts the value at ground_k[i,j] + height_offset_k. Cells that are solid at the extraction point, have no valid ground, or are above the domain are marked as NaN. :param flux_3d: 3D array of flux values (ni, nj, nk) :param ground_k: 2D array of ground k-levels (ni, nj), -1 means no valid ground :param height_offset_k: Number of cells above ground to extract :param is_solid: 3D array marking solid cells (ni, nj, nk) :returns: 2D array of extracted values (ni, nj) with NaN for invalid cells .. py:function:: accumulate_terrain_following_slice(cumulative_map: numpy.ndarray, flux_3d: numpy.ndarray, ground_k: numpy.ndarray, height_offset_k: int, is_solid: numpy.ndarray, weight: float = 1.0) -> None Accumulate terrain-following values from a 3D flux field into a 2D map (vectorized, in-place). For each (i,j), adds flux_3d[i,j,k_extract] * weight to cumulative_map[i,j] where k_extract = ground_k[i,j] + height_offset_k. :param cumulative_map: 2D array to accumulate into (ni, nj), modified in-place :param flux_3d: 3D array of flux values (ni, nj, nk) :param ground_k: 2D array of ground k-levels (ni, nj), -1 means no valid ground :param height_offset_k: Number of cells above ground to extract :param is_solid: 3D array marking solid cells (ni, nj, nk) :param weight: Multiplier for values before accumulating (e.g., time_step_hours) .. py:function:: add_metadata_to_array(arr: numpy.ndarray, metadata: dict) -> numpy.ndarray Add metadata dict to a numpy array as an attribute. :param arr: Input numpy array :param metadata: Dictionary of metadata to attach :returns: Array with metadata attribute .. py:function:: compute_boundary_vertical_mask(mesh_face_centers: numpy.ndarray, mesh_face_normals: numpy.ndarray, grid_bounds: numpy.ndarray, boundary_epsilon: float) -> numpy.ndarray Compute mask for vertical faces on domain boundary. :param mesh_face_centers: (N, 3) array of face center coordinates :param mesh_face_normals: (N, 3) array of face normal vectors :param grid_bounds: (2, 3) array of [[min_x, min_y, min_z], [max_x, max_y, max_z]] :param boundary_epsilon: Tolerance for boundary detection :returns: Boolean mask (N,) - True for vertical boundary faces .. py:function:: apply_computation_mask_to_faces(values: numpy.ndarray, mesh_face_centers: numpy.ndarray, computation_mask: numpy.ndarray, meshsize: float, grid_shape: Tuple[int, int]) -> numpy.ndarray Apply 2D computation mask to mesh face values. :param values: (N,) array of face values :param mesh_face_centers: (N, 3) array of face center coordinates :param computation_mask: 2D boolean mask matching grid_shape :param meshsize: Grid cell size :param grid_shape: (ny_vc, nx_vc) grid dimensions :returns: Modified values array with NaN for masked-out faces