diff --git a/README.md b/README.md new file mode 100644 index 0000000..f991ece --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# CERC PV Workflow + +## Introduction +The CERC PV Workflow is designed to size and model different types of PV systems + +## System Requirements +- **`Software Requirements`**: +Operating System: Windows, macOS, or Linux +Python Version: 3.8 or higher + + +- **`Python Dependencies`**: +The application requires several Python libraries. These can be installed using the following command: +```bash +pip install -r requirements.txt +``` + +Ensure you have the required database drivers installed (e.g., psycopg2 for PostgreSQL, mysql-connector-python for MySQL). + +## Application Setup +- **`Clone or Download the Project`**: +Start by downloading or cloning the project repository into your local system: +```bash +git clone https://ngci.encs.concordia.ca/gitea/s_ranjbar/pv_workflow.git +``` + +## Workflow Applications +- **`Solar Angles Calculation`**: +- **`Solar Radiation on Tilted Surfaces`**: +- **`PV system assessment`**: + + diff --git a/example_codes/pv_system_assessment.py b/example_codes/pv_system_assessment.py index d6475c3..4f3ddf5 100644 --- a/example_codes/pv_system_assessment.py +++ b/example_codes/pv_system_assessment.py @@ -77,7 +77,8 @@ for building in city.buildings: system_catalogue_handler=None, roof_percentage_coverage=0.75, facade_coverage_percentage=0, - csv_output=False, + csv_output=True, output_path=pv_assessment_path).enrich() + diff --git a/pv_assessment/pv_system_assessment.py b/pv_assessment/pv_system_assessment.py index 0baf381..64d1a6d 100644 --- a/pv_assessment/pv_system_assessment.py +++ b/pv_assessment/pv_system_assessment.py @@ -7,20 +7,51 @@ from hub.helpers.monthly_values import MonthlyValues class PvSystemAssessment: + """ + A class to assess and size photovoltaic (PV) systems for buildings. + + This class provides methods for sizing PV systems on rooftops, calculating their energy output, + assigning system components from a catalog, and evaluating grid-tied systems. It also allows for + CSV output of system results. + + Attributes: + building (Building): The building for which the PV system is being assessed. + pv_system (PvSystem): The photovoltaic system used in the assessment. + battery (EnergyStorageSystem): The energy storage system associated with the PV system, if any. + tilt_angle (float): The tilt angle of the PV system. + solar_angles (DataFrame): Solar angles used in simulation (e.g., solar altitude, azimuth). + pv_installation_type (str): Type of installation (e.g., rooftop, facade). + simulation_model_type (str): Type of simulation model (e.g., 'explicit'). + module_model_name (str): Model name of the PV module being used. + inverter_efficiency (float): Efficiency of the inverter. + system_catalogue_handler (str): Handler for the PV system catalog. + roof_percentage_coverage (float): Percentage of the roof to be covered by PV panels. + facade_coverage_percentage (float): Percentage of the facade to be covered by PV panels. + csv_output (bool): Whether or not to generate CSV output of the results. + output_path (str): Path to store the output CSV file. + results (dict): Dictionary to store the results of the system assessment. + """ def __init__(self, building=None, pv_system=None, battery=None, tilt_angle=None, solar_angles=None, pv_installation_type=None, simulation_model_type=None, module_model_name=None, inverter_efficiency=None, system_catalogue_handler=None, roof_percentage_coverage=None, facade_coverage_percentage=None, csv_output=False, output_path=None): """ - :param building: - :param tilt_angle: - :param solar_angles: - :param simulation_model_type: - :param module_model_name: - :param inverter_efficiency: - :param system_catalogue_handler: - :param roof_percentage_coverage: - :param facade_coverage_percentage: + Initializes the PvSystemAssessment instance with the provided parameters or defaults. + + :param building: The building object for which the PV system is assessed. + :param pv_system: The photovoltaic system (optional, defaults to building's existing PV system). + :param battery: The battery storage system (optional, defaults to building's associated storage). + :param tilt_angle: Tilt angle of the PV system. + :param solar_angles: DataFrame containing solar angles (e.g., solar altitude, azimuth). + :param pv_installation_type: Type of installation ('rooftop', 'facade', etc.). + :param simulation_model_type: Type of simulation model ('explicit'). + :param module_model_name: Model name of the PV module. + :param inverter_efficiency: Efficiency of the inverter. + :param system_catalogue_handler: Catalog handler to select PV module from a catalog. + :param roof_percentage_coverage: Percentage of roof to cover with PV modules. + :param facade_coverage_percentage: Percentage of facade to cover with PV modules. + :param csv_output: Boolean indicating whether to generate CSV output. + :param output_path: Path for saving the CSV output. """ self.building = building self.tilt_angle = tilt_angle @@ -56,6 +87,16 @@ class PvSystemAssessment: @staticmethod def explicit_model(pv_system, inverter_efficiency, number_of_panels, irradiance, outdoor_temperature): + """ + Calculates the PV system's energy output using the explicit model. + + :param pv_system: The photovoltaic system. + :param inverter_efficiency: Efficiency of the inverter. + :param number_of_panels: Number of PV panels in the system. + :param irradiance: Hourly irradiance values. + :param outdoor_temperature: Hourly outdoor temperature values. + :return: List of energy outputs for each hour. + """ inverter_efficiency = inverter_efficiency stc_power = float(pv_system.standard_test_condition_maximum_power) stc_irradiance = float(pv_system.standard_test_condition_radiation) @@ -78,6 +119,11 @@ class PvSystemAssessment: return pv_output def rooftop_sizing(self): + """ + Sizing of the rooftop PV system. + + :return: Number of panels per row and number of rows to be installed. + """ pv_system = self.pv_system if self.module_model_name is not None: self.system_assignation() @@ -109,6 +155,11 @@ class PvSystemAssessment: return panels_per_row, number_of_rows def system_assignation(self): + """ + Assigns a PV system from the energy systems catalog based on the module model name. + + :raises ValueError: If no PV module with the provided model name exists in the catalog. + """ generation_units_catalogue = EnergySystemsCatalogFactory(self.system_catalogue_handler).catalog catalog_pv_generation_equipments = [component for component in generation_units_catalogue.entries('generation_equipments') if @@ -133,6 +184,13 @@ class PvSystemAssessment: energy_system.generation_systems[idx] = new_system def grid_tied_system(self): + """ + Evaluates the performance and sizing of a grid-tied PV system. + + This method assesses energy production, storage, and demand matching for a grid-tied PV system. + + :return: The evaluation results as a dictionary. + """ building_hourly_electricity_demand = [demand / cte.WATTS_HOUR_TO_JULES for demand in HourlyElectricityDemand(self.building).calculate()] rooftop_pv_output = [0] * 8760 diff --git a/pv_assessment/solar_calculator.py b/pv_assessment/solar_calculator.py index 87e4336..ee0b559 100644 --- a/pv_assessment/solar_calculator.py +++ b/pv_assessment/solar_calculator.py @@ -1,3 +1,9 @@ +""" +solar_calculator module +SPDX-License-Identifier: LGPL-3.0-or-later +Copyright © 2022 Concordia CERC group +Project Coder: Saeed Ranjbar saeed.ranjbar@cerc.com +""" import math import pandas as pd from datetime import datetime @@ -6,20 +12,21 @@ from hub.helpers.monthly_values import MonthlyValues class SolarCalculator: + """ + SolarCalculator class performs solar angle and irradiance calculations for a given city and tilt angles + """ def __init__(self, city, tilt_angle, surface_azimuth_angle, standard_meridian=-75, solar_constant=1366.1, maximum_clearness_index=1, min_cos_zenith=0.065, maximum_zenith_angle=87): """ - A class to calculate the solar angles and solar irradiance on a tilted surface in the City - :param city: An object from the City class -> City - :param tilt_angle: tilt angle of surface -> float - :param surface_azimuth_angle: The orientation of the surface. 0 is North -> float - :param standard_meridian: A standard meridian is the meridian whose mean solar time is the basis of the time of day - observed in a time zone -> float - :param solar_constant: The amount of energy received by a given area one astronomical unit away from the Sun. It is - constant and must not be changed - :param maximum_clearness_index: This is used to calculate the diffuse fraction of the solar irradiance -> float - :param min_cos_zenith: This is needed to avoid unrealistic values in tilted irradiance calculations -> float - :param maximum_zenith_angle: This is needed to avoid negative values in tilted irradiance calculations -> float + Initialize SolarCalculator with city and solar panel configurations. + :param city: City object containing latitude and longitude + :param tilt_angle: Tilt angle of the solar panel in degrees + :param surface_azimuth_angle: Azimuth angle of the solar panel in degrees + :param standard_meridian: Standard meridian for time zone correction + :param solar_constant: Extraterrestrial radiation constant in W/m2 + :param maximum_clearness_index: Maximum clearness index for calculation + :param min_cos_zenith: Minimum cosine zenith value + :param maximum_zenith_angle: Maximum allowable zenith angle in degrees """ self.city = city self.location_latitude = city.latitude @@ -52,6 +59,12 @@ class SolarCalculator: self.day_of_year = self.solar_angles.index.dayofyear def solar_time(self, datetime_val, day_of_year): + """ + Calculate apparent solar time for a given datetime and day of the year. + :param datetime_val: Input datetime value + :param day_of_year: Day of the year (1-365) + :return: Apparent solar time (datetime) + """ b = (day_of_year - 81) * 2 * math.pi / 364 eot = 9.87 * math.sin(2 * b) - 7.53 * math.cos(b) - 1.5 * math.sin(b) self.eot.append(eot) @@ -79,18 +92,42 @@ class SolarCalculator: return ast_time def declination_angle(self, day_of_year): + """ + Calculate the solar declination angle for a given day of the year. + :param day_of_year: Day of the year (1-365) + :return: Declination angle in radians + """ declination = 23.45 * math.sin(math.radians(360 / 365 * (284 + day_of_year))) declination_radian = math.radians(declination) self.declinations.append(declination) return declination_radian def hour_angle(self, ast_time): + """ + Calculate the hour angle for a given apparent solar time (AST). + + The hour angle is a measure of time since solar noon in degrees or radians, + where solar noon corresponds to 0°. It is negative in the morning and positive in the afternoon. + + :param ast_time: Apparent solar time as a datetime object. + :return: Hour angle in radians. + """ hour_angle = ((ast_time.hour * 60 + ast_time.minute) - 720) / 4 hour_angle_radian = math.radians(hour_angle) self.hour_angles.append(hour_angle) return hour_angle_radian def solar_altitude(self, declination_radian, hour_angle_radian): + """ + Calculate the solar altitude angle in radians for a given declination and hour angle. + + The solar altitude angle is the angle between the sun's rays and the horizontal plane + at a given location and time. It indicates the sun's height in the sky. + + :param declination_radian: Solar declination angle in radians. + :param hour_angle_radian: Hour angle in radians. + :return: Solar altitude angle in radians. + """ solar_altitude_radians = math.asin(math.cos(self.location_latitude_rad) * math.cos(declination_radian) * math.cos(hour_angle_radian) + math.sin(self.location_latitude_rad) * math.sin(declination_radian)) @@ -99,6 +136,16 @@ class SolarCalculator: return solar_altitude_radians def zenith(self, solar_altitude_radians): + """ + Calculate the solar zenith angle in radians from the solar altitude angle. + + The solar zenith angle is the angle between the vertical direction + (directly overhead) and the line to the sun. It is complementary to + the solar altitude angle. + + :param solar_altitude_radians: Solar altitude angle in radians. + :return: Solar zenith angle in radians. + """ solar_altitude = math.degrees(solar_altitude_radians) zenith_degree = 90 - solar_altitude zenith_radian = math.radians(zenith_degree) @@ -106,6 +153,21 @@ class SolarCalculator: return zenith_radian def solar_azimuth_analytical(self, hourangle, declination, zenith): + """ + Calculate the solar azimuth angle analytically in radians. + + The solar azimuth angle represents the sun's position relative to true north, + measured clockwise. The method uses the hour angle, solar declination, and + solar zenith to compute the azimuth. + + :param hourangle: Hour angle of the sun in radians, indicating its position + relative to the solar noon. + :param declination: Solar declination angle in radians, which is the angle + between the sun's rays and the plane of Earth's equator. + :param zenith: Solar zenith angle in radians, the angle between the vertical + direction and the sun's position. + :return: Solar azimuth angle in radians. + """ numer = (math.cos(zenith) * math.sin(self.location_latitude_rad) - math.sin(declination)) denom = (math.sin(zenith) * math.cos(self.location_latitude_rad)) if math.isclose(denom, 0.0, abs_tol=1e-8): @@ -122,6 +184,19 @@ class SolarCalculator: return solar_azimuth_radians def incident_angle(self, solar_altitude_radians, solar_azimuth_radians): + """ + Calculate the solar incident angle in radians. + + The incident angle represents the angle between the solar rays and the + normal to a tilted surface. It is a critical parameter for evaluating + the performance of solar panels. + + :param solar_altitude_radians: Solar altitude angle in radians, indicating + the sun's position above the horizon. + :param solar_azimuth_radians: Solar azimuth angle in radians, specifying + the sun's position relative to true north. + :return: Solar incident angle in radians. + """ incident_radian = math.acos(math.cos(solar_altitude_radians) * math.cos(abs(solar_azimuth_radians - self.surface_azimuth_rad)) * math.sin(self.tilt_angle_rad) + math.sin(solar_altitude_radians) * @@ -131,6 +206,15 @@ class SolarCalculator: return incident_radian def dni_extra(self, day_of_year, zenith_radian): + """ + Calculate extraterrestrial DNI and horizontal irradiance. + + :param day_of_year: Day of the year (1–365/366). + :param zenith_radian: Solar zenith angle in radians. + :return: Tuple (i_on, i_oh) where: + - i_on: Extraterrestrial normal irradiance [W/m²]. + - i_oh: Extraterrestrial horizontal irradiance [W/m²]. + """ i_on = self.solar_constant * (1 + 0.033 * math.cos(math.radians(360 * day_of_year / 365))) i_oh = i_on * max(math.cos(zenith_radian), self.min_cos_zenith) self.i_on.append(i_on) @@ -138,12 +222,26 @@ class SolarCalculator: return i_on, i_oh def clearness_index(self, ghi, i_oh): + """ + Calculate the clearness index (Kt). + + :param ghi: Global horizontal irradiance [W/m²]. + :param i_oh: Extraterrestrial horizontal irradiance [W/m²]. + :return: Clearness index (Kt). + """ k_t = ghi / i_oh k_t = max(0, k_t) k_t = min(self.maximum_clearness_index, k_t) return k_t def diffuse_fraction(self, k_t, zenith): + """ + Estimate the diffuse fraction of irradiance. + + :param k_t: Clearness index (Kt). + :param zenith: Solar zenith angle in degrees. + :return: Diffuse fraction. + """ if k_t <= 0.22: fraction_diffuse = 1 - 0.09 * k_t elif k_t <= 0.8: @@ -155,6 +253,14 @@ class SolarCalculator: return fraction_diffuse def radiation_components_horizontal(self, ghi, fraction_diffuse, zenith): + """ + Compute diffuse and beam components of horizontal radiation. + + :param ghi: Global horizontal irradiance [W/m²]. + :param fraction_diffuse: Diffuse fraction. + :param zenith: Solar zenith angle in degrees. + :return: Tuple (diffuse_horizontal, dni). + """ diffuse_horizontal = ghi * fraction_diffuse dni = (ghi - diffuse_horizontal) / math.cos(math.radians(zenith)) if zenith > self.maximum_zenith_angle or dni < 0: @@ -162,6 +268,14 @@ class SolarCalculator: return diffuse_horizontal, dni def radiation_components_tilted(self, diffuse_horizontal, dni, incident_angle): + """ + Compute total radiation on a tilted surface. + + :param diffuse_horizontal: Diffuse horizontal irradiance [W/m²]. + :param dni: Direct normal irradiance [W/m²]. + :param incident_angle: Solar incident angle in degrees. + :return: Total radiation on tilted surface [W/m²]. + """ beam_tilted = dni * math.cos(math.radians(incident_angle)) beam_tilted = max(beam_tilted, 0) diffuse_tilted = diffuse_horizontal * ((1 + math.cos(math.radians(self.tilt_angle))) / 2) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..15185b9 Binary files /dev/null and b/requirements.txt differ