class ECMS def apply_pv_ground(model:, pv_ground_type:, pv_ground_total_area_pv_panels_m2:, pv_ground_tilt_angle:, pv_ground_azimuth_angle:, pv_ground_module_description:) ##### Remove leading or trailing whitespace in case users add them in inputs if pv_ground_total_area_pv_panels_m2.instance_of?(String) pv_ground_total_area_pv_panels_m2 = pv_ground_total_area_pv_panels_m2.strip end if pv_ground_tilt_angle.instance_of?(String) pv_ground_tilt_angle = pv_ground_tilt_angle.strip end if pv_ground_azimuth_angle.instance_of?(String) pv_ground_azimuth_angle = pv_ground_azimuth_angle.strip end ##### If any of users' inputs are nil/false do nothing. return if pv_ground_type.nil? || pv_ground_type == false || pv_ground_type == 'none' || pv_ground_type == 'NECB_Default' return if pv_ground_total_area_pv_panels_m2 == nil? || pv_ground_total_area_pv_panels_m2 == false || pv_ground_total_area_pv_panels_m2 == 'none' return if pv_ground_tilt_angle == nil? || pv_ground_tilt_angle == false || pv_ground_tilt_angle == 'none' return if pv_ground_azimuth_angle == nil? || pv_ground_azimuth_angle == false || pv_ground_azimuth_angle == 'none' return if pv_ground_module_description == nil? || pv_ground_module_description == false || pv_ground_module_description == 'none' ##### Convert a string to a float (except for pv_ground_type and pv_ground_module_description) if pv_ground_total_area_pv_panels_m2.instance_of?(String) && pv_ground_total_area_pv_panels_m2 != 'NECB_Default' pv_ground_total_area_pv_panels_m2 = pv_ground_total_area_pv_panels_m2.to_f end if pv_ground_tilt_angle.instance_of?(String) && pv_ground_tilt_angle != 'NECB_Default' pv_ground_tilt_angle = pv_ground_tilt_angle.to_f end if pv_ground_azimuth_angle.instance_of?(String) && pv_ground_azimuth_angle != 'NECB_Default' pv_ground_azimuth_angle = pv_ground_azimuth_angle.to_f end ##### Calculate footprint of the building model (this is used as default value for pv_ground_total_area_pv_panels_m2) building_footprint_m2 = calculate_building_footprint(model: model) # puts "building_footprint_m2 is #{building_footprint_m2}" ##### Set default PV panels' total area as the building footprint if pv_ground_total_area_pv_panels_m2 == 'NECB_Default' pv_ground_total_area_pv_panels_m2 = building_footprint_m2 end ##### Set default PV panels' tilt angle as the latitude if pv_ground_tilt_angle == 'NECB_Default' epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get) pv_ground_tilt_angle = epw.latitude end ##### Set default PV panels' azimuth angle as south-facing arrays if pv_ground_azimuth_angle == 'NECB_Default' pv_ground_azimuth_angle = 180 # EnergyPlus I/O Reference: "An azimuth angle of 180deg is for a south-facing array, and an azimuth angle of 0deg is for a north-facing array." end ##### Set default PV module type as the the below one if pv_ground_module_description == 'NECB_Default' pv_ground_module_description = 'HES-160-36PV 26.6 x 58.3 x 1.38' # Note: As per Mike Lubun's comment, assuming a typical panel is 5 ft x 2 ft, the closest standard type PV panel in the spreadsheet would be the 160W HES. end ##### Calculate number of PV panels # Note: assuming 5 ft x 2 ft as PV panel's size since it seems to fit the racking system used for ground mounts as per Mike Lubun's comment. pv_area_each_ft2 = 5.0 * 2.0 pv_area_each_m2 = OpenStudio.convert(pv_area_each_ft2, 'ft^2', 'm^2').get # convert pv_area_each_ft2 to m2 pv_number_panels = pv_ground_total_area_pv_panels_m2 / pv_area_each_m2 ##### Get data of the PV panel from the json file pv_info = @standards_data['tables']['pv']['table'].detect { |item| item['pv_module_description'] == pv_ground_module_description } pv_ground_module_type = pv_info['pv_module_type'] pv_watt = pv_info['pv_module_wattage'] ##### Create the generator # Assuming one PVWatts generator in E+ as per Mike Lubun's comment for simplification, however exact number of PVWatts generators (and inverters) are calculated for costing. dc_system_capacity = pv_number_panels * pv_watt generator = OpenStudio::Model::GeneratorPVWatts.new(model, dc_system_capacity) generator.setModuleType(pv_ground_module_type) # generator.setArrayType('OneAxis') # Note: "tilt and azimuth are fixed" for this array type (see E+ I/O Reference). This array type has been chosen as per Mike Lubun's costing spec. generator.setArrayType('FixedOpenRack') # Note: The 'FixedOpenRack' array type has been used instead of 'OneAxis' since the 'OneAxis' array type did not allow to have a non-zero tilt angle in OpenStudio 3.2.1. # (As per E+ I/O Reference: 'FixedOpenRack' is used for ground mounted arrays, assumes air flows freely around the array.) generator.setTiltAngle(pv_ground_tilt_angle) generator.setAzimuthAngle(pv_ground_azimuth_angle) ##### Create the inverter inverter = OpenStudio::Model::ElectricLoadCenterInverterPVWatts.new(model) inverter.setDCToACSizeRatio(1.1) # Note: This is EnergyPlus' default value; This default value has been chosen for ground-mounted PV, assuming no storage as per Mike Lubun's costing spec. inverter.setInverterEfficiency(0.96) # Note: This is EnergyPlus' default value; This default value has been chosen as per Mike Lubun's costing spec. ##### Add distribution systems, set relevant parameters, and add created generator to it elc_distribution = OpenStudio::Model::ElectricLoadCenterDistribution.new(model) elc_distribution.setInverter(inverter) elc_distribution.setGeneratorOperationSchemeType('Baseload') # E+ I/O Reference: "The Baseload scheme requests all generators scheduled ON (available) to operate, even if the amount of electric power generated exceeds the total facility electric power demand." This scheme type has been chosen as per Mike Lubun's costing spec. elc_distribution.addGenerator(generator) end # Method for calculating footprint of the building model def calculate_building_footprint(model:) building_footprint_m2_array = [] lowest_floor = 10000000000.0 # dummy number as initialization to find the lowest floor among spaces # @todo Question:it it fine that it has been assumed that the floor of all lowest spaces are at the same level? model.getSpaces.sort.each do |space| space.surfaces.sort.select { |surface| (surface.surfaceType == 'Floor') && (surface.outsideBoundaryCondition != 'Surface') && (surface.outsideBoundaryCondition != 'Adiabatic') }.each do |surface| floor_vertices = surface.vertices floor_z = floor_vertices[0].z.round(1) if floor_z <= lowest_floor lowest_floor = floor_z building_footprint_m2_array << surface.netArea end end end building_footprint_m2 = building_footprint_m2_array.sum return building_footprint_m2 end end