import numpy as np import pandas as pd import logging import math import matplotlib.pyplot as pl import pvlib from pvfactors.geometry import OrderedPVArray from pvfactors.engine import PVEngine from Utilities.Processes import calculate_no_of_panels, calculate_required_system_size logger = logging.getLogger(__name__) def get_location(c): location = pvlib.location.Location( latitude=c["environment"]["location"]["latitude"], longitude=c["environment"]["location"]["longitude"], tz=c["simulation_date_time"]["tz"], ) return location def define_grid_layout(c, panel_tilt): no_of_panels = calculate_no_of_panels( c["array"]["system_size"], c["panel"]["peak_power"] ) # calculate pitch pitch = c["array"]["spacing"] # calculate minimum pitch if we don't want panel overlap at all min_pitch = np.round( c["panel"]["dimensions"]["length"] * math.cos(panel_tilt / 180 * math.pi), 1 ) if pitch < min_pitch: logger.warning( f"Spacing is less than minimum pitch. Setting spacing to {min_pitch}." ) pitch = min_pitch logger.info(f"Pitch between panels: {pitch}m") # 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"]) ) / c["panel"]["dimensions"]["width"] ) max_number_of_rows = np.floor( ( c["environment"]["roof"]["dimensions"]["length"] - (2 * c["array"]["edge_setback"]) ) / pitch ) 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 * pitch) z.append(0) counter += 1 else: break coordinates = pd.DataFrame( { "x": x, "y": y, "z": z, } ) return coordinates, no_of_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 """ location = get_location(c) times = pd.date_range( c["simulation_date_time"]["start"], c["simulation_date_time"]["end"], freq="15min", tz=location.tz, ) # Get solar position data using PVLib solar_positions = location.get_solarposition(times) # filter solar positions to only include times when the sun is above the horizon solar_positions = solar_positions[solar_positions["apparent_elevation"] > 0] # get datetime range from solar_positions day_times = solar_positions.index clearsky_data = location.get_clearsky(day_times) return solar_positions, clearsky_data def sanity_check_minimum_pitch(c): solar_positions, _ = get_solar_data(c) zenith = solar_positions["zenith"].values solar_positions["shadow_length"] = (c["panel"]["dimensions"]["length"]) * np.tan( zenith / 180 * np.pi ) return solar_positions def calculate_energy_production(c, orientation): orientation = c["array"]["orientation"][orientation] c = calculate_required_system_size(c) panel_coordinates, no_of_panels = define_grid_layout( c, panel_tilt=orientation["panel_tilt"] ) solar_positions, clearsky_data = get_solar_data(c) # the first row is always not shaded so exclude no_of_rows = np.unique(panel_coordinates["y"]).shape[0] no_of_shaded_rows = no_of_rows - 1 no_of_panels_in_row = np.unique(panel_coordinates["x"]).shape[0] collector_width = c["panel"]["dimensions"]["length"] # calculate delta between unique y coordinates of panels to get pitch pitch = np.unique(panel_coordinates["y"])[1] - np.unique(panel_coordinates["y"])[0] surface_azimuth = orientation["surface_azimuth"] axis_azimuth = orientation["axis_azimuth"] gcr = np.divide(c["panel"]["dimensions"]["length"], pitch) gcr = min(1, gcr) logger.info(f"Ground coverage ratio: {gcr}") pvrow_height = ( c["panel"]["dimensions"]["length"] * orientation["pvrow_height_ratio_to_length"] ) pvarray_parameters = { "n_pvrows": 3, # number of pv rows "pvrow_height": pvrow_height, # height of pvrows (measured at center / torque tube) "pvrow_width": c["panel"]["dimensions"]["length"], # width of pvrows "axis_azimuth": axis_azimuth, # azimuth angle of rotation axis "gcr": gcr, # ground coverage ratio } pvarray = OrderedPVArray.init_from_dict(pvarray_parameters) engine = PVEngine(pvarray) inputs = pd.DataFrame( { "dni": clearsky_data["dni"], "dhi": clearsky_data["dhi"], "solar_zenith": solar_positions["zenith"], "solar_azimuth": solar_positions["azimuth"], "surface_tilt": np.repeat(orientation["panel_tilt"], len(solar_positions)), "surface_azimuth": np.repeat(surface_azimuth, len(solar_positions)), } ) inputs.index = clearsky_data.index inputs.index.name = "index" engine.fit( inputs.index, inputs.dni, inputs.dhi, inputs.solar_zenith, inputs.solar_azimuth, inputs.surface_tilt, inputs.surface_azimuth, albedo=0.2, ) pvarray = engine.run_full_mode(fn_build_report=lambda pvarray: pvarray) gamma_pdc = c["panel"]["temperature_coefficient"] temp_cell = c["panel"]["nominal_operating_cell_temperature"] p_row = no_of_panels_in_row * c["panel"]["peak_power"] p_middle_rows = (no_of_panels - 2 * no_of_panels_in_row) * c["panel"]["peak_power"] pdc_first_row_front = pvlib.pvsystem.pvwatts_dc( pdc0=p_row, gamma_pdc=gamma_pdc, temp_cell=temp_cell, g_poa_effective=pvarray.ts_pvrows[0].front.get_param_weighted("qinc"), ) pdc_last_row_front = pvlib.pvsystem.pvwatts_dc( pdc0=p_row, gamma_pdc=gamma_pdc, temp_cell=temp_cell, g_poa_effective=pvarray.ts_pvrows[2].front.get_param_weighted("qinc"), ) pdc_middle_rows_front = pvlib.pvsystem.pvwatts_dc( pdc0=p_middle_rows, gamma_pdc=gamma_pdc, temp_cell=temp_cell, g_poa_effective=pvarray.ts_pvrows[1].front.get_param_weighted("qinc"), ) pdc_front = pdc_first_row_front + pdc_last_row_front + pdc_middle_rows_front total_hourly_energy = pdc * 15 / 60 / 1e3 # convert to kWh total_energy = total_hourly_energy.sum() logger.info(f"Total energy yield calculated: {total_energy} kWh") return total_hourly_energy, no_of_panels