2025-04-11 21:58:24 +01:00

235 lines
7.7 KiB
Python

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