import math import csv import hub.helpers.constants as cte from pv_assessment.electricity_demand_calculator import HourlyElectricityDemand from hub.catalog_factories.energy_systems_catalog_factory import EnergySystemsCatalogFactory from hub.helpers.monthly_values import MonthlyValues class PvSystemProduction: 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: """ self.building = building self.tilt_angle = tilt_angle self.solar_angles = solar_angles self.pv_installation_type = pv_installation_type self.simulation_model_type = simulation_model_type self.module_model_name = module_model_name self.inverter_efficiency = inverter_efficiency self.system_catalogue_handler = system_catalogue_handler self.roof_percentage_coverage = roof_percentage_coverage self.facade_coverage_percentage = facade_coverage_percentage self.pv_hourly_generation = None self.t_cell = None self.results = {} self.csv_output = csv_output self.output_path = output_path if pv_system is not None: self.pv_system = pv_system else: for energy_system in self.building.energy_systems: for generation_system in energy_system.generation_systems: if generation_system.system_type == cte.PHOTOVOLTAIC: self.pv_system = generation_system if battery is not None: self.battery = battery else: for energy_system in self.building.energy_systems: for generation_system in energy_system.generation_systems: if generation_system.system_type == cte.PHOTOVOLTAIC and generation_system.energy_storage_systems is not None: for storage_system in generation_system.energy_storage_systems: if storage_system.type_energy_stored == cte.ELECTRICAL: self.battery = storage_system @staticmethod def explicit_model(pv_system, inverter_efficiency, number_of_panels, irradiance, outdoor_temperature): inverter_efficiency = inverter_efficiency stc_power = float(pv_system.standard_test_condition_maximum_power) stc_irradiance = float(pv_system.standard_test_condition_radiation) cell_temperature_coefficient = float(pv_system.cell_temperature_coefficient) / 100 if ( pv_system.cell_temperature_coefficient is not None) else None stc_t_cell = float(pv_system.standard_test_condition_cell_temperature) nominal_condition_irradiance = float(pv_system.nominal_radiation) nominal_condition_cell_temperature = float(pv_system.nominal_cell_temperature) nominal_t_out = float(pv_system.nominal_ambient_temperature) g_i = irradiance t_out = outdoor_temperature t_cell = [] pv_output = [] for i in range(len(g_i)): t_cell.append((t_out[i] + (g_i[i] / nominal_condition_irradiance) * (nominal_condition_cell_temperature - nominal_t_out))) pv_output.append((inverter_efficiency * number_of_panels * (stc_power * (g_i[i] / stc_irradiance) * (1 - cell_temperature_coefficient * (t_cell[i] - stc_t_cell))))) return pv_output def rooftop_sizing(self, roof): pv_system = self.pv_system if self.module_model_name is not None: self.system_assignation() # System Sizing module_width = float(pv_system.width) module_height = float(pv_system.height) roof_area = roof.perimeter_area pv_module_area = module_width * module_height available_roof = (self.roof_percentage_coverage * roof_area) # Inter-Row Spacing winter_solstice = self.solar_angles[(self.solar_angles['AST'].dt.month == 12) & (self.solar_angles['AST'].dt.day == 21) & (self.solar_angles['AST'].dt.hour == 12)] solar_altitude = winter_solstice['solar altitude'].values[0] solar_azimuth = winter_solstice['solar azimuth'].values[0] distance = ((module_height * math.sin(math.radians(self.tilt_angle)) * abs( math.cos(math.radians(solar_azimuth)))) / math.tan(math.radians(solar_altitude))) distance = float(format(distance, '.2f')) # Calculation of the number of panels space_dimension = math.sqrt(available_roof) space_dimension = float(format(space_dimension, '.2f')) panels_per_row = math.ceil(space_dimension / module_width) number_of_rows = math.ceil(space_dimension / distance) total_number_of_panels = panels_per_row * number_of_rows total_pv_area = total_number_of_panels * pv_module_area roof.installed_solar_collector_area = total_pv_area return panels_per_row, number_of_rows def system_assignation(self): generation_units_catalogue = EnergySystemsCatalogFactory(self.system_catalogue_handler).catalog catalog_pv_generation_equipments = [component for component in generation_units_catalogue.entries('generation_equipments') if component.system_type == 'photovoltaic'] selected_pv_module = None for pv_module in catalog_pv_generation_equipments: if self.module_model_name == pv_module.model_name: selected_pv_module = pv_module if selected_pv_module is None: raise ValueError("No PV module with the provided model name exists in the catalogue") for energy_system in self.building.energy_systems: for idx, generation_system in enumerate(energy_system.generation_systems): if generation_system.system_type == cte.PHOTOVOLTAIC: new_system = selected_pv_module # Preserve attributes that exist in the original but not in the new system for attr in dir(generation_system): # Skip private attributes and methods if not attr.startswith('__') and not callable(getattr(generation_system, attr)): if not hasattr(new_system, attr): setattr(new_system, attr, getattr(generation_system, attr)) # Replace the old generation system with the new one energy_system.generation_systems[idx] = new_system def grid_tied_system(self): rooftops_pv_output = [0] * 8760 facades_pv_output = [0] * 8760 rooftop_number_of_panels = 0 if 'rooftop' in self.pv_installation_type.lower(): for roof in self.building.roofs: if roof.perimeter_area > 40: np, ns = self.rooftop_sizing(roof) single_roof_number_of_panels = np * ns rooftop_number_of_panels += single_roof_number_of_panels if self.simulation_model_type == 'explicit': single_roof_pv_output = self.explicit_model(pv_system=self.pv_system, inverter_efficiency=self.inverter_efficiency, number_of_panels=single_roof_number_of_panels, irradiance=roof.global_irradiance_tilted[cte.HOUR], outdoor_temperature=self.building.external_temperature[ cte.HOUR]) for i in range(len(rooftops_pv_output)): rooftops_pv_output[i] += single_roof_pv_output[i] total_hourly_pv_output = [rooftops_pv_output[i] + facades_pv_output[i] for i in range(8760)] results = {'building_name': self.building.name, 'total_floor_area_m2': self.building.thermal_zones_from_internal_zones[0].total_floor_area, 'roof_area_m2': self.building.roofs[0].perimeter_area, 'rooftop_panels': rooftop_number_of_panels, 'rooftop_panels_area_m2': self.building.roofs[0].installed_solar_collector_area, 'yearly_rooftop_ghi_kW/m2': self.building.roofs[0].global_irradiance[cte.YEAR][0] / 1000, f'yearly_rooftop_tilted_radiation_{self.tilt_angle}_degree_kW/m2': self.building.roofs[0].global_irradiance_tilted[cte.YEAR][0] / 1000, 'yearly_rooftop_pv_production_kWh': sum(rooftops_pv_output) / 1000, 'yearly_total_pv_production_kWh': sum(total_hourly_pv_output) / 1000, 'specific_pv_production_kWh/kWp': sum(rooftops_pv_output) / ( float(self.pv_system.standard_test_condition_maximum_power) * rooftop_number_of_panels), 'hourly_rooftop_poa_irradiance_W/m2': self.building.roofs[0].global_irradiance_tilted[cte.HOUR], 'hourly_rooftop_pv_output_W': rooftops_pv_output, 'T_out': self.building.external_temperature[cte.HOUR], 'total_hourly_pv_system_output_W': total_hourly_pv_output} return results def enrich(self): system_archetype_name = self.building.energy_systems_archetype_name archetype_name = '_'.join(system_archetype_name.lower().split()) if 'grid_tied' in archetype_name: self.results = self.grid_tied_system() for energy_system in self.building.energy_systems: for generation_system in energy_system.generation_systems: if generation_system.system_type == cte.PHOTOVOLTAIC: generation_system.installed_capacity = (self.results['rooftop_panels'] * float(generation_system.standard_test_condition_maximum_power)) hourly_pv_output = self.results['total_hourly_pv_system_output_W'] self.building.pv_generation[cte.HOUR] = hourly_pv_output self.building.pv_generation[cte.MONTH] = MonthlyValues.get_total_month(hourly_pv_output) self.building.pv_generation[cte.YEAR] = [sum(hourly_pv_output)] if self.csv_output: self.save_to_csv(self.results, self.output_path, f'{self.building.name}_pv_system_analysis.csv') @staticmethod def save_to_csv(data, output_path, filename='rooftop_system_results.csv'): # Separate keys based on whether their values are single values or lists single_value_keys = [key for key, value in data.items() if not isinstance(value, list)] list_value_keys = [key for key, value in data.items() if isinstance(value, list)] # Check if all lists have the same length list_lengths = [len(data[key]) for key in list_value_keys] if not all(length == list_lengths[0] for length in list_lengths): raise ValueError("All lists in the dictionary must have the same length") # Get the length of list values (assuming all lists are of the same length, e.g., 8760 for hourly data) num_rows = list_lengths[0] if list_value_keys else 1 # Open the CSV file for writing with open(output_path / filename, mode='w', newline='') as csv_file: writer = csv.writer(csv_file) # Write single-value data as a header section for key in single_value_keys: writer.writerow([key, data[key]]) # Write an empty row for separation writer.writerow([]) # Write the header for the list values writer.writerow(list_value_keys) # Write each row for the lists for i in range(num_rows): row = [data[key][i] for key in list_value_keys] writer.writerow(row)