235 lines
7.7 KiB
Python
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
|