import numpy as np import pandas as pd import logging import math from ladybug_geometry.geometry3d.pointvector import Point3D, Vector3D from ladybug_geometry.geometry3d.plane import Plane from ladybug_geometry.geometry3d.polyface import Polyface3D import pvlib from Utilities.Processes import calculate_no_of_panels logger = logging.getLogger(__name__) def define_grid_layout(c): # get number of panels required no_of_panels = calculate_no_of_panels( c["array"]["system_size"], c["panel"]["peak_power"] ) # get maximum number of panels based on spacing and dimensions max__panels_per_row = np.floor( ( c["environment"]["roof"]["dimensions"]["width"] - 2 * c["array"]["edge_setback"] ) / c["panel"]["dimensions"]["width"] ) max_number_of_rows = np.floor( ( c["environment"]["roof"]["dimensions"]["length"] - 2 * c["array"]["edge_setback"] ) / (c["array"]["spacing"] + c["panel"]["dimensions"]["thickness"]) ) max_no_of_panels = max__panels_per_row * max_number_of_rows logger.info( f"Number of panels required: {no_of_panels}, Maximum panels possible: {max_no_of_panels}" ) if no_of_panels > max_no_of_panels: no_of_panels = max_no_of_panels logger.warning( f"Number of panels required exceeds maximum possible. Setting number of panels to {no_of_panels}." ) else: logger.info( f"Number of panels required is within the maximum possible. Setting number of panels to {no_of_panels}." ) # coordinate of panel determined by bottom left corner # x - row wise position, y - column wise position, z - height # first panel in row 1 is at (0, 0, 0) # nth panel in row 1 is at ((n-1)*panel_width, 0, 0) # first panel in nth row is at (0, (n-1)*(panel_thickness + spacing), 0) # create matrices for x, y, z coordinates of panels x = [] y = [] z = [] counter = 0 for j in range(int(max_number_of_rows)): for i in range(int(max__panels_per_row)): if counter < no_of_panels: x.append(i * c["panel"]["dimensions"]["width"]) y.append( j * (c["panel"]["dimensions"]["thickness"] + c["array"]["spacing"]) ) z.append(0) counter += 1 else: break coordinates = pd.DataFrame( { "x": x, "y": y, "z": z, } ) return coordinates def create_panels(coordinates, c): panel_width = c["panel"]["dimensions"]["width"] panel_length = c["panel"]["dimensions"]["length"] panel_thickness = c["panel"]["dimensions"]["thickness"] # if viewed from above, and assumming the roof is a rectangle, the # global origin is at the bottom left corner of the roof # For a vertical panel: # - The vertical direction (panel height) is along the Z-axis. y_axis = Vector3D(0, 1, 0) # points east, therefore front face is east facing # - The horizontal direction along the panel's width. # Here, we assume the width runs in the positive X-direction. x_axis = Vector3D(1, 0, 0) # points north panels = [] for index, row in coordinates.iterrows(): # Create the bottom-left corner of the panel panel_origin = Point3D(row["x"], row["y"], row["z"]) # Create the plane for the panel panel_plane = Plane(o=panel_origin, n=y_axis, x=x_axis) # Create the panel geometry panel = Polyface3D.from_box( width=panel_width, depth=panel_length, height=panel_thickness, base_plane=panel_plane, ) panels.append(panel) return panels def get_solar_data(c): logger.info( f"Getting solar position data for {c['simulation_date_time']['start']} to {c['simulation_date_time']['end']}" ) """ Function to get solar position from PVLib """ latitude = c["environment"]["location"]["latitude"] longitude = c["environment"]["location"]["longitude"] tz = c["simulation_date_time"]["tz"] times = pd.date_range( c["simulation_date_time"]["start"], c["simulation_date_time"]["end"], freq="15min", tz=tz, ) # Get solar position data using PVLib solar_positions = pvlib.solarposition.get_solarposition(times, latitude, longitude) return solar_positions def calculate_sun_vector(solar_zenith, solar_azimuth): """ Calculate the sun vector from solar zenith and azimuth angles. Args: solar_zenith (float): Solar zenith angle in degrees. solar_azimuth (float): Solar azimuth angle in degrees. Returns: Vector3D: Sun vector as a 3D vector. """ # Convert angles from degrees to radians zenith_rad = math.radians(solar_zenith) azimuth_rad = math.radians(solar_azimuth) # Calculate the sun vector components x = math.sin(zenith_rad) * math.cos(azimuth_rad) y = math.sin(zenith_rad) * math.sin(azimuth_rad) z = math.cos(zenith_rad) return Vector3D(x, y, z) def compute_array_shading(panels, sun_vector, n_samples=25): """ Given a list of panel geometries (Polyface3D) and the sun vector, compute the shading fraction for each panel and return the overall average shading. Parameters: panels: List of Polyface3D objects representing the PV panels. sun_vector: Unit Vector3D in the direction of the sun. n_samples: Number of sample points per panel. Returns: Dictionary mapping panel index to its shading fraction, and the overall average. """ shading_results = {} for i, panel in enumerate(panels): # Define obstacles as all other panels in the array obstacles = [pan for j, pan in enumerate(panels) if j != i] shading_frac = calculate_shading_fraction( panel, sun_vector, obstacles, n_samples=n_samples ) shading_results[i] = shading_frac # Compute the overall average shading fraction across all panels: overall_avg = np.mean(list(shading_results.values())) return shading_results, overall_avg def calculate_shading_fraction(c): coordinates = define_grid_layout(c) panels = create_panels(coordinates, c) solar_positions = get_solar_data(c) shading_fractions = [] for panel in panels: shading_fraction = [] for index, row in solar_positions.iterrows(): # Get the solar position for the current time step # in a sphere, azimuth is the angle in the x-y plane from the north # and zenith is the angle from the vertical axis solar_zenith = row["apparent_zenith"] solar_azimuth = row["apparent_azimuth"] sun_vector = calculate_sun_vector(solar_zenith, solar_azimuth) # Calculate the shading fraction using the panel and solar position shading_fraction.append(panel.shading_fraction(solar_zenith, solar_azimuth)) shading_fractions.append(shading_fraction) return shading_fractions