wip using pvfactors engine simulation
This commit is contained in:
parent
e9d426e6ec
commit
2504b4e5fc
@ -1,4 +1,4 @@
|
|||||||
from Utilities.Shading import calculate_energy_production_vertical
|
from Utilities.Shading import calculate_energy_production
|
||||||
from scipy.optimize import minimize
|
from scipy.optimize import minimize
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ def optimise_vertical_panel_pitch(c):
|
|||||||
pitch += c["panel"]["dimensions"]["thickness"]
|
pitch += c["panel"]["dimensions"]["thickness"]
|
||||||
c["array"]["spacing"] = pitch
|
c["array"]["spacing"] = pitch
|
||||||
logging.info(f"Optimizing with pitch: {pitch}m")
|
logging.info(f"Optimizing with pitch: {pitch}m")
|
||||||
vertical_energy, _ = calculate_energy_production_vertical(c)
|
vertical_energy, _ = calculate_energy_production(c, "vertical")
|
||||||
total_energy_yield = vertical_energy.sum()
|
total_energy_yield = vertical_energy.sum()
|
||||||
logger.info(f"Total energy yield for pitch {pitch}m: {total_energy_yield}kWh")
|
logger.info(f"Total energy yield for pitch {pitch}m: {total_energy_yield}kWh")
|
||||||
return -total_energy_yield
|
return -total_energy_yield
|
||||||
@ -36,5 +36,5 @@ def optimise_vertical_panel_pitch(c):
|
|||||||
optimal_pitch = result.x[0]
|
optimal_pitch = result.x[0]
|
||||||
c["array"]["spacing"] = optimal_pitch
|
c["array"]["spacing"] = optimal_pitch
|
||||||
logger.info(f"Optimal pitch found: {optimal_pitch}m")
|
logger.info(f"Optimal pitch found: {optimal_pitch}m")
|
||||||
vetical_energy, no_of_panels = calculate_energy_production_vertical(c)
|
vetical_energy, no_of_panels = calculate_energy_production(c, "vertical")
|
||||||
return (optimal_pitch, vetical_energy, no_of_panels)
|
return (optimal_pitch, vetical_energy, no_of_panels)
|
||||||
|
@ -5,7 +5,9 @@ import math
|
|||||||
import matplotlib.pyplot as pl
|
import matplotlib.pyplot as pl
|
||||||
|
|
||||||
import pvlib
|
import pvlib
|
||||||
from pvlib.bifacial.pvfactors import pvfactors_timeseries
|
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
|
from Utilities.Processes import calculate_no_of_panels, calculate_required_system_size
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ def define_grid_layout(c, panel_tilt):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# calculate pitch
|
# calculate pitch
|
||||||
pitch = c["array"]["spacing"] + c["panel"]["dimensions"]["thickness"]
|
pitch = c["array"]["spacing"]
|
||||||
# calculate minimum pitch if we don't want panel overlap at all
|
# calculate minimum pitch if we don't want panel overlap at all
|
||||||
min_pitch = c["panel"]["dimensions"]["length"] * math.cos(
|
min_pitch = c["panel"]["dimensions"]["length"] * math.cos(
|
||||||
panel_tilt / 180 * math.pi
|
panel_tilt / 180 * math.pi
|
||||||
@ -136,9 +138,12 @@ def sanity_check_minimum_pitch(c):
|
|||||||
return solar_positions
|
return solar_positions
|
||||||
|
|
||||||
|
|
||||||
def calculate_energy_production_vertical(c):
|
def calculate_energy_production(c, orientation):
|
||||||
|
orientation = c["array"]["orientation"][orientation]
|
||||||
c = calculate_required_system_size(c)
|
c = calculate_required_system_size(c)
|
||||||
panel_coordinates, no_of_panels = define_grid_layout(c, panel_tilt=90)
|
panel_coordinates, no_of_panels = define_grid_layout(
|
||||||
|
c, panel_tilt=orientation["panel_tilt"]
|
||||||
|
)
|
||||||
solar_positions, clearsky_data = get_solar_data(c)
|
solar_positions, clearsky_data = get_solar_data(c)
|
||||||
|
|
||||||
# the first row is always not shaded so exclude
|
# the first row is always not shaded so exclude
|
||||||
@ -150,43 +155,66 @@ def calculate_energy_production_vertical(c):
|
|||||||
collector_width = c["panel"]["dimensions"]["length"]
|
collector_width = c["panel"]["dimensions"]["length"]
|
||||||
# calculate delta between unique y coordinates of panels to get pitch
|
# calculate delta between unique y coordinates of panels to get pitch
|
||||||
pitch = np.unique(panel_coordinates["y"])[1] - np.unique(panel_coordinates["y"])[0]
|
pitch = np.unique(panel_coordinates["y"])[1] - np.unique(panel_coordinates["y"])[0]
|
||||||
surface_to_axis_offset = 0
|
surface_azimuth = orientation["surface_azimuth"]
|
||||||
shaded_row_rotation = 90
|
axis_azimuth = orientation["axis_azimuth"]
|
||||||
shading_row_rotation = 90
|
|
||||||
surface_azimuth = 90 # east facing
|
|
||||||
axis_tilt = 0
|
|
||||||
axis_azimuth = 180
|
|
||||||
|
|
||||||
gcr = np.divide(c["panel"]["dimensions"]["length"], pitch)
|
gcr = np.divide(c["panel"]["dimensions"]["length"], pitch)
|
||||||
gcr = min(1, gcr)
|
gcr = min(1, gcr)
|
||||||
logger.info(f"Ground coverage ratio: {gcr}")
|
logger.info(f"Ground coverage ratio: {gcr}")
|
||||||
|
|
||||||
# use pvfactors bifacial modelling package
|
pvrow_height = (
|
||||||
POA_data = pd.Series(dtype=object)
|
c["panel"]["dimensions"]["length"] * orientation["pvrow_height_ratio_to_length"]
|
||||||
for row in range(0, 3):
|
)
|
||||||
result = pvfactors_timeseries(
|
|
||||||
solar_zenith=solar_positions["apparent_zenith"],
|
|
||||||
solar_azimuth=solar_positions["azimuth"],
|
|
||||||
surface_azimuth=surface_azimuth,
|
|
||||||
surface_tilt=90,
|
|
||||||
axis_azimuth=axis_azimuth,
|
|
||||||
timestamps=solar_positions.index,
|
|
||||||
dni=clearsky_data["dni"],
|
|
||||||
dhi=clearsky_data["dhi"],
|
|
||||||
gcr=gcr,
|
|
||||||
pvrow_height=c["panel"]["dimensions"]["length"],
|
|
||||||
pvrow_width=c["panel"]["dimensions"]["width"] * no_of_panels_in_row,
|
|
||||||
albedo=0.2,
|
|
||||||
n_pvrows=3,
|
|
||||||
index_observed_pvrow=row,
|
|
||||||
)
|
|
||||||
# set negative values to 0
|
|
||||||
poa_front = result[2].clip(lower=0)
|
|
||||||
poa_rear = result[3].clip(lower=0)
|
|
||||||
poa_global = poa_front + poa_rear * c["panel"]["bifaciality"]
|
|
||||||
POA_data.at[row] = poa_global
|
|
||||||
|
|
||||||
total_hourly_irradiance = POA_data.at[0] + POA_data.at[2] + (POA_data.at[1] * 20)
|
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"]
|
gamma_pdc = c["panel"]["temperature_coefficient"]
|
||||||
temp_cell = c["panel"]["nominal_operating_cell_temperature"]
|
temp_cell = c["panel"]["nominal_operating_cell_temperature"]
|
||||||
p_row = no_of_panels_in_row * c["panel"]["peak_power"]
|
p_row = no_of_panels_in_row * c["panel"]["peak_power"]
|
||||||
@ -217,76 +245,3 @@ def calculate_energy_production_vertical(c):
|
|||||||
logger.info(f"Total energy yield calculated: {total_energy} kWh")
|
logger.info(f"Total energy yield calculated: {total_energy} kWh")
|
||||||
|
|
||||||
return total_hourly_energy, no_of_panels
|
return total_hourly_energy, no_of_panels
|
||||||
|
|
||||||
|
|
||||||
def calculate_energy_production_horizontal(c):
|
|
||||||
c["array"]["system_size"] = (
|
|
||||||
c["array"]["system_size"] * c["array"]["horizontal_max_capacity"]
|
|
||||||
)
|
|
||||||
panel_coordinates, no_of_panels = define_grid_layout(c, panel_tilt=0)
|
|
||||||
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
|
|
||||||
|
|
||||||
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_to_axis_offset = 0
|
|
||||||
shaded_row_rotation = 0
|
|
||||||
shading_row_rotation = 0
|
|
||||||
axis_tilt = 0
|
|
||||||
axis_azimuth = 270 # south facing surface
|
|
||||||
|
|
||||||
projected_solar_zenith = pvlib.shading.projected_solar_zenith_angle(
|
|
||||||
solar_zenith=solar_positions["apparent_zenith"],
|
|
||||||
solar_azimuth=solar_positions["azimuth"],
|
|
||||||
axis_azimuth=axis_azimuth,
|
|
||||||
axis_tilt=axis_tilt,
|
|
||||||
)
|
|
||||||
|
|
||||||
shaded_fraction = pvlib.shading.shaded_fraction1d(
|
|
||||||
solar_zenith=projected_solar_zenith,
|
|
||||||
solar_azimuth=solar_positions["azimuth"],
|
|
||||||
axis_azimuth=axis_azimuth,
|
|
||||||
shaded_row_rotation=shaded_row_rotation,
|
|
||||||
shading_row_rotation=shading_row_rotation,
|
|
||||||
collector_width=collector_width,
|
|
||||||
pitch=pitch,
|
|
||||||
surface_to_axis_offset=surface_to_axis_offset,
|
|
||||||
axis_tilt=axis_tilt,
|
|
||||||
)
|
|
||||||
shaded_fraction = shaded_fraction * no_of_shaded_rows / no_of_rows
|
|
||||||
|
|
||||||
poa = pvlib.irradiance.get_total_irradiance(
|
|
||||||
surface_tilt=0,
|
|
||||||
surface_azimuth=180,
|
|
||||||
solar_zenith=projected_solar_zenith,
|
|
||||||
solar_azimuth=solar_positions["azimuth"],
|
|
||||||
dni=clearsky_data["dni"],
|
|
||||||
ghi=clearsky_data["ghi"],
|
|
||||||
dhi=clearsky_data["dhi"],
|
|
||||||
surface_type="urban",
|
|
||||||
)
|
|
||||||
poa = poa.dropna(subset=["poa_global"])
|
|
||||||
|
|
||||||
effective_front = poa["poa_global"] * (1 - shaded_fraction)
|
|
||||||
|
|
||||||
system_size = c["panel"]["peak_power"] * no_of_panels
|
|
||||||
|
|
||||||
pdc0 = system_size
|
|
||||||
gamma_pdc = c["panel"]["temperature_coefficient"]
|
|
||||||
temp_cell = c["panel"]["nominal_operating_cell_temperature"]
|
|
||||||
pdc = pvlib.pvsystem.pvwatts_dc(
|
|
||||||
pdc0=pdc0,
|
|
||||||
gamma_pdc=gamma_pdc,
|
|
||||||
temp_cell=temp_cell,
|
|
||||||
g_poa_effective=effective_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
|
|
||||||
|
12
config.yml
12
config.yml
@ -5,8 +5,18 @@ array:
|
|||||||
edge_setback: 1.8 # distance from the edge of the roof to the array
|
edge_setback: 1.8 # distance from the edge of the roof to the array
|
||||||
roof_slope: 0
|
roof_slope: 0
|
||||||
slope: 0 # degrees from horizontal (+ve means shaded row is higher than the row in front)
|
slope: 0 # degrees from horizontal (+ve means shaded row is higher than the row in front)
|
||||||
horizontal_max_capacity: 0.75 # scale down due to peak power demand limit of NOVA
|
|
||||||
performance_ratio: 0.9 # ratio of actual energy output to the theoretical maximum
|
performance_ratio: 0.9 # ratio of actual energy output to the theoretical maximum
|
||||||
|
orientation:
|
||||||
|
vertical:
|
||||||
|
surface_azimuth: 90 # degrees from North (clockwise)
|
||||||
|
axis_azimuth: 180 # degrees from North (clockwise)
|
||||||
|
panel_tilt: 90
|
||||||
|
pvrow_height_ratio_to_length: 0.5
|
||||||
|
horizontal:
|
||||||
|
surface_azimuth: 180 # degrees from North (clockwise)
|
||||||
|
axis_azimuth: 270 # degrees from North (clockwise)
|
||||||
|
panel_tilt: 0
|
||||||
|
pvrow_height_ratio_to_length: 0
|
||||||
|
|
||||||
simulation_date_time:
|
simulation_date_time:
|
||||||
start: 2025-03-30 00:00 # start date and time in ISO 8601 format
|
start: 2025-03-30 00:00 # start date and time in ISO 8601 format
|
||||||
|
7
main.py
7
main.py
@ -5,8 +5,7 @@ import numpy as np
|
|||||||
import matplotlib.pyplot as pl
|
import matplotlib.pyplot as pl
|
||||||
import matplotlib.dates as mdates
|
import matplotlib.dates as mdates
|
||||||
from Utilities.Shading import (
|
from Utilities.Shading import (
|
||||||
calculate_energy_production_horizontal,
|
calculate_energy_production,
|
||||||
calculate_energy_production_vertical,
|
|
||||||
sanity_check_minimum_pitch,
|
sanity_check_minimum_pitch,
|
||||||
)
|
)
|
||||||
from Utilities.Optimisation import optimise_vertical_panel_pitch
|
from Utilities.Optimisation import optimise_vertical_panel_pitch
|
||||||
@ -41,7 +40,9 @@ logger.debug(f"Vertical Energy Production: {vertical_energy.sum()}")
|
|||||||
logger.debug("Number of panels: %d", no_of_panels_vertical)
|
logger.debug("Number of panels: %d", no_of_panels_vertical)
|
||||||
logger.debug(f"System size: {no_of_panels_vertical * c['panel']['peak_power']/1e3} kWp")
|
logger.debug(f"System size: {no_of_panels_vertical * c['panel']['peak_power']/1e3} kWp")
|
||||||
|
|
||||||
horizontal_energy, no_of_panels_horizontal = calculate_energy_production_horizontal(c)
|
horizontal_energy, no_of_panels_horizontal = calculate_energy_production(
|
||||||
|
c, "horizontal"
|
||||||
|
)
|
||||||
logger.info("Energy production for horizontal panels calculated successfully.")
|
logger.info("Energy production for horizontal panels calculated successfully.")
|
||||||
logger.debug(f"Horizontal Energy Production: {horizontal_energy.sum()}")
|
logger.debug(f"Horizontal Energy Production: {horizontal_energy.sum()}")
|
||||||
logger.debug("Number of panels: %d", no_of_panels_horizontal)
|
logger.debug("Number of panels: %d", no_of_panels_horizontal)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user