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 import matplotlib.animation as animation 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 = c["panel"]["dimensions"]["length"] * math.cos( panel_tilt / 180 * math.pi ) 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) f, ax = pl.subplots(figsize=(10, 3)) def update(frame): ax.clear() pvarray.plot_at_idx(frame, ax, with_surface_index=True) ax.set_title(inputs.index[frame]) return ax ani = animation.FuncAnimation( f, update, frames=len(inputs.index), interval=100, repeat=True ) pl.show() 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 = pvlib.pvsystem.pvwatts_dc( pdc0=p_row, gamma_pdc=gamma_pdc, temp_cell=temp_cell, g_poa_effective=POA_data.at[0], ) pdc_last_row = pvlib.pvsystem.pvwatts_dc( pdc0=p_row, gamma_pdc=gamma_pdc, temp_cell=temp_cell, g_poa_effective=POA_data.at[2], ) pdc_middle_rows = pvlib.pvsystem.pvwatts_dc( pdc0=p_middle_rows, gamma_pdc=gamma_pdc, temp_cell=temp_cell, g_poa_effective=POA_data.at[1], ) pdc = pdc_first_row + pdc_last_row + pdc_middle_rows 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