From 07e436e140169a487618cecc580e4f488491eb81 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Thu, 29 Aug 2024 15:23:40 -0700 Subject: [PATCH 01/16] Start relocating translator methods to resource files. --- BuildResidentialHPXML/measure.rb | 2 +- BuildResidentialHPXML/measure.xml | 6 +- HPXMLtoOpenStudio/measure.rb | 116 ++++++------------------ HPXMLtoOpenStudio/measure.xml | 8 +- HPXMLtoOpenStudio/resources/geometry.rb | 48 +++++++++- 5 files changed, 84 insertions(+), 96 deletions(-) diff --git a/BuildResidentialHPXML/measure.rb b/BuildResidentialHPXML/measure.rb index b93287692c..d821dae132 100644 --- a/BuildResidentialHPXML/measure.rb +++ b/BuildResidentialHPXML/measure.rb @@ -6033,7 +6033,7 @@ def self.set_duct_leakages(args, hvac_distribution) # Get the specific HPXML foundation or attic location based on general HPXML location and specific HPXML foundation or attic type. # - # @param location [String] the general HPXML location (crawlspace or attic) + # @param location [String] the location of interest (HPXML::LocationCrawlspace or HPXML::LocationAttic) # @param foundation_type [String] the specific HPXML foundation type (unvented crawlspace, vented crawlspace, conditioned crawlspace) # @param attic_type [String] the specific HPXML attic type (unvented attic, vented attic, conditioned attic) # @return [nil] diff --git a/BuildResidentialHPXML/measure.xml b/BuildResidentialHPXML/measure.xml index 2400cb5d3e..f61e2101c5 100644 --- a/BuildResidentialHPXML/measure.xml +++ b/BuildResidentialHPXML/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_hpxml a13a8983-2b01-4930-8af2-42030b6e4233 - 3852085e-1613-4c19-983f-3c5bcfdc19aa - 2024-08-28T13:49:31Z + 15230e25-c533-4248-a0e3-78cb9fdbe6b7 + 2024-08-29T22:22:50Z 2C38F48B BuildResidentialHPXML HPXML Builder @@ -7406,7 +7406,7 @@ measure.rb rb script - 55D502E8 + 0BE8CFA7 constants.rb diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index e07df0006a..234699d1fd 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -325,7 +325,7 @@ def add_unit_model_to_model(model, hpxml_osm_map) end hpxml_osm_map.values.each_with_index do |unit_model, unit_number| - shift_geometry(unit_model, unit_number) + Geometry.shift_unit(unit_model, unit_number) prefix_all_unit_model_objects(unit_model, unit_number) # Handle remaining (non-unique) objects now @@ -340,45 +340,12 @@ def add_unit_model_to_model(model, hpxml_osm_map) model.addObjects(unit_model_objects, true) end - # TODO - # - # @param unit_model [TODO] TODO - # @param unit_number [TODO] TODO - # @return [TODO] TODO - def shift_geometry(unit_model, unit_number) - # Shift units so they aren't right on top and shade each other - y_shift = 200.0 * unit_number # meters - - # shift the unit so it's not right on top of the previous one - unit_model.getSpaces.sort.each do |space| - space.setYOrigin(y_shift) - end - - # shift shading surfaces - m = OpenStudio::Matrix.new(4, 4, 0) - m[0, 0] = 1 - m[1, 1] = 1 - m[2, 2] = 1 - m[3, 3] = 1 - m[1, 3] = y_shift - t = OpenStudio::Transformation.new(m) - - unit_model.getShadingSurfaceGroups.each do |shading_surface_group| - next if shading_surface_group.space.is_initialized # already got shifted - - shading_surface_group.shadingSurfaces.each do |shading_surface| - shading_surface.setVertices(t * shading_surface.vertices) - end - end - end - - # TODO + # Prefix all objects with name using unit number. # # @param unit_model [TODO] TODO # @param unit_number [TODO] TODO # @return [TODO] TODO def prefix_all_unit_model_objects(unit_model, unit_number) - # Prefix all objects with name using unit number # FUTURE: Create objects with unique names up front so we don't have to do this # EMS objects @@ -504,7 +471,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug # Conditioned space/zone spaces = {} - create_or_get_space(model, spaces, HPXML::LocationConditionedSpace) + Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg) set_foundation_and_walls_top() set_heating_and_cooling_seasons(runner) add_setpoints(runner, model, weather, spaces) @@ -553,13 +520,12 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug add_building_unit(model, unit_num) end - # TODO + # Check/update emissions file references. # # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param hpxml_path [String] Path to the HPXML file # @return [TODO] TODO def check_emissions_references(hpxml_header, hpxml_path) - # Check/update file references hpxml_header.emissions_scenarios.each do |scenario| if hpxml_header.emissions_scenarios.select { |s| s.emissions_type == scenario.emissions_type && s.name == scenario.name }.size > 1 fail "Found multiple Emissions Scenarios with the Scenario Name=#{scenario.name} and Emissions Type=#{scenario.emissions_type}." @@ -572,13 +538,12 @@ def check_emissions_references(hpxml_header, hpxml_path) end end - # TODO + # Check/update schedule file references. # # @param hpxml_bldg_header [TODO] TODO # @param hpxml_path [String] Path to the HPXML file # @return [TODO] TODO def check_schedule_references(hpxml_bldg_header, hpxml_path) - # Check/update file references hpxml_bldg_header.schedules_filepaths = hpxml_bldg_header.schedules_filepaths.collect { |sfp| FilePath.check_path(sfp, File.dirname(hpxml_path), @@ -672,19 +637,6 @@ def add_num_occupants(model, runner, spaces) @schedules_file, @hpxml_header.unavailable_periods) end - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param location [TODO] TODO - # @return [TODO] TODO - def create_or_get_space(model, spaces, location) - if spaces[location].nil? - Geometry.create_space_and_zone(model: model, spaces: spaces, location: location, zone_multiplier: @hpxml_bldg.building_construction.number_of_units) - end - return spaces[location] - end - # Adds any HPXML Roofs to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -1358,16 +1310,14 @@ def add_foundation_slab(model, weather, spaces, slab, z_origin, exposed_length, return kiva_foundation end - # TODO + # Check if we need to add floors between conditioned spaces (e.g., between first + # and second story or conditioned basement ceiling). + # This ensures that the E+ reported Conditioned Floor Area is correct. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [TODO] TODO def add_conditioned_floor_area(model, spaces) - # Check if we need to add floors between conditioned spaces (e.g., between first - # and second story or conditioned basement ceiling). - # This ensures that the E+ reported Conditioned Floor Area is correct. - sum_cfa = 0.0 @hpxml_bldg.floors.each do |floor| next unless floor.is_floor @@ -1400,7 +1350,7 @@ def add_conditioned_floor_area(model, spaces) floor_surface.setWindExposure(EPlus::SurfaceWindExposureNo) floor_surface.setName('inferred conditioned floor') floor_surface.setSurfaceType(EPlus::SurfaceTypeFloor) - floor_surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)) + floor_surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) floor_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) floor_surface.additionalProperties.setFeature('SurfaceType', 'InferredFloor') floor_surface.additionalProperties.setFeature('Tilt', 0.0) @@ -1413,7 +1363,7 @@ def add_conditioned_floor_area(model, spaces) ceiling_surface.setWindExposure(EPlus::SurfaceWindExposureNo) ceiling_surface.setName('inferred conditioned ceiling') ceiling_surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) - ceiling_surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)) + ceiling_surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) ceiling_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) ceiling_surface.additionalProperties.setFeature('SurfaceType', 'InferredCeiling') ceiling_surface.additionalProperties.setFeature('Tilt', 0.0) @@ -1597,7 +1547,7 @@ def add_skylights(model, spaces) surface.additionalProperties.setFeature('SurfaceType', 'Skylight') surface.setName("surface #{skylight.id}") surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) - surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)) + surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) # cannot be adiabatic because subsurfaces won't be created vertices = Geometry.create_roof_vertices(length: length, width: width, z_origin: z_origin, azimuth: skylight.azimuth, tilt: tilt) @@ -1695,16 +1645,15 @@ def add_doors(model, spaces) apply_adiabatic_construction(model, surfaces, 'wall') end - # TODO + # Arbitrary construction for heat capacitance. + # Only applies to surfaces where outside boundary conditioned is + # adiabatic or surface net area is near zero. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param surfaces [TODO] TODO # @param type [TODO] TODO # @return [TODO] TODO def apply_adiabatic_construction(model, surfaces, type) - # Arbitrary construction for heat capacitance. - # Only applies to surfaces where outside boundary conditioned is - # adiabatic or surface net area is near zero. return if surfaces.empty? if type == 'wall' @@ -2433,7 +2382,7 @@ def add_building_unit(model, unit_num) end end - # TODO + # Store some data for use in reporting measure # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml [HPXML] HPXML object @@ -2443,7 +2392,6 @@ def add_building_unit(model, unit_num) # @param hpxml_defaults_path [TODO] TODO # @return [TODO] TODO def add_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_id, hpxml_defaults_path) - # Store some data for use in reporting measure additionalProperties = model.getBuilding.additionalProperties additionalProperties.setFeature('hpxml_path', hpxml_path) additionalProperties.setFeature('hpxml_defaults_path', hpxml_defaults_path) @@ -2462,17 +2410,15 @@ def add_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_ additionalProperties.setFeature('is_southern_hemisphere', hpxml_osm_map.keys[0].latitude < 0) end - # TODO + # We do our own unmet hours calculation via EMS so that we can incorporate, + # e.g., heating/cooling seasons into the logic. The calculation layers on top + # of the built-in EnergyPlus unmet hours output. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit # @param hpxml [HPXML] HPXML object # @return [TODO] TODO def add_unmet_hours_output(model, hpxml_osm_map, hpxml) - # We do our own unmet hours calculation via EMS so that we can incorporate, - # e.g., heating/cooling seasons into the logic. The calculation layers on top - # of the built-in EnergyPlus unmet hours output. - # Create sensors and gather data htg_sensors, clg_sensors = {}, {} zone_air_temp_sensors, htg_spt_sensors, clg_spt_sensors = {}, {}, {} @@ -3247,9 +3193,9 @@ def add_ems_debug_output(model) def set_surface_interior(model, spaces, surface, hpxml_surface) interior_adjacent_to = hpxml_surface.interior_adjacent_to if HPXML::conditioned_below_grade_locations.include? interior_adjacent_to - surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)) + surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) else - surface.setSpace(create_or_get_space(model, spaces, interior_adjacent_to)) + surface.setSpace(Geometry.create_or_get_space(model, spaces, interior_adjacent_to, @hpxml_bldg)) end end @@ -3273,10 +3219,10 @@ def set_surface_exterior(model, spaces, surface, hpxml_surface) HPXML::LocationOtherNonFreezingSpace, HPXML::LocationOtherHousingUnit].include? exterior_adjacent_to set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spaces) elsif HPXML::conditioned_below_grade_locations.include? exterior_adjacent_to - adjacent_surface = surface.createAdjacentSurface(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)).get + adjacent_surface = surface.createAdjacentSurface(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)).get adjacent_surface.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get) else - adjacent_surface = surface.createAdjacentSurface(create_or_get_space(model, spaces, exterior_adjacent_to)).get + adjacent_surface = surface.createAdjacentSurface(Geometry.create_or_get_space(model, spaces, exterior_adjacent_to, @hpxml_bldg)).get adjacent_surface.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get) end end @@ -3309,16 +3255,14 @@ def set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spa surface.setWindExposure(EPlus::SurfaceWindExposureNo) end - # TODO + # Create outside boundary schedules to be actuated by EMS, + # can be shared by any surface, duct adjacent to / located in those spaces. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param location [TODO] TODO + # @param location [String] the location of interest (HPXML::LocationXXX) # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [TODO] TODO def get_space_temperature_schedule(model, location, spaces) - # Create outside boundary schedules to be actuated by EMS, - # can be shared by any surface, duct adjacent to / located in those spaces - # return if already exists model.getScheduleConstants.each do |sch| next unless sch.name.to_s == location @@ -3427,7 +3371,7 @@ def get_space_temperature_schedule(model, location, spaces) # Should be called when the object's energy use is sensitive to ambient temperature # (e.g., water heaters, ducts, and refrigerators). # - # @param location [TODO] TODO + # @param location [String] the location of interest (HPXML::LocationXXX) # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [TODO] TODO @@ -3457,7 +3401,7 @@ def get_space_or_schedule_from_location(location, model, spaces) # Should be called when the object's energy use is NOT sensitive to ambient temperature # (e.g., appliances). # - # @param location [TODO] TODO + # @param location [String] the location of interest (HPXML::LocationXXX) # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [TODO] TODO def get_space_from_location(location, spaces) @@ -3474,7 +3418,8 @@ def get_space_from_location(location, spaces) return spaces[location] end - # TODO + # Set its parent surface outside boundary condition, which will be also applied to subsurfaces through OS + # The parent surface is entirely comprised of the subsurface. # # @param surface [TODO] TODO # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects @@ -3482,9 +3427,6 @@ def get_space_from_location(location, spaces) # @param hpxml_surface [TODO] TODO # @return [nil] def set_subsurface_exterior(surface, spaces, model, hpxml_surface) - # Set its parent surface outside boundary condition, which will be also applied to subsurfaces through OS - # The parent surface is entirely comprised of the subsurface. - # Subsurface on foundation wall, set it to be adjacent to outdoors if hpxml_surface.exterior_adjacent_to == HPXML::LocationGround surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index abf815c5df..d40a64167d 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 6f1b71cd-073a-4b20-bbc1-1e4d5ce1a151 - 2024-08-28T20:08:55Z + 78a17c23-45b1-4625-aa57-770074f25e74 + 2024-08-29T22:22:53Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - 6064F61F + 5D5B647E airflow.rb @@ -345,7 +345,7 @@ geometry.rb rb resource - D4DA7927 + FECF8D4F hotwater_appliances.rb diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index a6abbba192..cc66db4f07 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -626,7 +626,7 @@ def self.add_neighbor_shading(model:, # TODO # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param location [TODO] TODO + # @param location [String] the location of interest (HPXML::LocationXXX) # @return [TODO] TODO def self.calculate_zone_volume(hpxml_bldg:, location:) @@ -772,4 +772,50 @@ def self.calculate_subsurface_parent_buffer(length:, min_surface_area = 0.005 # m^2 return 0.5 * (((length + width)**2 + 4.0 * min_surface_area)**0.5 - length - width) end + + # Shift units so they aren't right on top and shade each other. + # + # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [nil] + def self.shift_unit(unit_model, unit_number) + y_shift = 200.0 * unit_number # meters + + # shift the unit so it's not right on top of the previous one + unit_model.getSpaces.sort.each do |space| + space.setYOrigin(y_shift) + end + + # shift shading surfaces + m = OpenStudio::Matrix.new(4, 4, 0) + m[0, 0] = 1 + m[1, 1] = 1 + m[2, 2] = 1 + m[3, 3] = 1 + m[1, 3] = y_shift + t = OpenStudio::Transformation.new(m) + + unit_model.getShadingSurfaceGroups.each do |shading_surface_group| + next if shading_surface_group.space.is_initialized # already got shifted + + shading_surface_group.shadingSurfaces.each do |shading_surface| + shading_surface.setVertices(t * shading_surface.vertices) + end + end + end + + # For a provided HPXML Location, create an OpenStudio Space and Thermal Zone if the provided spaces hash doesn't already contain the OpenStudio Space. + # Otherwise, return the already-created OpenStudio Space for the provided HPXML Location. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param location [String] the location of interest (HPXML::LocationXXX) + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [OpenStudio::Model::Space] the OpenStudio::Model::Space object corresponding to HPXML::LocationXXX + def self.create_or_get_space(model, spaces, location, hpxml_bldg) + if spaces[location].nil? + create_space_and_zone(model: model, spaces: spaces, location: location, zone_multiplier: hpxml_bldg.building_construction.number_of_units) + end + return spaces[location] + end end From 74264da49dab7d72bd3082ce1bf3f949dc76e90b Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Fri, 30 Aug 2024 11:24:53 -0700 Subject: [PATCH 02/16] Progress. --- HPXMLtoOpenStudio/measure.rb | 247 ++++++++++------------- HPXMLtoOpenStudio/measure.xml | 12 +- HPXMLtoOpenStudio/resources/geometry.rb | 4 +- HPXMLtoOpenStudio/resources/hvac.rb | 4 +- HPXMLtoOpenStudio/resources/schedules.rb | 52 +++++ 5 files changed, 170 insertions(+), 149 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 4a670d7612..c3295e837e 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -182,9 +182,9 @@ def run(model, runner, user_arguments) # Apply HPXML defaults upfront; process schedules & emissions hpxml_sch_map = {} - check_emissions_references(hpxml.header, args[:hpxml_path]) + Schedule.check_emissions_references(hpxml.header, args[:hpxml_path]) hpxml.buildings.each_with_index do |hpxml_bldg, i| - check_schedule_references(hpxml_bldg.header, args[:hpxml_path]) + Schedule.check_schedule_references(hpxml_bldg.header, args[:hpxml_path]) in_schedules_csv = 'in.schedules.csv' in_schedules_csv = "in.schedules#{i + 1}.csv" if i > 0 schedules_file = SchedulesFile.new(runner: runner, @@ -198,7 +198,7 @@ def run(model, runner, user_arguments) output_format: args[:output_format]) hpxml_sch_map[hpxml_bldg] = schedules_file end - validate_emissions_files(hpxml.header) + Schedule.validate_emissions_files(hpxml.header) # Write updated HPXML object (w/ defaults) to file for inspection hpxml_defaults_path = File.join(args[:output_dir], 'in.xml') @@ -261,11 +261,14 @@ def run(model, runner, user_arguments) return true end - # TODO + # When there are multiple dwelling units, merge all unit models into a single model. + # First deal with unique objects; look for differences in values across unit models. + # Then make all unit models "unique" by shifting geometry and prefixing object names. + # Then bulk add all modified objects to the main OpenStudio Model object. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @return [TODO] TODO + # @return [nil] def add_unit_model_to_model(model, hpxml_osm_map) unique_objects = { 'OS:ConvergenceLimits' => 'ConvergenceLimits', 'OS:Foundation:Kiva:Settings' => 'FoundationKivaSettings', @@ -342,9 +345,9 @@ def add_unit_model_to_model(model, hpxml_osm_map) # Prefix all objects with name using unit number. # - # @param unit_model [TODO] TODO - # @param unit_number [TODO] TODO - # @return [TODO] TODO + # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object (corresponding to one of multiple dwelling units) + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [nil] def prefix_all_unit_model_objects(unit_model, unit_number) # FUTURE: Create objects with unique names up front so we don't have to do this @@ -422,11 +425,11 @@ def prefix_all_unit_model_objects(unit_model, unit_number) end end - # TODO + # Create a new OpenStudio object name by prefixing the old with "unit" plus the unit number. # - # @param obj_name [TODO] TODO - # @param unit_number [TODO] TODO - # @return [TODO] TODO + # @param obj_name [String] the OpenStudio object name + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [String] the new OpenStudio object name with unique unit prefix def make_variable_name(obj_name, unit_number) return "unit#{unit_number + 1}_#{obj_name}".gsub(' ', '_').gsub('-', '_') end @@ -440,10 +443,10 @@ def make_variable_name(obj_name, unit_number) # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param epw_path [String] Path to the EPW weather file # @param weather [WeatherFile] Weather object containing EPW information - # @param debug [TODO] TODO - # @param schedules_file [TODO] TODO - # @param eri_version [TODO] TODO - # @param unit_num [TODO] TODO + # @param debug [Boolean] true writes in.osm, generates additional log output, and creates all E+ output files + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions + # @param unit_num [Integer] index number corresponding to an HPXML Building object # @return [nil] def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug, schedules_file, eri_version, unit_num) @hpxml_header = hpxml.header @@ -520,61 +523,11 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug add_building_unit(model, unit_num) end - # Check/update emissions file references. - # - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param hpxml_path [String] Path to the HPXML file - # @return [TODO] TODO - def check_emissions_references(hpxml_header, hpxml_path) - hpxml_header.emissions_scenarios.each do |scenario| - if hpxml_header.emissions_scenarios.select { |s| s.emissions_type == scenario.emissions_type && s.name == scenario.name }.size > 1 - fail "Found multiple Emissions Scenarios with the Scenario Name=#{scenario.name} and Emissions Type=#{scenario.emissions_type}." - end - next if scenario.elec_schedule_filepath.nil? - - scenario.elec_schedule_filepath = FilePath.check_path(scenario.elec_schedule_filepath, - File.dirname(hpxml_path), - 'Emissions File') - end - end - - # Check/update schedule file references. - # - # @param hpxml_bldg_header [TODO] TODO - # @param hpxml_path [String] Path to the HPXML file - # @return [TODO] TODO - def check_schedule_references(hpxml_bldg_header, hpxml_path) - hpxml_bldg_header.schedules_filepaths = hpxml_bldg_header.schedules_filepaths.collect { |sfp| - FilePath.check_path(sfp, - File.dirname(hpxml_path), - 'Schedules') - } - end - - # TODO + # Initialize heat/load frac variables ahead of HVAC system methods. + # Globalize select HPXML Building properties, e.g., conditioned floor area, number of conditioned floors, number of bedrooms, etc. + # Perform other leading operations like applying unit multipliers, collapsing like HPXML surfaces, etc. # - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @return [TODO] TODO - def validate_emissions_files(hpxml_header) - hpxml_header.emissions_scenarios.each do |scenario| - next if scenario.elec_schedule_filepath.nil? - - data = File.readlines(scenario.elec_schedule_filepath) - num_header_rows = scenario.elec_schedule_number_of_header_rows - col_index = scenario.elec_schedule_column_number - 1 - - if data.size != 8760 + num_header_rows - fail "Emissions File has invalid number of rows (#{data.size}). Expected 8760 plus #{num_header_rows} header row(s)." - end - if col_index > data[num_header_rows, 8760].map { |x| x.count(',') }.min - fail "Emissions File has too few columns. Cannot find column number (#{scenario.elec_schedule_column_number})." - end - end - end - - # TODO - # - # @return [TODO] TODO + # @return [nil] def set_inits_and_globals() # Initialize @remaining_heat_load_frac = 1.0 @@ -611,20 +564,20 @@ def set_inits_and_globals() end end - # TODO + # Call the SimControls module for applying high-level simulation controls and settings. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [TODO] TODO + # @return [nil] def add_simulation_params(model) SimControls.apply(model, @hpxml_header) end - # TODO + # Call the apply_occupants method for creating an OpenStudio People object. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_num_occupants(model, runner, spaces) # Occupants if @hpxml_bldg.building_occupancy.number_of_residents.nil? # Asset calculation @@ -997,13 +950,13 @@ def add_floors(runner, model, spaces) end end - # TODO + # Adds any HPXML Foundation Walls and Slabs to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_foundation_walls_slabs(runner, model, weather, spaces) foundation_types = @hpxml_bldg.slabs.map { |s| s.interior_adjacent_to }.uniq @@ -1121,14 +1074,14 @@ def add_foundation_walls_slabs(runner, model, weather, spaces) end end - # TODO + # Adds an HPXML Foundation Wall to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param foundation_wall [TODO] TODO - # @param exposed_length [TODO] TODO - # @param fnd_wall_length [TODO] TODO + # @param foundation_wall [HPXML::FoundationWall] Object for /HPXML/Building/BuildingDetails/Enclosure/FoundationWalls/FoundationWall + # @param exposed_length [Double] TODO + # @param fnd_wall_length [Double] TODO # @return [TODO] TODO def add_foundation_wall(runner, model, spaces, foundation_wall, exposed_length, fnd_wall_length) exposed_fraction = exposed_length / fnd_wall_length @@ -1211,16 +1164,16 @@ def add_foundation_wall(runner, model, spaces, foundation_wall, exposed_length, return surface.adjacentFoundation.get end - # TODO + # Adds an HPXML Slab to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param slab [TODO] TODO + # @param slab [HPXML::Slab] Object for /HPXML/Building/BuildingDetails/Enclosure/Slabs/Slab # @param z_origin [TODO] TODO - # @param exposed_length [TODO] TODO - # @param kiva_foundation [TODO] TODO - # @return [TODO] TODO + # @param exposed_length [Double] TODO + # @param kiva_foundation [Double] TODO + # @return [nil] def add_foundation_slab(model, weather, spaces, slab, z_origin, exposed_length, kiva_foundation) exposed_fraction = exposed_length / slab.exposed_perimeter slab_tot_perim = exposed_length @@ -1320,7 +1273,7 @@ def add_foundation_slab(model, weather, spaces, slab, z_origin, exposed_length, # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_conditioned_floor_area(model, spaces) sum_cfa = 0.0 @hpxml_bldg.floors.each do |floor| @@ -1376,11 +1329,11 @@ def add_conditioned_floor_area(model, spaces) apply_adiabatic_construction(model, [floor_surface, ceiling_surface], 'floor') end - # TODO + # Calls construction methods for applying partition walls and furniture to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_thermal_mass(model, spaces) if @apply_ashrae140_assumptions # 1024 ft2 of interior partition wall mass, no furniture mass @@ -1654,9 +1607,9 @@ def add_doors(model, spaces) # adiabatic or surface net area is near zero. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param surfaces [TODO] TODO - # @param type [TODO] TODO - # @return [TODO] TODO + # @param surfaces [Array] array of OpenStudio::Model::Surface objects + # @param type [String] floor, wall, or roof + # @return [nil] def apply_adiabatic_construction(model, surfaces, type) return if surfaces.empty? @@ -1679,13 +1632,24 @@ def apply_adiabatic_construction(model, surfaces, type) end end - # TODO + # First assign OpenStudio Space object for appliances based on HPXML Location. + # Then adds any of the following to the OpenStudio model: + # - HPXML Clothes Washers + # - HPXML Clothes Dryers + # - HPXML Dishwashers + # - HPXML Refrigerators + # - HPXML Freezers + # - HPXML Cooking Ranges / Ovens + # - HPXML Hot Water Distribution + # - HPXML Solar Thermal System + # - HPXML Water Heating Systems + # - HPXML Water Fixtures # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_hot_water_and_appliances(runner, model, weather, spaces) # Assign spaces @hpxml_bldg.clothes_washers.each do |clothes_washer| @@ -1763,14 +1727,15 @@ def add_hot_water_and_appliances(runner, model, weather, spaces) Waterheater.apply_combi_system_EMS(model, @hpxml_bldg.water_heating_systems, plantloop_map) end + # Adds any HPXML Cooling Systems to the OpenStudio model. # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [TODO] TODO - # @return [TODO] TODO + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] def add_cooling_system(model, runner, weather, spaces, airloop_map) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get @@ -1818,14 +1783,15 @@ def add_cooling_system(model, runner, weather, spaces, airloop_map) end end + # Adds any HPXML Heating Systems to the OpenStudio model. # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [TODO] TODO - # @return [TODO] TODO + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] def add_heating_system(runner, model, weather, spaces, airloop_map) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get @@ -1891,14 +1857,15 @@ def add_heating_system(runner, model, weather, spaces, airloop_map) end end + # Adds any HPXML Heat Pumps to the OpenStudio model. # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [TODO] TODO - # @return [TODO] TODO + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] def add_heat_pump(runner, model, weather, spaces, airloop_map) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get @@ -2000,13 +1967,14 @@ def add_ideal_system(model, spaces, weather) end end + # Adds an HPXML HVAC Control to the OpenStudio model. # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_setpoints(runner, model, weather, spaces) return if @hpxml_bldg.hvac_controls.size == 0 @@ -2017,13 +1985,13 @@ def add_setpoints(runner, model, weather, spaces) HVAC.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone, has_ceiling_fan, @heating_days, @cooling_days, @hpxml_header, @schedules_file) end - # TODO + # Adds an HPXML Ceiling Fan to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_ceiling_fans(runner, model, weather, spaces) return if @hpxml_bldg.ceiling_fans.size == 0 @@ -2032,12 +2000,12 @@ def add_ceiling_fans(runner, model, weather, spaces) @schedules_file, @hpxml_header.unavailable_periods) end - # TODO + # Adds any HPXML Dehumidifiers to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_dehumidifiers(runner, model, spaces) return if @hpxml_bldg.dehumidifiers.size == 0 @@ -2049,7 +2017,7 @@ def add_dehumidifiers(runner, model, spaces) # # @param hvac_distribution [TODO] TODO # @param system_type [TODO] TODO - # @return [TODO] TODO + # @return [nil] def check_distribution_system(hvac_distribution, system_type) return if hvac_distribution.nil? @@ -2068,12 +2036,12 @@ def check_distribution_system(hvac_distribution, system_type) end end - # TODO + # Adds any HPXML Plug Loads to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_mels(runner, model, spaces) # Misc @hpxml_bldg.plug_loads.each do |plug_load| @@ -2096,12 +2064,12 @@ def add_mels(runner, model, spaces) end end - # TODO + # Adds any HPXML Fuel Loads to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_mfls(runner, model, spaces) # Misc @hpxml_bldg.fuel_loads.each do |fuel_load| @@ -2122,23 +2090,23 @@ def add_mfls(runner, model, spaces) end end - # TODO + # Adds any HPXML Lighting Groups and Lighting to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_lighting(runner, model, spaces) Lighting.apply(runner, model, spaces, @hpxml_bldg.lighting_groups, @hpxml_bldg.lighting, @eri_version, @schedules_file, @cfa, @hpxml_header.unavailable_periods, @hpxml_bldg.building_construction.number_of_units) end - # TODO + # Adds any HPXML Pools and Permanent Spas to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def add_pools_and_permanent_spas(runner, model, spaces) (@hpxml_bldg.pools + @hpxml_bldg.permanent_spas).each do |pool_or_spa| next if pool_or_spa.type == HPXML::TypeNone @@ -2158,8 +2126,8 @@ def add_pools_and_permanent_spas(runner, model, spaces) # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [TODO] TODO - # @return [TODO] TODO + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] def add_airflow(runner, model, weather, spaces, airloop_map) # Ducts duct_systems = {} @@ -2332,7 +2300,7 @@ def create_ducts(model, hvac_distribution, spaces) return air_ducts end - # TODO + # Adds any HPXML Photovoltaics to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @return [nil] @@ -2347,7 +2315,7 @@ def add_photovoltaics(model) end end - # TODO + # Adds any HPXML Generators to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @return [nil] @@ -2357,7 +2325,7 @@ def add_generators(model) end end - # TODO + # Adds any HPXML Batteries to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -2374,8 +2342,8 @@ def add_batteries(runner, model, spaces) # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param unit_num [TODO] TODO - # @return [TODO] TODO + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [nil] def add_building_unit(model, unit_num) return if unit_num.nil? @@ -2386,7 +2354,7 @@ def add_building_unit(model, unit_num) end end - # Store some data for use in reporting measure + # Store some data for use in reporting measure. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml [HPXML] HPXML object @@ -2394,7 +2362,7 @@ def add_building_unit(model, unit_num) # @param hpxml_path [String] Path to the HPXML file # @param building_id [TODO] TODO # @param hpxml_defaults_path [TODO] TODO - # @return [TODO] TODO + # @return [nil] def add_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_id, hpxml_defaults_path) additionalProperties = model.getBuilding.additionalProperties additionalProperties.setFeature('hpxml_path', hpxml_path) @@ -2421,7 +2389,7 @@ def add_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_ # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit # @param hpxml [HPXML] HPXML object - # @return [TODO] TODO + # @return [Hash] TODO def add_unmet_hours_output(model, hpxml_osm_map, hpxml) # Create sensors and gather data htg_sensors, clg_sensors = {}, {} @@ -2663,7 +2631,7 @@ def add_total_loads_output(model, hpxml_osm_map) # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit # @param loads_data [TODO] TODO # @param season_day_nums [TODO] TODO - # @return [TODO] TODO + # @return [nil] def add_component_loads_output(model, hpxml_osm_map, loads_data, season_day_nums) htg_cond_load_sensors, clg_cond_load_sensors, total_heat_load_serveds, total_cool_load_serveds, dehumidifier_sensors = loads_data @@ -3150,10 +3118,11 @@ def add_total_airflows_output(model, hpxml_osm_map) program_calling_manager.addProgram(program) end - # TODO + # Populate fields of both unique OpenStudio objects OutputJSON and OutputControlFiles based on the debug argument. + # Always request MessagePack output. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [TODO] TODO + # @return [nil] def set_output_files(model) oj = model.getOutputJSON oj.setOptionType('TimeSeriesAndTabular') @@ -3179,7 +3148,7 @@ def set_output_files(model) # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [TODO] TODO + # @return [nil] def add_ems_debug_output(model) oems = model.getOutputEnergyManagementSystem oems.setActuatorAvailabilityDictionaryReporting('Verbose') @@ -3191,9 +3160,9 @@ def add_ems_debug_output(model) # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param surface [TODO] TODO - # @param hpxml_surface [TODO] TODO - # @return [TODO] TODO + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object + # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface + # @return [nil] def set_surface_interior(model, spaces, surface, hpxml_surface) interior_adjacent_to = hpxml_surface.interior_adjacent_to if HPXML::conditioned_below_grade_locations.include? interior_adjacent_to @@ -3207,9 +3176,9 @@ def set_surface_interior(model, spaces, surface, hpxml_surface) # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param surface [TODO] TODO - # @param hpxml_surface [TODO] TODO - # @return [TODO] TODO + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object + # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface + # @return [nil] def set_surface_exterior(model, spaces, surface, hpxml_surface) exterior_adjacent_to = hpxml_surface.exterior_adjacent_to is_adiabatic = hpxml_surface.is_adiabatic @@ -3233,11 +3202,11 @@ def set_surface_exterior(model, spaces, surface, hpxml_surface) # TODO # - # @param surface [TODO] TODO + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object # @param exterior_adjacent_to [TODO] TODO # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [nil] def set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spaces) otherside_coeffs = nil model.getSurfacePropertyOtherSideCoefficientss.each do |c| @@ -3265,7 +3234,7 @@ def set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spa # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param location [String] the location of interest (HPXML::LocationXXX) # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [OpenStudio::Model::ScheduleConstant] OpenStudio ScheduleConstant object def get_space_temperature_schedule(model, location, spaces) # return if already exists model.getScheduleConstants.each do |sch| @@ -3407,7 +3376,7 @@ def get_space_or_schedule_from_location(location, model, spaces) # # @param location [String] the location of interest (HPXML::LocationXXX) # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [OpenStudio::Model::Space] OpenStudio Space object def get_space_from_location(location, spaces) return if [HPXML::LocationOutside, HPXML::LocationOtherHeatedSpace, @@ -3425,10 +3394,10 @@ def get_space_from_location(location, spaces) # Set its parent surface outside boundary condition, which will be also applied to subsurfaces through OS # The parent surface is entirely comprised of the subsurface. # - # @param surface [TODO] TODO + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_surface [TODO] TODO + # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface # @return [nil] def set_subsurface_exterior(surface, spaces, model, hpxml_surface) # Subsurface on foundation wall, set it to be adjacent to outdoors diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 0542b45f6e..c85d1a0e02 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 9817033e-3f82-469b-a5f1-c0b1a78586b2 - 2024-08-30T16:16:59Z + 017ee7e0-7ff2-420a-a156-7341f22c0e2b + 2024-08-30T18:22:01Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - 03FE08F4 + 76DE8058 airflow.rb @@ -345,7 +345,7 @@ geometry.rb rb resource - FECF8D4F + 196E00C2 hotwater_appliances.rb @@ -393,7 +393,7 @@ hvac.rb rb resource - 64AEF468 + 7EC4C04D hvac_sizing.rb @@ -585,7 +585,7 @@ schedules.rb rb resource - 0B74AC68 + 79BD7907 simcontrols.rb diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index cc66db4f07..fe13b6d42f 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -486,7 +486,7 @@ def self.explode_surfaces(model:, end end - # TODO + # Create an OpenStudio People object using number of occupants and people/activity schedules. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -775,7 +775,7 @@ def self.calculate_subsurface_parent_buffer(length:, # Shift units so they aren't right on top and shade each other. # - # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object + # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object (corresponding to one of multiple dwelling units) # @param unit_number [Integer] index number corresponding to an HPXML Building object # @return [nil] def self.shift_unit(unit_model, unit_number) diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index 5736b7f28c..8d849c3f58 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -785,7 +785,7 @@ def self.apply_boiler(model, runner, heating_system, sequential_heat_load_fracs, # @param sequential_heat_load_fracs [TODO] TODO # @param control_zone [OpenStudio::Model::ThermalZone] Conditioned space thermal zone # @param hvac_unavailable_periods [TODO] TODO - # @return [TODO] TODO + # @return [nil] def self.apply_electric_baseboard(model, heating_system, sequential_heat_load_fracs, control_zone, hvac_unavailable_periods) @@ -2362,7 +2362,7 @@ def self.create_air_loop_unitary_system(model, obj_name, fan, htg_coil, clg_coil # @param airflow_cfm [TODO] TODO # @param heating_system [TODO] TODO # @param hvac_unavailable_periods [TODO] TODO - # @return [TODO] TODO + # @return [OpenStudio::Model::AirLoopHVAC] OpenStudio Air Loop HVAC object def self.create_air_loop(model, obj_name, system, control_zone, sequential_heat_load_fracs, sequential_cool_load_fracs, airflow_cfm, heating_system, hvac_unavailable_periods) air_loop = OpenStudio::Model::AirLoopHVAC.new(model) air_loop.setAvailabilitySchedule(model.alwaysOnDiscreteSchedule) diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index f5b036d5a9..c742a8f41e 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1096,6 +1096,58 @@ def self.valid_float?(str) end return floats end + + # Check/update emissions file references. + # + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param hpxml_path [String] Path to the HPXML file + # @return [nil] + def self.check_emissions_references(hpxml_header, hpxml_path) + hpxml_header.emissions_scenarios.each do |scenario| + if hpxml_header.emissions_scenarios.select { |s| s.emissions_type == scenario.emissions_type && s.name == scenario.name }.size > 1 + fail "Found multiple Emissions Scenarios with the Scenario Name=#{scenario.name} and Emissions Type=#{scenario.emissions_type}." + end + next if scenario.elec_schedule_filepath.nil? + + scenario.elec_schedule_filepath = FilePath.check_path(scenario.elec_schedule_filepath, + File.dirname(hpxml_path), + 'Emissions File') + end + end + + # Check/update schedule file references. + # + # @param hpxml_bldg_header [TODO] TODO + # @param hpxml_path [String] Path to the HPXML file + # @return [nil] + def self.check_schedule_references(hpxml_bldg_header, hpxml_path) + hpxml_bldg_header.schedules_filepaths = hpxml_bldg_header.schedules_filepaths.collect { |sfp| + FilePath.check_path(sfp, + File.dirname(hpxml_path), + 'Schedules') + } + end + + # Check that any electricity emissions schedule files contain the correct number of rows and columns. + # + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.validate_emissions_files(hpxml_header) + hpxml_header.emissions_scenarios.each do |scenario| + next if scenario.elec_schedule_filepath.nil? + + data = File.readlines(scenario.elec_schedule_filepath) + num_header_rows = scenario.elec_schedule_number_of_header_rows + col_index = scenario.elec_schedule_column_number - 1 + + if data.size != 8760 + num_header_rows + fail "Emissions File has invalid number of rows (#{data.size}). Expected 8760 plus #{num_header_rows} header row(s)." + end + if col_index > data[num_header_rows, 8760].map { |x| x.count(',') }.min + fail "Emissions File has too few columns. Cannot find column number (#{scenario.elec_schedule_column_number})." + end + end + end end # Object that contains information for detailed schedule CSVs. From 08f8c95ff9a4e9390bcb7dfb921dd1784945c51d Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 3 Sep 2024 12:09:44 -0700 Subject: [PATCH 03/16] Clean up and refactor methods called from create_unit_model. --- HPXMLtoOpenStudio/measure.rb | 194 ++++++++++++++---------- HPXMLtoOpenStudio/measure.xml | 8 +- HPXMLtoOpenStudio/resources/geometry.rb | 4 +- 3 files changed, 123 insertions(+), 83 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index c3295e837e..63b9f6956a 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -451,6 +451,7 @@ def make_variable_name(obj_name, unit_number) def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug, schedules_file, eri_version, unit_num) @hpxml_header = hpxml.header @hpxml_bldg = hpxml_bldg + @epw_path = epw_path @debug = debug @schedules_file = schedules_file @eri_version = eri_version @@ -467,17 +468,20 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug model.setStrictnessLevel('None'.to_StrictnessLevel) # Init - OpenStudio::Model::WeatherFile.setWeatherFile(model, OpenStudio::EpwFile.new(epw_path)) - set_inits_and_globals() - Location.apply(model, weather, @hpxml_header, @hpxml_bldg) + set_inits_and_globals(model) + set_foundation_and_walls_top() + set_hvac_seasons(runner) + set_unavailable_periods(runner) + + spaces = {} # Map of HPXML locations => OpenStudio Space objects + airloop_map = {} # Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil) + + # Sim controls/location add_simulation_params(model) + add_location(model, weather) # Conditioned space/zone - spaces = {} - Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg) - set_foundation_and_walls_top() - set_heating_and_cooling_seasons(runner) - add_setpoints(runner, model, weather, spaces) + add_spaces(model, spaces) # Geometry/Envelope add_roofs(runner, model, spaces) @@ -490,13 +494,11 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug add_skylights(model, spaces) add_conditioned_floor_area(model, spaces) add_thermal_mass(model, spaces) - Geometry.set_zone_volumes(spaces: spaces, hpxml_bldg: @hpxml_bldg, apply_ashrae140_assumptions: @apply_ashrae140_assumptions) - Geometry.explode_surfaces(model: model, hpxml_bldg: @hpxml_bldg, walls_top: @walls_top) add_num_occupants(model, runner, spaces) + add_geometry_other(model, spaces) # HVAC - @hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, @hpxml_header.unavailable_periods) - airloop_map = {} # Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil) + add_setpoints(runner, model, weather, spaces) add_ideal_system(model, spaces, weather) add_cooling_system(model, runner, weather, spaces, airloop_map) add_heating_system(runner, model, weather, spaces, airloop_map) @@ -527,8 +529,12 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug # Globalize select HPXML Building properties, e.g., conditioned floor area, number of conditioned floors, number of bedrooms, etc. # Perform other leading operations like applying unit multipliers, collapsing like HPXML surfaces, etc. # + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @return [nil] - def set_inits_and_globals() + def set_inits_and_globals(model) + # Weather file + OpenStudio::Model::WeatherFile.setWeatherFile(model, OpenStudio::EpwFile.new(@epw_path)) + # Initialize @remaining_heat_load_frac = 1.0 @remaining_cool_load_frac = 1.0 @@ -564,7 +570,59 @@ def set_inits_and_globals() end end - # Call the SimControls module for applying high-level simulation controls and settings. + # TODO + # + # @return [nil] + def set_foundation_and_walls_top() + @foundation_top = 0 + @hpxml_bldg.floors.each do |floor| + # Keeping the floor at ground level for ASHRAE 140 tests yields the expected results + if floor.is_floor && floor.is_exterior && !@apply_ashrae140_assumptions + @foundation_top = 2.0 + end + end + @hpxml_bldg.foundation_walls.each do |foundation_wall| + top = -1 * foundation_wall.depth_below_grade + foundation_wall.height + @foundation_top = top if top > @foundation_top + end + @walls_top = @foundation_top + @hpxml_bldg.building_construction.average_ceiling_height * @ncfl_ag + end + + # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling season begin/end month/day, respectively. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @return [nil] + def set_hvac_seasons(runner) + return if @hpxml_bldg.hvac_controls.size == 0 + + hvac_control = @hpxml_bldg.hvac_controls[0] + + htg_start_month = hvac_control.seasons_heating_begin_month + htg_start_day = hvac_control.seasons_heating_begin_day + htg_end_month = hvac_control.seasons_heating_end_month + htg_end_day = hvac_control.seasons_heating_end_day + clg_start_month = hvac_control.seasons_cooling_begin_month + clg_start_day = hvac_control.seasons_cooling_begin_day + clg_end_month = hvac_control.seasons_cooling_end_month + clg_end_day = hvac_control.seasons_cooling_end_day + + @heating_days = Calendar.get_daily_season(@hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) + @cooling_days = Calendar.get_daily_season(@hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) + + if (htg_start_month != 1) || (htg_start_day != 1) || (htg_end_month != 12) || (htg_end_day != 31) || (clg_start_month != 1) || (clg_start_day != 1) || (clg_end_month != 12) || (clg_end_day != 31) + runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.') + end + end + + # TODO + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @return [nil] + def set_unavailable_periods(runner) + @hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, @hpxml_header.unavailable_periods) + end + + # Adds HPXML Simulation Control to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @return [nil] @@ -572,7 +630,23 @@ def add_simulation_params(model) SimControls.apply(model, @hpxml_header) end - # Call the apply_occupants method for creating an OpenStudio People object. + # Adds HPXML Building Site to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @return [nil] + def add_location(model, weather) + Location.apply(model, weather, @hpxml_header, @hpxml_bldg) + end + + # TODO + # + # @return [nil] + def add_spaces(model, spaces) + Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg) + end + + # Adds HPXML Building Occupancy to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -1079,10 +1153,10 @@ def add_foundation_walls_slabs(runner, model, weather, spaces) # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param foundation_wall [HPXML::FoundationWall] Object for /HPXML/Building/BuildingDetails/Enclosure/FoundationWalls/FoundationWall + # @param foundation_wall [HPXML::FoundationWall] HPXML Foundation Wall object # @param exposed_length [Double] TODO # @param fnd_wall_length [Double] TODO - # @return [TODO] TODO + # @return [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object def add_foundation_wall(runner, model, spaces, foundation_wall, exposed_length, fnd_wall_length) exposed_fraction = exposed_length / fnd_wall_length net_exposed_area = foundation_wall.net_area * exposed_fraction @@ -1169,10 +1243,10 @@ def add_foundation_wall(runner, model, spaces, foundation_wall, exposed_length, # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param slab [HPXML::Slab] Object for /HPXML/Building/BuildingDetails/Enclosure/Slabs/Slab - # @param z_origin [TODO] TODO + # @param slab [HPXML::Slab] HPXML Slab object + # @param z_origin [Double] The z-coordinate for which the slab is relative (ft) # @param exposed_length [Double] TODO - # @param kiva_foundation [Double] TODO + # @param kiva_foundation [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object # @return [nil] def add_foundation_slab(model, weather, spaces, slab, z_origin, exposed_length, kiva_foundation) exposed_fraction = exposed_length / slab.exposed_perimeter @@ -1602,6 +1676,16 @@ def add_doors(model, spaces) apply_adiabatic_construction(model, surfaces, 'wall') end + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [nil] + def add_geometry_other(model, spaces) + Geometry.set_zone_volumes(spaces: spaces, hpxml_bldg: @hpxml_bldg, apply_ashrae140_assumptions: @apply_ashrae140_assumptions) + Geometry.explode_surfaces(model: model, hpxml_bldg: @hpxml_bldg, walls_top: @walls_top) + end + # Arbitrary construction for heat capacitance. # Only applies to surfaces where outside boundary conditioned is # adiabatic or surface net area is near zero. @@ -1728,7 +1812,7 @@ def add_hot_water_and_appliances(runner, model, weather, spaces) end # Adds any HPXML Cooling Systems to the OpenStudio model. - # TODO + # TODO for adding more description (e.g., around sequential load fractions) # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -1784,7 +1868,7 @@ def add_cooling_system(model, runner, weather, spaces, airloop_map) end # Adds any HPXML Heating Systems to the OpenStudio model. - # TODO + # TODO for adding more description (e.g., around sequential load fractions) # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -1858,7 +1942,7 @@ def add_heating_system(runner, model, weather, spaces, airloop_map) end # Adds any HPXML Heat Pumps to the OpenStudio model. - # TODO + # TODO for adding more description (e.g., around sequential load fractions) # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -1968,7 +2052,6 @@ def add_ideal_system(model, spaces, weather) end # Adds an HPXML HVAC Control to the OpenStudio model. - # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -2013,10 +2096,10 @@ def add_dehumidifiers(runner, model, spaces) @hpxml_bldg.building_construction.number_of_units) end - # TODO + # Check provided HVAC system and distribution types against what is allowed. # - # @param hvac_distribution [TODO] TODO - # @param system_type [TODO] TODO + # @param hvac_distribution [HPXML::HVACDistribution] HPXML HVAC Distribution object + # @param system_type [String] the HVAC system type of interest # @return [nil] def check_distribution_system(hvac_distribution, system_type) return if hvac_distribution.nil? @@ -2120,7 +2203,8 @@ def add_pools_and_permanent_spas(runner, model, spaces) end end - # TODO + # Adds HPXML Air Infiltration and HPXML HVAC Distribution to the OpenStudio model. + # TODO for adding more description (e.g., around checks and warnings) # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -2218,9 +2302,9 @@ def add_airflow(runner, model, weather, spaces, airloop_map) # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hvac_distribution [TODO] TODO + # @param hvac_distribution [HPXML::HVACDistribution] HPXML HVAC Distribution object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [Array] list of initialized Duct class objects from the airflow resource file def create_ducts(model, hvac_distribution, spaces) air_ducts = [] @@ -2360,7 +2444,7 @@ def add_building_unit(model, unit_num) # @param hpxml [HPXML] HPXML object # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit # @param hpxml_path [String] Path to the HPXML file - # @param building_id [TODO] TODO + # @param building_id [String] HPXML Building ID # @param hpxml_defaults_path [TODO] TODO # @return [nil] def add_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_id, hpxml_defaults_path) @@ -3203,7 +3287,7 @@ def set_surface_exterior(model, spaces, surface, hpxml_surface) # TODO # # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object - # @param exterior_adjacent_to [TODO] TODO + # @param exterior_adjacent_to [String] Exterior adjacent to location (HPXML::LocationXXX) # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [nil] @@ -3347,7 +3431,7 @@ def get_space_temperature_schedule(model, location, spaces) # @param location [String] the location of interest (HPXML::LocationXXX) # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [TODO] TODO + # @return [OpenStudio::Model::Space or OpenStudio::Model::ScheduleConstant] OpenStudio Space or Schedule object def get_space_or_schedule_from_location(location, model, spaces) return if [HPXML::LocationOtherExterior, HPXML::LocationOutside, @@ -3407,50 +3491,6 @@ def set_subsurface_exterior(surface, spaces, model, hpxml_surface) set_surface_exterior(model, spaces, surface, hpxml_surface) end end - - # TODO - # - # @return [nil] - def set_foundation_and_walls_top() - @foundation_top = 0 - @hpxml_bldg.floors.each do |floor| - # Keeping the floor at ground level for ASHRAE 140 tests yields the expected results - if floor.is_floor && floor.is_exterior && !@apply_ashrae140_assumptions - @foundation_top = 2.0 - end - end - @hpxml_bldg.foundation_walls.each do |foundation_wall| - top = -1 * foundation_wall.depth_below_grade + foundation_wall.height - @foundation_top = top if top > @foundation_top - end - @walls_top = @foundation_top + @hpxml_bldg.building_construction.average_ceiling_height * @ncfl_ag - end - - # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling season begin/end month/day, respectively. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @return [nil] - def set_heating_and_cooling_seasons(runner) - return if @hpxml_bldg.hvac_controls.size == 0 - - hvac_control = @hpxml_bldg.hvac_controls[0] - - htg_start_month = hvac_control.seasons_heating_begin_month - htg_start_day = hvac_control.seasons_heating_begin_day - htg_end_month = hvac_control.seasons_heating_end_month - htg_end_day = hvac_control.seasons_heating_end_day - clg_start_month = hvac_control.seasons_cooling_begin_month - clg_start_day = hvac_control.seasons_cooling_begin_day - clg_end_month = hvac_control.seasons_cooling_end_month - clg_end_day = hvac_control.seasons_cooling_end_day - - @heating_days = Calendar.get_daily_season(@hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) - @cooling_days = Calendar.get_daily_season(@hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) - - if (htg_start_month != 1) || (htg_start_day != 1) || (htg_end_month != 12) || (htg_end_day != 31) || (clg_start_month != 1) || (clg_start_day != 1) || (clg_end_month != 12) || (clg_end_day != 31) - runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.') - end - end end # register the measure to be used by the application diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index c85d1a0e02..16cf4e9281 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 017ee7e0-7ff2-420a-a156-7341f22c0e2b - 2024-08-30T18:22:01Z + 51d9506d-72cb-4698-8f80-f74ab37168d4 + 2024-09-03T19:08:15Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - 76DE8058 + B23EBC72 airflow.rb @@ -345,7 +345,7 @@ geometry.rb rb resource - 196E00C2 + C8582762 hotwater_appliances.rb diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index fe13b6d42f..7ccdcff353 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -255,7 +255,7 @@ def self.create_wall_vertices(length:, # # @param length [TODO] TODO # @param width [TODO] TODO - # @param z_origin [TODO] TODO + # @param z_origin [Double] The z-coordinate for which the length and width are relative (ft) # @param default_azimuths [TODO] TODO # @return [TODO] TODO def self.create_ceiling_vertices(length:, @@ -269,7 +269,7 @@ def self.create_ceiling_vertices(length:, # # @param length [TODO] TODO # @param width [TODO] TODO - # @param z_origin [TODO] TODO + # @param z_origin [Double] The z-coordinate for which the length and width are relative (ft) # @param default_azimuths [TODO] TODO # @return [TODO] TODO def self.create_floor_vertices(length:, From e50348fb38ef92266abf212becb03fb56914133e Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 3 Sep 2024 13:34:23 -0700 Subject: [PATCH 04/16] Must have add_setpoints before enclosure. --- HPXMLtoOpenStudio/measure.rb | 2 +- HPXMLtoOpenStudio/measure.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 63b9f6956a..9d27ec608b 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -482,6 +482,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug # Conditioned space/zone add_spaces(model, spaces) + add_setpoints(runner, model, weather, spaces) # Geometry/Envelope add_roofs(runner, model, spaces) @@ -498,7 +499,6 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug add_geometry_other(model, spaces) # HVAC - add_setpoints(runner, model, weather, spaces) add_ideal_system(model, spaces, weather) add_cooling_system(model, runner, weather, spaces, airloop_map) add_heating_system(runner, model, weather, spaces, airloop_map) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 16cf4e9281..6ee31d80ce 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 51d9506d-72cb-4698-8f80-f74ab37168d4 - 2024-09-03T19:08:15Z + b3002480-328b-49af-bb30-ebcf37ca8737 + 2024-09-03T20:33:51Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - B23EBC72 + 41FF93B9 airflow.rb From 38763d5d32e5ab4a5a372bc8ea654844a4c9014c Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 3 Sep 2024 14:26:09 -0700 Subject: [PATCH 05/16] Continue to relocate constructions and geometry related methods. --- HPXMLtoOpenStudio/measure.rb | 356 ++----------------- HPXMLtoOpenStudio/measure.xml | 10 +- HPXMLtoOpenStudio/resources/constructions.rb | 30 ++ HPXMLtoOpenStudio/resources/geometry.rb | 254 ++++++++++++- 4 files changed, 325 insertions(+), 325 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 9d27ec608b..9ea510b28e 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -481,7 +481,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug add_location(model, weather) # Conditioned space/zone - add_spaces(model, spaces) + add_conditioned_space(model, spaces) add_setpoints(runner, model, weather, spaces) # Geometry/Envelope @@ -639,10 +639,10 @@ def add_location(model, weather) Location.apply(model, weather, @hpxml_header, @hpxml_bldg) end - # TODO + # Adds a conditioned space and zone to the OpenStudio model. # # @return [nil] - def add_spaces(model, spaces) + def add_conditioned_space(model, spaces) Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg) end @@ -707,7 +707,7 @@ def add_roofs(runner, model, spaces) end surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) - set_surface_interior(model, spaces, surface, roof) + Geometry.set_surface_interior(model, spaces, surface, roof, @hpxml_bldg) end next if surfaces.empty? @@ -822,8 +822,8 @@ def add_walls(runner, model, spaces) surface.setName(wall.id) end surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, wall) - set_surface_exterior(model, spaces, surface, wall) + Geometry.set_surface_interior(model, spaces, surface, wall, @hpxml_bldg) + Geometry.set_surface_exterior(model, spaces, surface, wall, @hpxml_bldg) if wall.is_interior surface.setSunExposure(EPlus::SurfaceSunExposureNo) surface.setWindExposure(EPlus::SurfaceWindExposureNo) @@ -898,8 +898,8 @@ def add_rim_joists(runner, model, spaces) surface.setName(rim_joist.id) end surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, rim_joist) - set_surface_exterior(model, spaces, surface, rim_joist) + Geometry.set_surface_interior(model, spaces, surface, rim_joist, @hpxml_bldg) + Geometry.set_surface_exterior(model, spaces, surface, rim_joist, @hpxml_bldg) if rim_joist.is_interior surface.setSunExposure(EPlus::SurfaceSunExposureNo) surface.setWindExposure(EPlus::SurfaceWindExposureNo) @@ -967,8 +967,8 @@ def add_floors(runner, model, spaces) surface.additionalProperties.setFeature('SurfaceType', 'Floor') end surface.additionalProperties.setFeature('Tilt', 0.0) - set_surface_interior(model, spaces, surface, floor) - set_surface_exterior(model, spaces, surface, floor) + Geometry.set_surface_interior(model, spaces, surface, floor, @hpxml_bldg) + Geometry.set_surface_exterior(model, spaces, surface, floor, @hpxml_bldg) surface.setName(floor.id) if floor.is_interior surface.setSunExposure(EPlus::SurfaceSunExposureNo) @@ -1108,8 +1108,8 @@ def add_foundation_walls_slabs(runner, model, weather, spaces) surface.additionalProperties.setFeature('SurfaceType', 'FoundationWall') surface.setName(fnd_wall.id) surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, fnd_wall) - set_surface_exterior(model, spaces, surface, fnd_wall) + Geometry.set_surface_interior(model, spaces, surface, fnd_wall, @hpxml_bldg) + Geometry.set_surface_exterior(model, spaces, surface, fnd_wall, @hpxml_bldg) surface.setSunExposure(EPlus::SurfaceSunExposureNo) surface.setWindExposure(EPlus::SurfaceWindExposureNo) @@ -1188,8 +1188,8 @@ def add_foundation_wall(runner, model, spaces, foundation_wall, exposed_length, surface.additionalProperties.setFeature('SurfaceType', 'FoundationWall') surface.setName(foundation_wall.id) surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, foundation_wall) - set_surface_exterior(model, spaces, surface, foundation_wall) + Geometry.set_surface_interior(model, spaces, surface, foundation_wall, @hpxml_bldg) + Geometry.set_surface_exterior(model, spaces, surface, foundation_wall, @hpxml_bldg) assembly_r = foundation_wall.insulation_assembly_r_value mat_int_finish = Material.InteriorFinishMaterial(foundation_wall.interior_finish_type, foundation_wall.interior_finish_thickness) @@ -1267,7 +1267,7 @@ def add_foundation_slab(model, weather, spaces, slab, z_origin, exposed_length, surface.setSurfaceType(EPlus::SurfaceTypeFloor) surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionFoundation) surface.additionalProperties.setFeature('SurfaceType', 'Slab') - set_surface_interior(model, spaces, surface, slab) + Geometry.set_surface_interior(model, spaces, surface, slab, @hpxml_bldg) surface.setSunExposure(EPlus::SurfaceSunExposureNo) surface.setWindExposure(EPlus::SurfaceWindExposureNo) @@ -1400,7 +1400,7 @@ def add_conditioned_floor_area(model, spaces) ceiling_surface.additionalProperties.setFeature('Tilt', 0.0) # Apply Construction - apply_adiabatic_construction(model, [floor_surface, ceiling_surface], 'floor') + Constructions.apply_adiabatic_construction(model, [floor_surface, ceiling_surface], 'floor') end # Calls construction methods for applying partition walls and furniture to the OpenStudio model. @@ -1469,7 +1469,7 @@ def add_windows(model, spaces) surface.additionalProperties.setFeature('SurfaceType', 'Window') surface.setName("surface #{window.id}") surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, window.wall) + Geometry.set_surface_interior(model, spaces, surface, window.wall, @hpxml_bldg) vertices = Geometry.create_wall_vertices(length: window_length, height: window_height, z_origin: z_origin, azimuth: window.azimuth) sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) @@ -1477,7 +1477,7 @@ def add_windows(model, spaces) sub_surface.setSurface(surface) sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeWindow) - set_subsurface_exterior(surface, spaces, model, window.wall) + Geometry.set_subsurface_exterior(surface, spaces, model, window.wall, @hpxml_bldg) surfaces << surface if not overhang_depth.nil? @@ -1505,7 +1505,7 @@ def add_windows(model, spaces) surface.additionalProperties.setFeature('SurfaceType', 'Door') surface.setName("surface #{window.id}") surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, window.wall) + Geometry.set_surface_interior(model, spaces, surface, window.wall, @hpxml_bldg) vertices = Geometry.create_wall_vertices(length: window_length, height: window_height, z_origin: z_origin, azimuth: window.azimuth) sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) @@ -1513,7 +1513,7 @@ def add_windows(model, spaces) sub_surface.setSurface(surface) sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeDoor) - set_subsurface_exterior(surface, spaces, model, window.wall) + Geometry.set_subsurface_exterior(surface, spaces, model, window.wall, @hpxml_bldg) surfaces << surface # Apply construction @@ -1523,7 +1523,7 @@ def add_windows(model, spaces) end end - apply_adiabatic_construction(model, surfaces, 'wall') + Constructions.apply_adiabatic_construction(model, surfaces, 'wall') end # Adds any HPXML Skylights to the OpenStudio model. @@ -1611,8 +1611,8 @@ def add_skylights(model, spaces) surface.additionalProperties.setFeature('SurfaceType', 'Skylight') surface.setName("surface #{skylight.id} shaft") surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, skylight.floor) - set_surface_exterior(model, spaces, surface, skylight.floor) + Geometry.set_surface_interior(model, spaces, surface, skylight.floor, @hpxml_bldg) + Geometry.set_surface_exterior(model, spaces, surface, skylight.floor, @hpxml_bldg) surface.setSunExposure(EPlus::SurfaceSunExposureNo) surface.setWindExposure(EPlus::SurfaceWindExposureNo) @@ -1626,7 +1626,7 @@ def add_skylights(model, spaces) surface.setConstruction(shaft_const) end - apply_adiabatic_construction(model, surfaces, 'roof') + Constructions.apply_adiabatic_construction(model, surfaces, 'roof') end # Adds any HPXML Doors to the OpenStudio model. @@ -1651,7 +1651,7 @@ def add_doors(model, spaces) surface.additionalProperties.setFeature('SurfaceType', 'Door') surface.setName("surface #{door.id}") surface.setSurfaceType(EPlus::SurfaceTypeWall) - set_surface_interior(model, spaces, surface, door.wall) + Geometry.set_surface_interior(model, spaces, surface, door.wall, @hpxml_bldg) vertices = Geometry.create_wall_vertices(length: door_length, height: door_height, z_origin: z_origin, azimuth: door.azimuth) sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) @@ -1659,7 +1659,7 @@ def add_doors(model, spaces) sub_surface.setSurface(surface) sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeDoor) - set_subsurface_exterior(surface, spaces, model, door.wall) + Geometry.set_subsurface_exterior(surface, spaces, model, door.wall, @hpxml_bldg) surfaces << surface # Apply construction @@ -1673,7 +1673,7 @@ def add_doors(model, spaces) Constructions.apply_door(model, [sub_surface], 'Door', ufactor, inside_film, outside_film) end - apply_adiabatic_construction(model, surfaces, 'wall') + Constructions.apply_adiabatic_construction(model, surfaces, 'wall') end # TODO @@ -1686,36 +1686,6 @@ def add_geometry_other(model, spaces) Geometry.explode_surfaces(model: model, hpxml_bldg: @hpxml_bldg, walls_top: @walls_top) end - # Arbitrary construction for heat capacitance. - # Only applies to surfaces where outside boundary conditioned is - # adiabatic or surface net area is near zero. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param surfaces [Array] array of OpenStudio::Model::Surface objects - # @param type [String] floor, wall, or roof - # @return [nil] - def apply_adiabatic_construction(model, surfaces, type) - return if surfaces.empty? - - if type == 'wall' - mat_int_finish = Material.InteriorFinishMaterial(HPXML::InteriorFinishGypsumBoard, 0.5) - mat_ext_finish = Material.ExteriorFinishMaterial(HPXML::SidingTypeWood) - Constructions.apply_wood_stud_wall(model, surfaces, 'AdiabaticWallConstruction', - 0, 1, 3.5, true, 0.1, mat_int_finish, 0, 99, mat_ext_finish, false, - Material.AirFilmVertical, Material.AirFilmVertical, nil) - elsif type == 'floor' - Constructions.apply_wood_frame_floor_ceiling(model, surfaces, 'AdiabaticFloorConstruction', false, - 0, 1, 0.07, 5.5, 0.75, 99, Material.CoveringBare, false, - Material.AirFilmFloorReduced, Material.AirFilmFloorReduced, nil) - elsif type == 'roof' - Constructions.apply_open_cavity_roof(model, surfaces, 'AdiabaticRoofConstruction', - 0, 1, 7.25, 0.07, 7.25, 0.75, 99, - Material.RoofMaterial(HPXML::RoofTypeAsphaltShingles), - false, Material.AirFilmOutside, - Material.AirFilmRoof(Geometry.get_roof_pitch(surfaces)), nil) - end - end - # First assign OpenStudio Space object for appliances based on HPXML Location. # Then adds any of the following to the OpenStudio model: # - HPXML Clothes Washers @@ -1737,26 +1707,26 @@ def apply_adiabatic_construction(model, surfaces, type) def add_hot_water_and_appliances(runner, model, weather, spaces) # Assign spaces @hpxml_bldg.clothes_washers.each do |clothes_washer| - clothes_washer.additional_properties.space = get_space_from_location(clothes_washer.location, spaces) + clothes_washer.additional_properties.space = Geometry.get_space_from_location(clothes_washer.location, spaces) end @hpxml_bldg.clothes_dryers.each do |clothes_dryer| - clothes_dryer.additional_properties.space = get_space_from_location(clothes_dryer.location, spaces) + clothes_dryer.additional_properties.space = Geometry.get_space_from_location(clothes_dryer.location, spaces) end @hpxml_bldg.dishwashers.each do |dishwasher| - dishwasher.additional_properties.space = get_space_from_location(dishwasher.location, spaces) + dishwasher.additional_properties.space = Geometry.get_space_from_location(dishwasher.location, spaces) end @hpxml_bldg.refrigerators.each do |refrigerator| - loc_space, loc_schedule = get_space_or_schedule_from_location(refrigerator.location, model, spaces) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(refrigerator.location, model, spaces) refrigerator.additional_properties.loc_space = loc_space refrigerator.additional_properties.loc_schedule = loc_schedule end @hpxml_bldg.freezers.each do |freezer| - loc_space, loc_schedule = get_space_or_schedule_from_location(freezer.location, model, spaces) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(freezer.location, model, spaces) freezer.additional_properties.loc_space = loc_space freezer.additional_properties.loc_schedule = loc_schedule end @hpxml_bldg.cooking_ranges.each do |cooking_range| - cooking_range.additional_properties.space = get_space_from_location(cooking_range.location, spaces) + cooking_range.additional_properties.space = Geometry.get_space_from_location(cooking_range.location, spaces) end # Distribution @@ -1777,7 +1747,7 @@ def add_hot_water_and_appliances(runner, model, weather, spaces) has_cond_bsmnt = @hpxml_bldg.has_location(HPXML::LocationBasementConditioned) plantloop_map = {} @hpxml_bldg.water_heating_systems.each do |water_heating_system| - loc_space, loc_schedule = get_space_or_schedule_from_location(water_heating_system.location, model, spaces) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) ec_adj = HotWaterAndAppliances.get_dist_energy_consumption_adjustment(has_uncond_bsmnt, has_cond_bsmnt, @cfa, @ncfl, water_heating_system, hot_water_distribution) @@ -1803,7 +1773,7 @@ def add_hot_water_and_appliances(runner, model, weather, spaces) @apply_ashrae140_assumptions) if (not solar_thermal_system.nil?) && (not solar_thermal_system.collector_area.nil?) # Detailed solar water heater - loc_space, loc_schedule = get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) Waterheater.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_system, plantloop_map, unit_multiplier) end @@ -2335,7 +2305,7 @@ def create_ducts(model, hvac_distribution, spaces) next if ducts.duct_type.nil? next if total_unconditioned_duct_area[ducts.duct_type] <= 0 - duct_loc_space, duct_loc_schedule = get_space_or_schedule_from_location(ducts.duct_location, model, spaces) + duct_loc_space, duct_loc_schedule = Geometry.get_space_or_schedule_from_location(ducts.duct_location, model, spaces) # Apportion leakage to individual ducts by surface area duct_leakage_value = leakage_to_outside[ducts.duct_type][0] * ducts.duct_surface_area * ducts.duct_surface_area_multiplier / total_unconditioned_duct_area[ducts.duct_type] @@ -2418,7 +2388,7 @@ def add_generators(model) def add_batteries(runner, model, spaces) @hpxml_bldg.batteries.each do |battery| # Assign space - battery.additional_properties.space = get_space_from_location(battery.location, spaces) + battery.additional_properties.space = Geometry.get_space_from_location(battery.location, spaces) Battery.apply(runner, model, @nbeds, @hpxml_bldg.pv_systems, battery, @schedules_file, @hpxml_bldg.building_construction.number_of_units) end end @@ -3239,258 +3209,6 @@ def add_ems_debug_output(model) oems.setInternalVariableAvailabilityDictionaryReporting('Verbose') oems.setEMSRuntimeLanguageDebugOutputLevel('Verbose') end - - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object - # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface - # @return [nil] - def set_surface_interior(model, spaces, surface, hpxml_surface) - interior_adjacent_to = hpxml_surface.interior_adjacent_to - if HPXML::conditioned_below_grade_locations.include? interior_adjacent_to - surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) - else - surface.setSpace(Geometry.create_or_get_space(model, spaces, interior_adjacent_to, @hpxml_bldg)) - end - end - - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object - # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface - # @return [nil] - def set_surface_exterior(model, spaces, surface, hpxml_surface) - exterior_adjacent_to = hpxml_surface.exterior_adjacent_to - is_adiabatic = hpxml_surface.is_adiabatic - if [HPXML::LocationOutside, HPXML::LocationManufacturedHomeUnderBelly].include? exterior_adjacent_to - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) - elsif exterior_adjacent_to == HPXML::LocationGround - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionFoundation) - elsif is_adiabatic - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) - elsif [HPXML::LocationOtherHeatedSpace, HPXML::LocationOtherMultifamilyBufferSpace, - HPXML::LocationOtherNonFreezingSpace, HPXML::LocationOtherHousingUnit].include? exterior_adjacent_to - set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spaces) - elsif HPXML::conditioned_below_grade_locations.include? exterior_adjacent_to - adjacent_surface = surface.createAdjacentSurface(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)).get - adjacent_surface.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get) - else - adjacent_surface = surface.createAdjacentSurface(Geometry.create_or_get_space(model, spaces, exterior_adjacent_to, @hpxml_bldg)).get - adjacent_surface.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get) - end - end - - # TODO - # - # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object - # @param exterior_adjacent_to [String] Exterior adjacent to location (HPXML::LocationXXX) - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spaces) - otherside_coeffs = nil - model.getSurfacePropertyOtherSideCoefficientss.each do |c| - next unless c.name.to_s == exterior_adjacent_to - - otherside_coeffs = c - end - if otherside_coeffs.nil? - # Create E+ other side coefficient object - otherside_coeffs = OpenStudio::Model::SurfacePropertyOtherSideCoefficients.new(model) - otherside_coeffs.setName(exterior_adjacent_to) - otherside_coeffs.setCombinedConvectiveRadiativeFilmCoefficient(UnitConversions.convert(1.0 / Material.AirFilmVertical.rvalue, 'Btu/(hr*ft^2*F)', 'W/(m^2*K)')) - # Schedule of space temperature, can be shared with water heater/ducts - sch = get_space_temperature_schedule(model, exterior_adjacent_to, spaces) - otherside_coeffs.setConstantTemperatureSchedule(sch) - end - surface.setSurfacePropertyOtherSideCoefficients(otherside_coeffs) - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - end - - # Create outside boundary schedules to be actuated by EMS, - # can be shared by any surface, duct adjacent to / located in those spaces. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param location [String] the location of interest (HPXML::LocationXXX) - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [OpenStudio::Model::ScheduleConstant] OpenStudio ScheduleConstant object - def get_space_temperature_schedule(model, location, spaces) - # return if already exists - model.getScheduleConstants.each do |sch| - next unless sch.name.to_s == location - - return sch - end - - sch = OpenStudio::Model::ScheduleConstant.new(model) - sch.setName(location) - sch.additionalProperties.setFeature('ObjectType', location) - - space_values = Geometry.get_temperature_scheduled_space_values(location: location) - - htg_weekday_setpoints, htg_weekend_setpoints = HVAC.get_default_heating_setpoint(HPXML::HVACControlTypeManual, @eri_version) - if htg_weekday_setpoints.split(', ').uniq.size == 1 && htg_weekend_setpoints.split(', ').uniq.size == 1 && htg_weekday_setpoints.split(', ').uniq == htg_weekend_setpoints.split(', ').uniq - default_htg_sp = htg_weekend_setpoints.split(', ').uniq[0].to_f # F - else - fail 'Unexpected heating setpoints.' - end - - clg_weekday_setpoints, clg_weekend_setpoints = HVAC.get_default_cooling_setpoint(HPXML::HVACControlTypeManual, @eri_version) - if clg_weekday_setpoints.split(', ').uniq.size == 1 && clg_weekend_setpoints.split(', ').uniq.size == 1 && clg_weekday_setpoints.split(', ').uniq == clg_weekend_setpoints.split(', ').uniq - default_clg_sp = clg_weekend_setpoints.split(', ').uniq[0].to_f # F - else - fail 'Unexpected cooling setpoints.' - end - - if location == HPXML::LocationOtherHeatedSpace - if spaces[HPXML::LocationConditionedSpace].thermalZone.get.thermostatSetpointDualSetpoint.is_initialized - # Create a sensor to get dynamic heating setpoint - htg_sch = spaces[HPXML::LocationConditionedSpace].thermalZone.get.thermostatSetpointDualSetpoint.get.heatingSetpointTemperatureSchedule.get - sensor_htg_spt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - sensor_htg_spt.setName('htg_spt') - sensor_htg_spt.setKeyName(htg_sch.name.to_s) - space_values[:temp_min] = sensor_htg_spt.name.to_s - else - # No HVAC system; use the defaulted heating setpoint. - space_values[:temp_min] = default_htg_sp # F - end - end - - # Schedule type limits compatible - schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) - schedule_type_limits.setUnitType('Temperature') - sch.setScheduleTypeLimits(schedule_type_limits) - - # Sensors - if space_values[:indoor_weight] > 0 - if not spaces[HPXML::LocationConditionedSpace].thermalZone.get.thermostatSetpointDualSetpoint.is_initialized - # No HVAC system; use the average of defaulted heating/cooling setpoints. - sensor_ia = UnitConversions.convert((default_htg_sp + default_clg_sp) / 2.0, 'F', 'C') - else - sensor_ia = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Temperature') - sensor_ia.setName('cond_zone_temp') - sensor_ia.setKeyName(spaces[HPXML::LocationConditionedSpace].thermalZone.get.name.to_s) - sensor_ia = sensor_ia.name - end - end - - if space_values[:outdoor_weight] > 0 - sensor_oa = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Drybulb Temperature') - sensor_oa.setName('oa_temp') - end - - if space_values[:ground_weight] > 0 - sensor_gnd = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Surface Ground Temperature') - sensor_gnd.setName('ground_temp') - end - - actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(sch, *EPlus::EMSActuatorScheduleConstantValue) - actuator.setName("#{location.gsub(' ', '_').gsub('-', '_')}_temp_sch") - - # EMS to actuate schedule - program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) - program.setName("#{location.gsub('-', '_')} Temperature Program") - program.addLine("Set #{actuator.name} = 0.0") - if not sensor_ia.nil? - program.addLine("Set #{actuator.name} = #{actuator.name} + (#{sensor_ia} * #{space_values[:indoor_weight]})") - end - if not sensor_oa.nil? - program.addLine("Set #{actuator.name} = #{actuator.name} + (#{sensor_oa.name} * #{space_values[:outdoor_weight]})") - end - if not sensor_gnd.nil? - program.addLine("Set #{actuator.name} = #{actuator.name} + (#{sensor_gnd.name} * #{space_values[:ground_weight]})") - end - if not space_values[:temp_min].nil? - if space_values[:temp_min].is_a? String - min_temp_c = space_values[:temp_min] - else - min_temp_c = UnitConversions.convert(space_values[:temp_min], 'F', 'C') - end - program.addLine("If #{actuator.name} < #{min_temp_c}") - program.addLine("Set #{actuator.name} = #{min_temp_c}") - program.addLine('EndIf') - end - - program_cm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - program_cm.setName("#{program.name} calling manager") - program_cm.setCallingPoint('EndOfSystemTimestepAfterHVACReporting') - program_cm.addProgram(program) - - return sch - end - - # Returns an OS:Space, or temperature OS:Schedule for a MF space, or nil if outside - # Should be called when the object's energy use is sensitive to ambient temperature - # (e.g., water heaters, ducts, and refrigerators). - # - # @param location [String] the location of interest (HPXML::LocationXXX) - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [OpenStudio::Model::Space or OpenStudio::Model::ScheduleConstant] OpenStudio Space or Schedule object - def get_space_or_schedule_from_location(location, model, spaces) - return if [HPXML::LocationOtherExterior, - HPXML::LocationOutside, - HPXML::LocationRoofDeck].include? location - - sch = nil - space = nil - if [HPXML::LocationOtherHeatedSpace, - HPXML::LocationOtherHousingUnit, - HPXML::LocationOtherMultifamilyBufferSpace, - HPXML::LocationOtherNonFreezingSpace, - HPXML::LocationExteriorWall, - HPXML::LocationUnderSlab].include? location - # if located in spaces where we don't model a thermal zone, create and return temperature schedule - sch = get_space_temperature_schedule(model, location, spaces) - else - space = get_space_from_location(location, spaces) - end - - return space, sch - end - - # Returns an OS:Space, or nil if a MF space or outside - # Should be called when the object's energy use is NOT sensitive to ambient temperature - # (e.g., appliances). - # - # @param location [String] the location of interest (HPXML::LocationXXX) - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [OpenStudio::Model::Space] OpenStudio Space object - def get_space_from_location(location, spaces) - return if [HPXML::LocationOutside, - HPXML::LocationOtherHeatedSpace, - HPXML::LocationOtherHousingUnit, - HPXML::LocationOtherMultifamilyBufferSpace, - HPXML::LocationOtherNonFreezingSpace].include? location - - if HPXML::conditioned_locations.include? location - location = HPXML::LocationConditionedSpace - end - - return spaces[location] - end - - # Set its parent surface outside boundary condition, which will be also applied to subsurfaces through OS - # The parent surface is entirely comprised of the subsurface. - # - # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface - # @return [nil] - def set_subsurface_exterior(surface, spaces, model, hpxml_surface) - # Subsurface on foundation wall, set it to be adjacent to outdoors - if hpxml_surface.exterior_adjacent_to == HPXML::LocationGround - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) - else - set_surface_exterior(model, spaces, surface, hpxml_surface) - end - end end # register the measure to be used by the application diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 6ee31d80ce..4a4e56e542 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - b3002480-328b-49af-bb30-ebcf37ca8737 - 2024-09-03T20:33:51Z + 38d82c1e-8b24-47f6-a30e-18cba2e6cd1d + 2024-09-03T21:24:46Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - 41FF93B9 + EFF8B910 airflow.rb @@ -213,7 +213,7 @@ constructions.rb rb resource - F2B9F3E6 + DE13291A data/Xing_okstate_0664D_13659_Table_A-3.csv @@ -345,7 +345,7 @@ geometry.rb rb resource - C8582762 + 08E9B56C hotwater_appliances.rb diff --git a/HPXMLtoOpenStudio/resources/constructions.rb b/HPXMLtoOpenStudio/resources/constructions.rb index c4ced8b388..46a1d5ba3f 100644 --- a/HPXMLtoOpenStudio/resources/constructions.rb +++ b/HPXMLtoOpenStudio/resources/constructions.rb @@ -2712,6 +2712,36 @@ def self.apply_floor_ceiling_construction(runner, model, surface, floor_id, floo check_surface_assembly_rvalue(runner, surface, inside_film, outside_film, assembly_r, match) end + # Arbitrary construction for heat capacitance. + # Only applies to surfaces where outside boundary conditioned is + # adiabatic or surface net area is near zero. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param surfaces [Array] array of OpenStudio::Model::Surface objects + # @param type [String] floor, wall, or roof + # @return [nil] + def self.apply_adiabatic_construction(model, surfaces, type) + return if surfaces.empty? + + if type == 'wall' + mat_int_finish = Material.InteriorFinishMaterial(HPXML::InteriorFinishGypsumBoard, 0.5) + mat_ext_finish = Material.ExteriorFinishMaterial(HPXML::SidingTypeWood) + apply_wood_stud_wall(model, surfaces, 'AdiabaticWallConstruction', + 0, 1, 3.5, true, 0.1, mat_int_finish, 0, 99, mat_ext_finish, false, + Material.AirFilmVertical, Material.AirFilmVertical, nil) + elsif type == 'floor' + apply_wood_frame_floor_ceiling(model, surfaces, 'AdiabaticFloorConstruction', false, + 0, 1, 0.07, 5.5, 0.75, 99, Material.CoveringBare, false, + Material.AirFilmFloorReduced, Material.AirFilmFloorReduced, nil) + elsif type == 'roof' + apply_open_cavity_roof(model, surfaces, 'AdiabaticRoofConstruction', + 0, 1, 7.25, 0.07, 7.25, 0.75, 99, + Material.RoofMaterial(HPXML::RoofTypeAsphaltShingles), + false, Material.AirFilmOutside, + Material.AirFilmRoof(Geometry.get_roof_pitch(surfaces)), nil) + end + end + # TODO # # @param assembly_r [TODO] TODO diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index 7ccdcff353..5d743a35ae 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -658,7 +658,7 @@ def self.calculate_zone_volume(hpxml_bldg:, # TODO # # @param location [String] the general HPXML location - # @return [TODO] TODO + # @return [Hash] TODO def self.get_temperature_scheduled_space_values(location:) if location == HPXML::LocationOtherHeatedSpace # Average of indoor/outdoor temperatures with minimum of heating setpoint @@ -715,6 +715,258 @@ def self.get_temperature_scheduled_space_values(location:) fail "Unhandled location: #{location}." end + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object + # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface + # @return [nil] + def self.set_surface_interior(model, spaces, surface, hpxml_surface, hpxml_bldg) + interior_adjacent_to = hpxml_surface.interior_adjacent_to + if HPXML::conditioned_below_grade_locations.include? interior_adjacent_to + surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, hpxml_bldg)) + else + surface.setSpace(create_or_get_space(model, spaces, interior_adjacent_to, hpxml_bldg)) + end + end + + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object + # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface + # @return [nil] + def self.set_surface_exterior(model, spaces, surface, hpxml_surface, hpxml_bldg) + exterior_adjacent_to = hpxml_surface.exterior_adjacent_to + is_adiabatic = hpxml_surface.is_adiabatic + if [HPXML::LocationOutside, HPXML::LocationManufacturedHomeUnderBelly].include? exterior_adjacent_to + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) + elsif exterior_adjacent_to == HPXML::LocationGround + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionFoundation) + elsif is_adiabatic + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) + elsif [HPXML::LocationOtherHeatedSpace, HPXML::LocationOtherMultifamilyBufferSpace, + HPXML::LocationOtherNonFreezingSpace, HPXML::LocationOtherHousingUnit].include? exterior_adjacent_to + set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spaces) + elsif HPXML::conditioned_below_grade_locations.include? exterior_adjacent_to + adjacent_surface = surface.createAdjacentSurface(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, hpxml_bldg)).get + adjacent_surface.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get) + else + adjacent_surface = surface.createAdjacentSurface(create_or_get_space(model, spaces, exterior_adjacent_to, hpxml_bldg)).get + adjacent_surface.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get) + end + end + + # Set its parent surface outside boundary condition, which will be also applied to subsurfaces through OS + # The parent surface is entirely comprised of the subsurface. + # + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_surface [HPXML::Wall or HPXML::Roof or HPXML::RimJoist or HPXML::FoundationWall or HPXML::Slab] any HPXML surface + # @return [nil] + def self.set_subsurface_exterior(surface, spaces, model, hpxml_surface, hpxml_bldg) + # Subsurface on foundation wall, set it to be adjacent to outdoors + if hpxml_surface.exterior_adjacent_to == HPXML::LocationGround + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) + else + set_surface_exterior(model, spaces, surface, hpxml_surface, hpxml_bldg) + end + end + + # TODO + # + # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object + # @param exterior_adjacent_to [String] Exterior adjacent to location (HPXML::LocationXXX) + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [nil] + def self.set_surface_otherside_coefficients(surface, exterior_adjacent_to, model, spaces) + otherside_coeffs = nil + model.getSurfacePropertyOtherSideCoefficientss.each do |c| + next unless c.name.to_s == exterior_adjacent_to + + otherside_coeffs = c + end + if otherside_coeffs.nil? + # Create E+ other side coefficient object + otherside_coeffs = OpenStudio::Model::SurfacePropertyOtherSideCoefficients.new(model) + otherside_coeffs.setName(exterior_adjacent_to) + otherside_coeffs.setCombinedConvectiveRadiativeFilmCoefficient(UnitConversions.convert(1.0 / Material.AirFilmVertical.rvalue, 'Btu/(hr*ft^2*F)', 'W/(m^2*K)')) + # Schedule of space temperature, can be shared with water heater/ducts + sch = get_space_temperature_schedule(model, exterior_adjacent_to, spaces) + otherside_coeffs.setConstantTemperatureSchedule(sch) + end + surface.setSurfacePropertyOtherSideCoefficients(otherside_coeffs) + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + end + + # Create outside boundary schedules to be actuated by EMS, + # can be shared by any surface, duct adjacent to / located in those spaces. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param location [String] the location of interest (HPXML::LocationXXX) + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [OpenStudio::Model::ScheduleConstant] OpenStudio ScheduleConstant object + def self.get_space_temperature_schedule(model, location, spaces) + # return if already exists + model.getScheduleConstants.each do |sch| + next unless sch.name.to_s == location + + return sch + end + + sch = OpenStudio::Model::ScheduleConstant.new(model) + sch.setName(location) + sch.additionalProperties.setFeature('ObjectType', location) + + space_values = get_temperature_scheduled_space_values(location: location) + + htg_weekday_setpoints, htg_weekend_setpoints = HVAC.get_default_heating_setpoint(HPXML::HVACControlTypeManual, @eri_version) + if htg_weekday_setpoints.split(', ').uniq.size == 1 && htg_weekend_setpoints.split(', ').uniq.size == 1 && htg_weekday_setpoints.split(', ').uniq == htg_weekend_setpoints.split(', ').uniq + default_htg_sp = htg_weekend_setpoints.split(', ').uniq[0].to_f # F + else + fail 'Unexpected heating setpoints.' + end + + clg_weekday_setpoints, clg_weekend_setpoints = HVAC.get_default_cooling_setpoint(HPXML::HVACControlTypeManual, @eri_version) + if clg_weekday_setpoints.split(', ').uniq.size == 1 && clg_weekend_setpoints.split(', ').uniq.size == 1 && clg_weekday_setpoints.split(', ').uniq == clg_weekend_setpoints.split(', ').uniq + default_clg_sp = clg_weekend_setpoints.split(', ').uniq[0].to_f # F + else + fail 'Unexpected cooling setpoints.' + end + + if location == HPXML::LocationOtherHeatedSpace + if spaces[HPXML::LocationConditionedSpace].thermalZone.get.thermostatSetpointDualSetpoint.is_initialized + # Create a sensor to get dynamic heating setpoint + htg_sch = spaces[HPXML::LocationConditionedSpace].thermalZone.get.thermostatSetpointDualSetpoint.get.heatingSetpointTemperatureSchedule.get + sensor_htg_spt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + sensor_htg_spt.setName('htg_spt') + sensor_htg_spt.setKeyName(htg_sch.name.to_s) + space_values[:temp_min] = sensor_htg_spt.name.to_s + else + # No HVAC system; use the defaulted heating setpoint. + space_values[:temp_min] = default_htg_sp # F + end + end + + # Schedule type limits compatible + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setUnitType('Temperature') + sch.setScheduleTypeLimits(schedule_type_limits) + + # Sensors + if space_values[:indoor_weight] > 0 + if not spaces[HPXML::LocationConditionedSpace].thermalZone.get.thermostatSetpointDualSetpoint.is_initialized + # No HVAC system; use the average of defaulted heating/cooling setpoints. + sensor_ia = UnitConversions.convert((default_htg_sp + default_clg_sp) / 2.0, 'F', 'C') + else + sensor_ia = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Temperature') + sensor_ia.setName('cond_zone_temp') + sensor_ia.setKeyName(spaces[HPXML::LocationConditionedSpace].thermalZone.get.name.to_s) + sensor_ia = sensor_ia.name + end + end + + if space_values[:outdoor_weight] > 0 + sensor_oa = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Drybulb Temperature') + sensor_oa.setName('oa_temp') + end + + if space_values[:ground_weight] > 0 + sensor_gnd = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Surface Ground Temperature') + sensor_gnd.setName('ground_temp') + end + + actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(sch, *EPlus::EMSActuatorScheduleConstantValue) + actuator.setName("#{location.gsub(' ', '_').gsub('-', '_')}_temp_sch") + + # EMS to actuate schedule + program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + program.setName("#{location.gsub('-', '_')} Temperature Program") + program.addLine("Set #{actuator.name} = 0.0") + if not sensor_ia.nil? + program.addLine("Set #{actuator.name} = #{actuator.name} + (#{sensor_ia} * #{space_values[:indoor_weight]})") + end + if not sensor_oa.nil? + program.addLine("Set #{actuator.name} = #{actuator.name} + (#{sensor_oa.name} * #{space_values[:outdoor_weight]})") + end + if not sensor_gnd.nil? + program.addLine("Set #{actuator.name} = #{actuator.name} + (#{sensor_gnd.name} * #{space_values[:ground_weight]})") + end + if not space_values[:temp_min].nil? + if space_values[:temp_min].is_a? String + min_temp_c = space_values[:temp_min] + else + min_temp_c = UnitConversions.convert(space_values[:temp_min], 'F', 'C') + end + program.addLine("If #{actuator.name} < #{min_temp_c}") + program.addLine("Set #{actuator.name} = #{min_temp_c}") + program.addLine('EndIf') + end + + program_cm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + program_cm.setName("#{program.name} calling manager") + program_cm.setCallingPoint('EndOfSystemTimestepAfterHVACReporting') + program_cm.addProgram(program) + + return sch + end + + # Returns an OS:Space, or temperature OS:Schedule for a MF space, or nil if outside + # Should be called when the object's energy use is sensitive to ambient temperature + # (e.g., water heaters, ducts, and refrigerators). + # + # @param location [String] the location of interest (HPXML::LocationXXX) + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [OpenStudio::Model::Space or OpenStudio::Model::ScheduleConstant] OpenStudio Space or Schedule object + def self.get_space_or_schedule_from_location(location, model, spaces) + return if [HPXML::LocationOtherExterior, + HPXML::LocationOutside, + HPXML::LocationRoofDeck].include? location + + sch = nil + space = nil + if [HPXML::LocationOtherHeatedSpace, + HPXML::LocationOtherHousingUnit, + HPXML::LocationOtherMultifamilyBufferSpace, + HPXML::LocationOtherNonFreezingSpace, + HPXML::LocationExteriorWall, + HPXML::LocationUnderSlab].include? location + # if located in spaces where we don't model a thermal zone, create and return temperature schedule + sch = get_space_temperature_schedule(model, location, spaces) + else + space = get_space_from_location(location, spaces) + end + + return space, sch + end + + # Returns an OS:Space, or nil if a MF space or outside + # Should be called when the object's energy use is NOT sensitive to ambient temperature + # (e.g., appliances). + # + # @param location [String] the location of interest (HPXML::LocationXXX) + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [OpenStudio::Model::Space] OpenStudio Space object + def self.get_space_from_location(location, spaces) + return if [HPXML::LocationOutside, + HPXML::LocationOtherHeatedSpace, + HPXML::LocationOtherHousingUnit, + HPXML::LocationOtherMultifamilyBufferSpace, + HPXML::LocationOtherNonFreezingSpace].include? location + + if HPXML::conditioned_locations.include? location + location = HPXML::LocationConditionedSpace + end + + return spaces[location] + end + # Calculates space heights as the max z coordinate minus the min z coordinate. # # @param spaces [Array] array of OpenStudio::Model::Space objects From f7858574b57d922ffdafd2768ef7534b8269091b Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 3 Sep 2024 14:30:29 -0700 Subject: [PATCH 06/16] Relocate airflow related create_ducts method. --- HPXMLtoOpenStudio/measure.rb | 89 +------------------------- HPXMLtoOpenStudio/measure.xml | 8 +-- HPXMLtoOpenStudio/resources/airflow.rb | 85 ++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 91 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 9ea510b28e..d4f47838ed 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -2188,7 +2188,7 @@ def add_airflow(runner, model, weather, spaces, airloop_map) @hpxml_bldg.hvac_distributions.each do |hvac_distribution| next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir - air_ducts = create_ducts(model, hvac_distribution, spaces) + air_ducts = Airflow.create_ducts(model, hvac_distribution, spaces) next if air_ducts.empty? # Connect AirLoopHVACs to ducts @@ -2203,7 +2203,7 @@ def add_airflow(runner, model, weather, spaces, airloop_map) elsif duct_systems[air_ducts] != object # Multiple air loops associated with this duct system, treat # as separate duct systems. - air_ducts2 = create_ducts(model, hvac_distribution, spaces) + air_ducts2 = Airflow.create_ducts(model, hvac_distribution, spaces) duct_systems[air_ducts2] = object added_ducts = true end @@ -2269,91 +2269,6 @@ def add_airflow(runner, model, weather, spaces, airloop_map) @hpxml_header.unavailable_periods, hvac_availability_sensor) end - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hvac_distribution [HPXML::HVACDistribution] HPXML HVAC Distribution object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [Array] list of initialized Duct class objects from the airflow resource file - def create_ducts(model, hvac_distribution, spaces) - air_ducts = [] - - # Duct leakage (supply/return => [value, units]) - leakage_to_outside = { HPXML::DuctTypeSupply => [0.0, nil], - HPXML::DuctTypeReturn => [0.0, nil] } - hvac_distribution.duct_leakage_measurements.each do |duct_leakage_measurement| - next unless [HPXML::UnitsCFM25, HPXML::UnitsCFM50, HPXML::UnitsPercent].include?(duct_leakage_measurement.duct_leakage_units) && (duct_leakage_measurement.duct_leakage_total_or_to_outside == 'to outside') - next if duct_leakage_measurement.duct_type.nil? - - leakage_to_outside[duct_leakage_measurement.duct_type] = [duct_leakage_measurement.duct_leakage_value, duct_leakage_measurement.duct_leakage_units] - end - - # Duct location, R-value, Area - total_unconditioned_duct_area = { HPXML::DuctTypeSupply => 0.0, - HPXML::DuctTypeReturn => 0.0 } - hvac_distribution.ducts.each do |ducts| - next if HPXML::conditioned_locations_this_unit.include? ducts.duct_location - next if ducts.duct_type.nil? - - # Calculate total duct area in unconditioned spaces - total_unconditioned_duct_area[ducts.duct_type] += ducts.duct_surface_area * ducts.duct_surface_area_multiplier - end - - # Create duct objects - hvac_distribution.ducts.each do |ducts| - next if HPXML::conditioned_locations_this_unit.include? ducts.duct_location - next if ducts.duct_type.nil? - next if total_unconditioned_duct_area[ducts.duct_type] <= 0 - - duct_loc_space, duct_loc_schedule = Geometry.get_space_or_schedule_from_location(ducts.duct_location, model, spaces) - - # Apportion leakage to individual ducts by surface area - duct_leakage_value = leakage_to_outside[ducts.duct_type][0] * ducts.duct_surface_area * ducts.duct_surface_area_multiplier / total_unconditioned_duct_area[ducts.duct_type] - duct_leakage_units = leakage_to_outside[ducts.duct_type][1] - - duct_leakage_frac = nil - if duct_leakage_units == HPXML::UnitsCFM25 - duct_leakage_cfm25 = duct_leakage_value - elsif duct_leakage_units == HPXML::UnitsCFM50 - duct_leakage_cfm50 = duct_leakage_value - elsif duct_leakage_units == HPXML::UnitsPercent - duct_leakage_frac = duct_leakage_value - else - fail "#{ducts.duct_type.capitalize} ducts exist but leakage was not specified for distribution system '#{hvac_distribution.id}'." - end - - air_ducts << Duct.new(ducts.duct_type, duct_loc_space, duct_loc_schedule, duct_leakage_frac, duct_leakage_cfm25, duct_leakage_cfm50, - ducts.duct_surface_area * ducts.duct_surface_area_multiplier, ducts.duct_effective_r_value, ducts.duct_buried_insulation_level) - end - - # If all ducts are in conditioned space, model leakage as going to outside - [HPXML::DuctTypeSupply, HPXML::DuctTypeReturn].each do |duct_side| - next unless (leakage_to_outside[duct_side][0] > 0) && (total_unconditioned_duct_area[duct_side] == 0) - - duct_area = 0.0 - duct_effective_r_value = 99 # arbitrary - duct_loc_space = nil # outside - duct_loc_schedule = nil # outside - duct_leakage_value = leakage_to_outside[duct_side][0] - duct_leakage_units = leakage_to_outside[duct_side][1] - - if duct_leakage_units == HPXML::UnitsCFM25 - duct_leakage_cfm25 = duct_leakage_value - elsif duct_leakage_units == HPXML::UnitsCFM50 - duct_leakage_cfm50 = duct_leakage_value - elsif duct_leakage_units == HPXML::UnitsPercent - duct_leakage_frac = duct_leakage_value - else - fail "#{duct_side.capitalize} ducts exist but leakage was not specified for distribution system '#{hvac_distribution.id}'." - end - - air_ducts << Duct.new(duct_side, duct_loc_space, duct_loc_schedule, duct_leakage_frac, duct_leakage_cfm25, duct_leakage_cfm50, duct_area, - duct_effective_r_value, HPXML::DuctBuriedInsulationNone) - end - - return air_ducts - end - # Adds any HPXML Photovoltaics to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 4a4e56e542..33ec736bee 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 38d82c1e-8b24-47f6-a30e-18cba2e6cd1d - 2024-09-03T21:24:46Z + edbd1ed6-9707-4eee-8657-d9b35b85911b + 2024-09-03T21:29:51Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,13 +183,13 @@ measure.rb rb script - EFF8B910 + 73D0E14B airflow.rb rb resource - 6D51E306 + 01125081 battery.rb diff --git a/HPXMLtoOpenStudio/resources/airflow.rb b/HPXMLtoOpenStudio/resources/airflow.rb index 98bb6fbf5a..014fd5bb49 100644 --- a/HPXMLtoOpenStudio/resources/airflow.rb +++ b/HPXMLtoOpenStudio/resources/airflow.rb @@ -2755,6 +2755,91 @@ def self.get_mech_vent_qfan_cfm(q_tot, q_inf, is_balanced, frac_imbal, a_ext, bl return [q_fan, 0.0].max end + + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hvac_distribution [HPXML::HVACDistribution] HPXML HVAC Distribution object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @return [Array] list of initialized Duct class objects from the airflow resource file + def self.create_ducts(model, hvac_distribution, spaces) + air_ducts = [] + + # Duct leakage (supply/return => [value, units]) + leakage_to_outside = { HPXML::DuctTypeSupply => [0.0, nil], + HPXML::DuctTypeReturn => [0.0, nil] } + hvac_distribution.duct_leakage_measurements.each do |duct_leakage_measurement| + next unless [HPXML::UnitsCFM25, HPXML::UnitsCFM50, HPXML::UnitsPercent].include?(duct_leakage_measurement.duct_leakage_units) && (duct_leakage_measurement.duct_leakage_total_or_to_outside == 'to outside') + next if duct_leakage_measurement.duct_type.nil? + + leakage_to_outside[duct_leakage_measurement.duct_type] = [duct_leakage_measurement.duct_leakage_value, duct_leakage_measurement.duct_leakage_units] + end + + # Duct location, R-value, Area + total_unconditioned_duct_area = { HPXML::DuctTypeSupply => 0.0, + HPXML::DuctTypeReturn => 0.0 } + hvac_distribution.ducts.each do |ducts| + next if HPXML::conditioned_locations_this_unit.include? ducts.duct_location + next if ducts.duct_type.nil? + + # Calculate total duct area in unconditioned spaces + total_unconditioned_duct_area[ducts.duct_type] += ducts.duct_surface_area * ducts.duct_surface_area_multiplier + end + + # Create duct objects + hvac_distribution.ducts.each do |ducts| + next if HPXML::conditioned_locations_this_unit.include? ducts.duct_location + next if ducts.duct_type.nil? + next if total_unconditioned_duct_area[ducts.duct_type] <= 0 + + duct_loc_space, duct_loc_schedule = Geometry.get_space_or_schedule_from_location(ducts.duct_location, model, spaces) + + # Apportion leakage to individual ducts by surface area + duct_leakage_value = leakage_to_outside[ducts.duct_type][0] * ducts.duct_surface_area * ducts.duct_surface_area_multiplier / total_unconditioned_duct_area[ducts.duct_type] + duct_leakage_units = leakage_to_outside[ducts.duct_type][1] + + duct_leakage_frac = nil + if duct_leakage_units == HPXML::UnitsCFM25 + duct_leakage_cfm25 = duct_leakage_value + elsif duct_leakage_units == HPXML::UnitsCFM50 + duct_leakage_cfm50 = duct_leakage_value + elsif duct_leakage_units == HPXML::UnitsPercent + duct_leakage_frac = duct_leakage_value + else + fail "#{ducts.duct_type.capitalize} ducts exist but leakage was not specified for distribution system '#{hvac_distribution.id}'." + end + + air_ducts << Duct.new(ducts.duct_type, duct_loc_space, duct_loc_schedule, duct_leakage_frac, duct_leakage_cfm25, duct_leakage_cfm50, + ducts.duct_surface_area * ducts.duct_surface_area_multiplier, ducts.duct_effective_r_value, ducts.duct_buried_insulation_level) + end + + # If all ducts are in conditioned space, model leakage as going to outside + [HPXML::DuctTypeSupply, HPXML::DuctTypeReturn].each do |duct_side| + next unless (leakage_to_outside[duct_side][0] > 0) && (total_unconditioned_duct_area[duct_side] == 0) + + duct_area = 0.0 + duct_effective_r_value = 99 # arbitrary + duct_loc_space = nil # outside + duct_loc_schedule = nil # outside + duct_leakage_value = leakage_to_outside[duct_side][0] + duct_leakage_units = leakage_to_outside[duct_side][1] + + if duct_leakage_units == HPXML::UnitsCFM25 + duct_leakage_cfm25 = duct_leakage_value + elsif duct_leakage_units == HPXML::UnitsCFM50 + duct_leakage_cfm50 = duct_leakage_value + elsif duct_leakage_units == HPXML::UnitsPercent + duct_leakage_frac = duct_leakage_value + else + fail "#{duct_side.capitalize} ducts exist but leakage was not specified for distribution system '#{hvac_distribution.id}'." + end + + air_ducts << Duct.new(duct_side, duct_loc_space, duct_loc_schedule, duct_leakage_frac, duct_leakage_cfm25, duct_leakage_cfm50, duct_area, + duct_effective_r_value, HPXML::DuctBuriedInsulationNone) + end + + return air_ducts + end end # TODO From 30dc01a201a68290a972e000283e2c0d5e71c148 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 3 Sep 2024 16:55:48 -0700 Subject: [PATCH 07/16] Move translator methods to new internal_gains and model resource files. --- HPXMLtoOpenStudio/measure.rb | 225 ++---------------- HPXMLtoOpenStudio/measure.xml | 24 +- HPXMLtoOpenStudio/resources/geometry.rb | 150 +++--------- .../resources/hotwater_appliances.rb | 48 +--- HPXMLtoOpenStudio/resources/hvac.rb | 23 ++ HPXMLtoOpenStudio/resources/internal_gains.rb | 151 ++++++++++++ HPXMLtoOpenStudio/resources/model.rb | 195 +++++++++++++++ 7 files changed, 431 insertions(+), 385 deletions(-) create mode 100644 HPXMLtoOpenStudio/resources/internal_gains.rb create mode 100644 HPXMLtoOpenStudio/resources/model.rb diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index bc75d23ad9..472107b050 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -108,7 +108,7 @@ def run(model, runner, user_arguments) return false end - Geometry.tear_down_model(model: model, runner: runner) + Model.tear_down(model: model, runner: runner) Version.check_openstudio_version() @@ -228,7 +228,7 @@ def run(model, runner, user_arguments) # Merge unit models into final model if hpxml.buildings.size > 1 - add_unit_model_to_model(model, hpxml_osm_map) + Model.add_unit_model(model, hpxml_osm_map) end # Output @@ -238,7 +238,7 @@ def run(model, runner, user_arguments) add_component_loads_output(model, hpxml_osm_map, loads_data, season_day_nums) end add_total_airflows_output(model, hpxml_osm_map) - set_output_files(model) + add_output_files(model) add_additional_properties(model, hpxml, hpxml_osm_map, args[:hpxml_path], args[:building_id], hpxml_defaults_path) # Uncomment to debug EMS # add_ems_debug_output(model) @@ -261,179 +261,6 @@ def run(model, runner, user_arguments) return true end - # When there are multiple dwelling units, merge all unit models into a single model. - # First deal with unique objects; look for differences in values across unit models. - # Then make all unit models "unique" by shifting geometry and prefixing object names. - # Then bulk add all modified objects to the main OpenStudio Model object. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @return [nil] - def add_unit_model_to_model(model, hpxml_osm_map) - unique_objects = { 'OS:ConvergenceLimits' => 'ConvergenceLimits', - 'OS:Foundation:Kiva:Settings' => 'FoundationKivaSettings', - 'OS:OutputControl:Files' => 'OutputControlFiles', - 'OS:Output:Diagnostics' => 'OutputDiagnostics', - 'OS:Output:JSON' => 'OutputJSON', - 'OS:PerformancePrecisionTradeoffs' => 'PerformancePrecisionTradeoffs', - 'OS:RunPeriod' => 'RunPeriod', - 'OS:RunPeriodControl:DaylightSavingTime' => 'RunPeriodControlDaylightSavingTime', - 'OS:ShadowCalculation' => 'ShadowCalculation', - 'OS:SimulationControl' => 'SimulationControl', - 'OS:Site' => 'Site', - 'OS:Site:GroundTemperature:Deep' => 'SiteGroundTemperatureDeep', - 'OS:Site:GroundTemperature:Shallow' => 'SiteGroundTemperatureShallow', - 'OS:Site:WaterMainsTemperature' => 'SiteWaterMainsTemperature', - 'OS:SurfaceConvectionAlgorithm:Inside' => 'InsideSurfaceConvectionAlgorithm', - 'OS:SurfaceConvectionAlgorithm:Outside' => 'OutsideSurfaceConvectionAlgorithm', - 'OS:Timestep' => 'Timestep' } - - # Handle unique objects first: Grab one from the first model we find the - # object on (may not be the first unit). - unit_model_objects = [] - unique_handles_to_skip = [] - uuid_regex = /\{(.*?)\}/ - unique_objects.each do |idd_obj, osm_class| - first_model_object_by_type = nil - hpxml_osm_map.values.each do |unit_model| - next if unit_model.getObjectsByType(idd_obj.to_IddObjectType).empty? - - model_object = unit_model.send("get#{osm_class}") - - if first_model_object_by_type.nil? - # Retain object for model - unit_model_objects << model_object - first_model_object_by_type = model_object - if idd_obj == 'OS:Site:WaterMainsTemperature' # Handle referenced child object too - unit_model_objects << unit_model.getObjectsByName(model_object.temperatureSchedule.get.name.to_s)[0] - end - else - # Throw error if different values between this model_object and first_model_object_by_type - if model_object.to_s.gsub(uuid_regex, '') != first_model_object_by_type.to_s.gsub(uuid_regex, '') - fail "Unique object (#{idd_obj}) has different values across dwelling units." - end - - if idd_obj == 'OS:Site:WaterMainsTemperature' # Handle referenced child object too - if model_object.temperatureSchedule.get.to_s.gsub(uuid_regex, '') != first_model_object_by_type.temperatureSchedule.get.to_s.gsub(uuid_regex, '') - fail "Unique object (#{idd_obj}) has different values across dwelling units." - end - end - end - - unique_handles_to_skip << model_object.handle.to_s - if idd_obj == 'OS:Site:WaterMainsTemperature' # Handle referenced child object too - unique_handles_to_skip << model_object.temperatureSchedule.get.handle.to_s - end - end - end - - hpxml_osm_map.values.each_with_index do |unit_model, unit_number| - Geometry.shift_unit(unit_model, unit_number) - prefix_all_unit_model_objects(unit_model, unit_number) - - # Handle remaining (non-unique) objects now - unit_model.objects.each do |obj| - next if unit_number > 0 && obj.to_Building.is_initialized - next if unique_handles_to_skip.include? obj.handle.to_s - - unit_model_objects << obj - end - end - - model.addObjects(unit_model_objects, true) - end - - # Prefix all objects with name using unit number. - # - # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object (corresponding to one of multiple dwelling units) - # @param unit_number [Integer] index number corresponding to an HPXML Building object - # @return [nil] - def prefix_all_unit_model_objects(unit_model, unit_number) - # FUTURE: Create objects with unique names up front so we don't have to do this - - # EMS objects - ems_map = {} - - unit_model.getEnergyManagementSystemSensors.each do |sensor| - ems_map[sensor.name.to_s] = make_variable_name(sensor.name, unit_number) - sensor.setKeyName(make_variable_name(sensor.keyName, unit_number)) unless sensor.keyName.empty? || sensor.keyName.downcase == 'environment' - end - - unit_model.getEnergyManagementSystemActuators.each do |actuator| - ems_map[actuator.name.to_s] = make_variable_name(actuator.name, unit_number) - end - - unit_model.getEnergyManagementSystemInternalVariables.each do |internal_variable| - ems_map[internal_variable.name.to_s] = make_variable_name(internal_variable.name, unit_number) - internal_variable.setInternalDataIndexKeyName(make_variable_name(internal_variable.internalDataIndexKeyName, unit_number)) unless internal_variable.internalDataIndexKeyName.empty? - end - - unit_model.getEnergyManagementSystemGlobalVariables.each do |global_variable| - ems_map[global_variable.name.to_s] = make_variable_name(global_variable.name, unit_number) - end - - unit_model.getEnergyManagementSystemOutputVariables.each do |output_variable| - next if output_variable.emsVariableObject.is_initialized - - new_ems_variable_name = make_variable_name(output_variable.emsVariableName, unit_number) - ems_map[output_variable.emsVariableName.to_s] = new_ems_variable_name - output_variable.setEMSVariableName(new_ems_variable_name) - end - - unit_model.getEnergyManagementSystemSubroutines.each do |subroutine| - ems_map[subroutine.name.to_s] = make_variable_name(subroutine.name, unit_number) - end - - # variables in program lines don't get updated automatically - lhs_characters = [' ', ',', '(', ')', '+', '-', '*', '/', ';'] - rhs_characters = [''] + lhs_characters - (unit_model.getEnergyManagementSystemPrograms + unit_model.getEnergyManagementSystemSubroutines).each do |program| - new_lines = [] - program.lines.each do |line| - ems_map.each do |old_name, new_name| - next unless line.include?(old_name) - - # old_name between at least 1 character, with the exception of '' on left and ' ' on right - lhs_characters.each do |lhs| - next unless line.include?("#{lhs}#{old_name}") - - rhs_characters.each do |rhs| - next unless line.include?("#{lhs}#{old_name}#{rhs}") - next if lhs == '' && ['', ' '].include?(rhs) - - line.gsub!("#{lhs}#{old_name}#{rhs}", "#{lhs}#{new_name}#{rhs}") - end - end - end - new_lines << line - end - program.setLines(new_lines) - end - - # All model objects - unit_model.objects.each do |model_object| - next if model_object.name.nil? - - if unit_number == 0 - # OpenStudio is unhappy if these schedules are renamed - next if model_object.name.to_s == unit_model.alwaysOnContinuousSchedule.name.to_s - next if model_object.name.to_s == unit_model.alwaysOnDiscreteSchedule.name.to_s - next if model_object.name.to_s == unit_model.alwaysOffDiscreteSchedule.name.to_s - end - - model_object.setName(make_variable_name(model_object.name, unit_number)) - end - end - - # Create a new OpenStudio object name by prefixing the old with "unit" plus the unit number. - # - # @param obj_name [String] the OpenStudio object name - # @param unit_number [Integer] index number corresponding to an HPXML Building object - # @return [String] the new OpenStudio object name with unique unit prefix - def make_variable_name(obj_name, unit_number) - return "unit#{unit_number + 1}_#{obj_name}".gsub(' ', '_').gsub('-', '_') - end - # Creates a full OpenStudio model that represents the given HPXML individual dwelling by # adding OpenStudio objects to the empty OpenStudio model for each component of the building. # @@ -483,6 +310,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug # Conditioned space/zone add_conditioned_space(model, spaces) add_setpoints(runner, model, weather, spaces) + add_internal_gains(runner, model, spaces) # Geometry/Envelope add_roofs(runner, model, spaces) @@ -495,7 +323,6 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug add_skylights(model, spaces) add_conditioned_floor_area(model, spaces) add_thermal_mass(model, spaces) - add_num_occupants(model, runner, spaces) add_geometry_other(model, spaces) # HVAC @@ -646,7 +473,7 @@ def add_conditioned_space(model, spaces) # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [nil] - def add_num_occupants(model, runner, spaces) + def add_internal_gains(runner, model, spaces) # Occupants if @hpxml_bldg.building_occupancy.number_of_residents.nil? # Asset calculation num_occ = Geometry.get_occupancy_default_num(nbeds: @nbeds) @@ -654,8 +481,8 @@ def add_num_occupants(model, runner, spaces) num_occ = @hpxml_bldg.building_occupancy.number_of_residents end - Geometry.apply_occupants(model, runner, @hpxml_bldg, num_occ, spaces[HPXML::LocationConditionedSpace], - @schedules_file, @hpxml_header.unavailable_periods) + InternalGains.apply(model, runner, @hpxml_bldg, num_occ, spaces[HPXML::LocationConditionedSpace], + @schedules_file, @hpxml_header.unavailable_periods, @apply_ashrae140_assumptions) end # Adds any HPXML Roofs to the OpenStudio model. @@ -1670,7 +1497,7 @@ def add_doors(model, spaces) Constructions.apply_adiabatic_construction(model, surfaces, 'wall') end - # TODO + # Adds other geometry properties like space/zone volumes and re-positioned surface coordinates. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects @@ -1763,8 +1590,7 @@ def add_hot_water_and_appliances(runner, model, weather, spaces) # Hot water fixtures and appliances HotWaterAndAppliances.apply(model, runner, @hpxml_header, @hpxml_bldg, weather, spaces, hot_water_distribution, solar_thermal_system, @eri_version, @schedules_file, plantloop_map, - @hpxml_header.unavailable_periods, @hpxml_bldg.building_construction.number_of_units, - @apply_ashrae140_assumptions) + @hpxml_header.unavailable_periods, @hpxml_bldg.building_construction.number_of_units) if (not solar_thermal_system.nil?) && (not solar_thermal_system.collector_area.nil?) # Detailed solar water heater loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) @@ -1794,7 +1620,7 @@ def add_cooling_system(model, runner, weather, spaces, airloop_map) cooling_system = hvac_system[:cooling] heating_system = hvac_system[:heating] - check_distribution_system(cooling_system.distribution_system, cooling_system.cooling_system_type) + HVAC.check_distribution_system(cooling_system.distribution_system, cooling_system.cooling_system_type) # Calculate cooling sequential load fractions sequential_cool_load_fracs = HVAC.calc_sequential_load_fractions(cooling_system.fraction_cool_load_served.to_f, @remaining_cool_load_frac, @cooling_days) @@ -1850,7 +1676,7 @@ def add_heating_system(runner, model, weather, spaces, airloop_map) cooling_system = hvac_system[:cooling] heating_system = hvac_system[:heating] - check_distribution_system(heating_system.distribution_system, heating_system.heating_system_type) + HVAC.check_distribution_system(heating_system.distribution_system, heating_system.heating_system_type) if (heating_system.heating_system_type == HPXML::HVACTypeFurnace) && (not cooling_system.nil?) next # Already processed combined AC+furnace @@ -1923,7 +1749,7 @@ def add_heat_pump(runner, model, weather, spaces, airloop_map) heat_pump = hvac_system[:cooling] - check_distribution_system(heat_pump.distribution_system, heat_pump.heat_pump_type) + HVAC.check_distribution_system(heat_pump.distribution_system, heat_pump.heat_pump_type) # Calculate heating sequential load fractions sequential_heat_load_fracs = HVAC.calc_sequential_load_fractions(heat_pump.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) @@ -2060,29 +1886,6 @@ def add_dehumidifiers(runner, model, spaces) @hpxml_bldg.building_construction.number_of_units) end - # Check provided HVAC system and distribution types against what is allowed. - # - # @param hvac_distribution [HPXML::HVACDistribution] HPXML HVAC Distribution object - # @param system_type [String] the HVAC system type of interest - # @return [nil] - def check_distribution_system(hvac_distribution, system_type) - return if hvac_distribution.nil? - - hvac_distribution_type_map = { HPXML::HVACTypeFurnace => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeBoiler => [HPXML::HVACDistributionTypeHydronic, HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeCentralAirConditioner => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeEvaporativeCooler => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeMiniSplitAirConditioner => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeHeatPumpAirToAir => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeHeatPumpMiniSplit => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeHeatPumpGroundToAir => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], - HPXML::HVACTypeHeatPumpWaterLoopToAir => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE] } - - if not hvac_distribution_type_map[system_type].include? hvac_distribution.distribution_system_type - fail "Incorrect HVAC distribution system type for HVAC type: '#{system_type}'. Should be one of: #{hvac_distribution_type_map[system_type]}" - end - end - # Adds any HPXML Plug Loads to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings @@ -2302,7 +2105,7 @@ def add_batteries(runner, model, spaces) end end - # TODO + # Store the HPXML Building object unit number for use in reporting measure. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param unit_number [Integer] index number corresponding to an HPXML Building object @@ -3086,7 +2889,7 @@ def add_total_airflows_output(model, hpxml_osm_map) # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @return [nil] - def set_output_files(model) + def add_output_files(model) oj = model.getOutputJSON oj.setOptionType('TimeSeriesAndTabular') oj.setOutputJSON(@debug) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 833eacd70f..e7977b110a 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 91250812-e128-411f-b6d9-1823d155458e - 2024-09-03T22:18:26Z + 1cae9e5f-f16d-42a1-9c30-492d9daa9dcd + 2024-09-03T23:54:58Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - E584F2E5 + 25D005AC airflow.rb @@ -345,13 +345,13 @@ geometry.rb rb resource - 08E9B56C + 3CF58696 hotwater_appliances.rb rb resource - 36092007 + B8578044 hpxml.rb @@ -393,7 +393,7 @@ hvac.rb rb resource - 7EC4C04D + 93926A7F hvac_sizing.rb @@ -401,6 +401,12 @@ resource 9B75637E + + internal_gains.rb + rb + resource + 808BF9F9 + lighting.rb rb @@ -443,6 +449,12 @@ resource D04D654F + + model.rb + rb + resource + 040BA07A + output.rb rb diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index 5d743a35ae..ea56f7b86b 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -2,23 +2,6 @@ # Collection of methods to get, add, assign, create, etc. geometry-related OpenStudio objects. module Geometry - # Tear down the existing model if it exists. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @return [nil] - def self.tear_down_model(model:, - runner:) - handles = OpenStudio::UUIDVector.new - model.objects.each do |obj| - handles << obj.handle - end - if !handles.empty? - runner.registerWarning('The model contains existing objects and is being reset.') - model.removeObjects(handles) - end - end - # Get the largest z difference for a surface. # # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object @@ -302,12 +285,13 @@ def self.create_floor_vertices(length:, return transformation * vertices end - # TODO + # Set calculated zone volumes for HPXML locations on OpenStudio Thermal Zone and Space objects. + # TODO why? for reporting? # - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param apply_ashrae140_assumptions [TODO] TODO - # @return [TODO] TODO + # @param apply_ashrae140_assumptions [Boolean] TODO + # @return [nil] def self.set_zone_volumes(spaces:, hpxml_bldg:, apply_ashrae140_assumptions:) @@ -486,66 +470,35 @@ def self.explode_surfaces(model:, end end - # Create an OpenStudio People object using number of occupants and people/activity schedules. + # Shift units so they aren't right on top and shade each other. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param num_occ [Double] Number of occupants in the dwelling unit - # @param space [OpenStudio::Model::Space] an OpenStudio::Model::Space object - # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies + # @param unit_number [Integer] index number corresponding to an HPXML Building object # @return [nil] - def self.apply_occupants(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods) - return if num_occ <= 0 + def self.shift_surfaces(model, unit_number) + y_shift = 200.0 * unit_number # meters - occ_gain, _hrs_per_day, sens_frac, _lat_frac = get_occupancy_default_values() - activity_per_person = UnitConversions.convert(occ_gain, 'Btu/hr', 'W') + # shift the unit so it's not right on top of the previous one + model.getSpaces.sort.each do |space| + space.setYOrigin(y_shift) + end - # Hard-coded convective, radiative, latent, and lost fractions - occ_sens = sens_frac - occ_rad = 0.558 * occ_sens + # shift shading surfaces + m = OpenStudio::Matrix.new(4, 4, 0) + m[0, 0] = 1 + m[1, 1] = 1 + m[2, 2] = 1 + m[3, 3] = 1 + m[1, 3] = y_shift + t = OpenStudio::Transformation.new(m) - # Create schedule - people_sch = nil - people_col_name = SchedulesFile::Columns[:Occupants].name - if not schedules_file.nil? - people_sch = schedules_file.create_schedule_file(model, col_name: people_col_name) - end - if people_sch.nil? - people_unavailable_periods = Schedule.get_unavailable_periods(runner, people_col_name, unavailable_periods) - weekday_sch = hpxml_bldg.building_occupancy.weekday_fractions.split(',').map(&:to_f) - weekday_sch = weekday_sch.map { |v| v / weekday_sch.max }.join(',') - weekend_sch = hpxml_bldg.building_occupancy.weekend_fractions.split(',').map(&:to_f) - weekend_sch = weekend_sch.map { |v| v / weekend_sch.max }.join(',') - monthly_sch = hpxml_bldg.building_occupancy.monthly_multipliers - people_sch = MonthWeekdayWeekendSchedule.new(model, Constants::ObjectTypeOccupants + ' schedule', weekday_sch, weekend_sch, monthly_sch, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: people_unavailable_periods) - people_sch = people_sch.schedule - else - runner.registerWarning("Both '#{people_col_name}' schedule file and weekday fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.weekday_fractions.nil? - runner.registerWarning("Both '#{people_col_name}' schedule file and weekend fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.weekend_fractions.nil? - runner.registerWarning("Both '#{people_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.monthly_multipliers.nil? - end + model.getShadingSurfaceGroups.each do |shading_surface_group| + next if shading_surface_group.space.is_initialized # already got shifted - # Create schedule - activity_sch = OpenStudio::Model::ScheduleConstant.new(model) - activity_sch.setValue(activity_per_person) - activity_sch.setName(Constants::ObjectTypeOccupants + ' activity schedule') - - # Add people definition for the occ - occ_def = OpenStudio::Model::PeopleDefinition.new(model) - occ = OpenStudio::Model::People.new(occ_def) - occ.setName(Constants::ObjectTypeOccupants) - occ.setSpace(space) - occ_def.setName(Constants::ObjectTypeOccupants) - occ_def.setNumberofPeople(num_occ) - occ_def.setFractionRadiant(occ_rad) - occ_def.setSensibleHeatFraction(occ_sens) - occ_def.setMeanRadiantTemperatureCalculationType('ZoneAveraged') - occ_def.setCarbonDioxideGenerationRate(0) - occ_def.setEnableASHRAE55ComfortWarnings(false) - occ.setActivityLevelSchedule(activity_sch) - occ.setNumberofPeopleSchedule(people_sch) + shading_surface_group.shadingSurfaces.each do |shading_surface| + shading_surface.setVertices(t * shading_surface.vertices) + end + end end # TODO @@ -594,7 +547,7 @@ def self.get_surface_transformation(offset:, # @param length [TODO] TODO # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param walls_top [TODO] TODO - # @return [TODO] TODO + # @return [nil] def self.add_neighbor_shading(model:, length:, hpxml_bldg:, @@ -627,7 +580,7 @@ def self.add_neighbor_shading(model:, # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param location [String] the location of interest (HPXML::LocationXXX) - # @return [TODO] TODO + # @return [Double] TODO def self.calculate_zone_volume(hpxml_bldg:, location:) if [HPXML::LocationBasementUnconditioned, @@ -998,20 +951,6 @@ def self.get_surface_length(surface:) return yrange end - # Table 4.2.2(3). Internal Gains for Reference Homes - # - # @return [Array] TODO - def self.get_occupancy_default_values() - hrs_per_day = 16.5 # hrs/day - sens_gains = 3716.0 # Btu/person/day - lat_gains = 2884.0 # Btu/person/day - tot_gains = sens_gains + lat_gains - heat_gain = tot_gains / hrs_per_day # Btu/person/hr - sens_frac = sens_gains / tot_gains - lat_frac = lat_gains / tot_gains - return heat_gain, hrs_per_day, sens_frac, lat_frac - end - # Calculates the minimum buffer distance that the parent surface # needs relative to the subsurface in order to prevent E+ warnings # about "Very small surface area". @@ -1025,37 +964,6 @@ def self.calculate_subsurface_parent_buffer(length:, return 0.5 * (((length + width)**2 + 4.0 * min_surface_area)**0.5 - length - width) end - # Shift units so they aren't right on top and shade each other. - # - # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object (corresponding to one of multiple dwelling units) - # @param unit_number [Integer] index number corresponding to an HPXML Building object - # @return [nil] - def self.shift_unit(unit_model, unit_number) - y_shift = 200.0 * unit_number # meters - - # shift the unit so it's not right on top of the previous one - unit_model.getSpaces.sort.each do |space| - space.setYOrigin(y_shift) - end - - # shift shading surfaces - m = OpenStudio::Matrix.new(4, 4, 0) - m[0, 0] = 1 - m[1, 1] = 1 - m[2, 2] = 1 - m[3, 3] = 1 - m[1, 3] = y_shift - t = OpenStudio::Transformation.new(m) - - unit_model.getShadingSurfaceGroups.each do |shading_surface_group| - next if shading_surface_group.space.is_initialized # already got shifted - - shading_surface_group.shadingSurfaces.each do |shading_surface| - shading_surface.setVertices(t * shading_surface.vertices) - end - end - end - # For a provided HPXML Location, create an OpenStudio Space and Thermal Zone if the provided spaces hash doesn't already contain the OpenStudio Space. # Otherwise, return the already-created OpenStudio Space for the provided HPXML Location. # diff --git a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb index 4bc17bb3e3..43f7f91119 100644 --- a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb +++ b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb @@ -17,11 +17,10 @@ module HotWaterAndAppliances # @param plantloop_map [TODO] TODO # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @param unit_multiplier [Integer] Number of similar dwelling units - # @param apply_ashrae140_assumptions [TODO] TODO # @return [TODO] TODO def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_water_distribution, solar_thermal_system, eri_version, schedules_file, plantloop_map, - unavailable_periods, unit_multiplier, apply_ashrae140_assumptions) + unavailable_periods, unit_multiplier) @runner = runner cfa = hpxml_bldg.building_construction.conditioned_floor_area @@ -29,7 +28,6 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat has_uncond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementUnconditioned) has_cond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementConditioned) fixtures_usage_multiplier = hpxml_bldg.water_heating.water_fixtures_usage_multiplier - general_water_use_usage_multiplier = hpxml_bldg.building_occupancy.general_water_use_usage_multiplier conditioned_space = spaces[HPXML::LocationConditionedSpace] nbeds = hpxml_bldg.building_construction.number_of_bedrooms nbeds_eq = hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms @@ -458,38 +456,6 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat end add_water_use_equipment(model, dw_obj_name, dw_peak_flow * gpd_frac * non_solar_fraction, water_dw_schedule, water_use_connections[water_heating_system.id], unit_multiplier) end - - if not apply_ashrae140_assumptions - # General water use internal gains - # Floor mopping, shower evaporation, water films on showers, tubs & sinks surfaces, plant watering, etc. - water_sens_btu, water_lat_btu = get_water_gains_sens_lat(nbeds_eq, general_water_use_usage_multiplier) - - # Create schedule - water_schedule = nil - water_col_name = SchedulesFile::Columns[:GeneralWaterUse].name - water_obj_name = Constants::ObjectTypeGeneralWaterUse - if not schedules_file.nil? - water_design_level_sens = schedules_file.calc_design_level_from_daily_kwh(col_name: SchedulesFile::Columns[:GeneralWaterUse].name, daily_kwh: UnitConversions.convert(water_sens_btu, 'Btu', 'kWh') / 365.0) - water_design_level_lat = schedules_file.calc_design_level_from_daily_kwh(col_name: SchedulesFile::Columns[:GeneralWaterUse].name, daily_kwh: UnitConversions.convert(water_lat_btu, 'Btu', 'kWh') / 365.0) - water_schedule = schedules_file.create_schedule_file(model, col_name: water_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) - end - if water_schedule.nil? - water_unavailable_periods = Schedule.get_unavailable_periods(runner, water_col_name, unavailable_periods) - water_weekday_sch = hpxml_bldg.building_occupancy.general_water_use_weekday_fractions - water_weekend_sch = hpxml_bldg.building_occupancy.general_water_use_weekend_fractions - water_monthly_sch = hpxml_bldg.building_occupancy.general_water_use_monthly_multipliers - water_schedule_obj = MonthWeekdayWeekendSchedule.new(model, water_obj_name + ' schedule', water_weekday_sch, water_weekend_sch, water_monthly_sch, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: water_unavailable_periods) - water_design_level_sens = water_schedule_obj.calc_design_level_from_daily_kwh(UnitConversions.convert(water_sens_btu, 'Btu', 'kWh') / 365.0) - water_design_level_lat = water_schedule_obj.calc_design_level_from_daily_kwh(UnitConversions.convert(water_lat_btu, 'Btu', 'kWh') / 365.0) - water_schedule = water_schedule_obj.schedule - else - runner.registerWarning("Both '#{water_col_name}' schedule file and weekday fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_weekday_fractions.nil? - runner.registerWarning("Both '#{water_col_name}' schedule file and weekend fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_weekend_fractions.nil? - runner.registerWarning("Both '#{water_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_monthly_multipliers.nil? - end - add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseSensible, conditioned_space, water_design_level_sens, 1.0, 0.0, water_schedule, nil) - add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseLatent, conditioned_space, water_design_level_lat, 0.0, 1.0, water_schedule, nil) - end end # TODO @@ -1309,18 +1275,6 @@ def self.get_fixtures_gpd(eri_version, nbeds, frac_low_flow_fixtures, daily_mw_f end end - # TODO - # - # @param nbeds_eq [Integer] Number of bedrooms (or equivalent bedrooms, as adjusted by the number of occupants) in the dwelling unit - # @param general_water_use_usage_multiplier [TODO] TODO - # @return [TODO] TODO - def self.get_water_gains_sens_lat(nbeds_eq, general_water_use_usage_multiplier = 1.0) - # Table 4.2.2(3). Internal Gains for Reference Homes - sens_gains = (-1227.0 - 409.0 * nbeds_eq) * general_water_use_usage_multiplier # Btu/day - lat_gains = (1245.0 + 415.0 * nbeds_eq) * general_water_use_usage_multiplier # Btu/day - return sens_gains * 365.0, lat_gains * 365.0 - end - # TODO # # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index 8d849c3f58..475c2d9b62 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -5351,4 +5351,27 @@ def self.calc_hspf_from_hspf2(hspf2, is_ducted) return hspf2 / 0.90 end end + + # Check provided HVAC system and distribution types against what is allowed. + # + # @param hvac_distribution [HPXML::HVACDistribution] HPXML HVAC Distribution object + # @param system_type [String] the HVAC system type of interest + # @return [nil] + def self.check_distribution_system(hvac_distribution, system_type) + return if hvac_distribution.nil? + + hvac_distribution_type_map = { HPXML::HVACTypeFurnace => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeBoiler => [HPXML::HVACDistributionTypeHydronic, HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeCentralAirConditioner => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeEvaporativeCooler => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeMiniSplitAirConditioner => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeHeatPumpAirToAir => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeHeatPumpMiniSplit => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeHeatPumpGroundToAir => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE], + HPXML::HVACTypeHeatPumpWaterLoopToAir => [HPXML::HVACDistributionTypeAir, HPXML::HVACDistributionTypeDSE] } + + if not hvac_distribution_type_map[system_type].include? hvac_distribution.distribution_system_type + fail "Incorrect HVAC distribution system type for HVAC type: '#{system_type}'. Should be one of: #{hvac_distribution_type_map[system_type]}" + end + end end diff --git a/HPXMLtoOpenStudio/resources/internal_gains.rb b/HPXMLtoOpenStudio/resources/internal_gains.rb new file mode 100644 index 0000000000..f1ae2e43a5 --- /dev/null +++ b/HPXMLtoOpenStudio/resources/internal_gains.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +# TODO +module InternalGains + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param num_occ [Double] Number of occupants in the dwelling unit + # @param space [OpenStudio::Model::Space] an OpenStudio::Model::Space object + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies + # @param apply_ashrae140_assumptions [Boolean] TODO + # @return [nil] + def self.apply(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods, apply_ashrae140_assumptions) + apply_building_occupancy(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods) + apply_general_water_use(model, runner, hpxml_bldg, space, schedules_file, unavailable_periods, apply_ashrae140_assumptions) + end + + # Create an OpenStudio People object using number of occupants and people/activity schedules. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param num_occ [Double] Number of occupants in the dwelling unit + # @param space [OpenStudio::Model::Space] an OpenStudio::Model::Space object + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies + # @return [nil] + def self.apply_building_occupancy(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods) + return if num_occ <= 0 + + occ_gain, _hrs_per_day, sens_frac, _lat_frac = get_occupancy_default_values() + activity_per_person = UnitConversions.convert(occ_gain, 'Btu/hr', 'W') + + # Hard-coded convective, radiative, latent, and lost fractions + occ_sens = sens_frac + occ_rad = 0.558 * occ_sens + + # Create schedule + people_sch = nil + people_col_name = SchedulesFile::Columns[:Occupants].name + if not schedules_file.nil? + people_sch = schedules_file.create_schedule_file(model, col_name: people_col_name) + end + if people_sch.nil? + people_unavailable_periods = Schedule.get_unavailable_periods(runner, people_col_name, unavailable_periods) + weekday_sch = hpxml_bldg.building_occupancy.weekday_fractions.split(',').map(&:to_f) + weekday_sch = weekday_sch.map { |v| v / weekday_sch.max }.join(',') + weekend_sch = hpxml_bldg.building_occupancy.weekend_fractions.split(',').map(&:to_f) + weekend_sch = weekend_sch.map { |v| v / weekend_sch.max }.join(',') + monthly_sch = hpxml_bldg.building_occupancy.monthly_multipliers + people_sch = MonthWeekdayWeekendSchedule.new(model, Constants::ObjectTypeOccupants + ' schedule', weekday_sch, weekend_sch, monthly_sch, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: people_unavailable_periods) + people_sch = people_sch.schedule + else + runner.registerWarning("Both '#{people_col_name}' schedule file and weekday fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.weekday_fractions.nil? + runner.registerWarning("Both '#{people_col_name}' schedule file and weekend fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.weekend_fractions.nil? + runner.registerWarning("Both '#{people_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.monthly_multipliers.nil? + end + + # Create schedule + activity_sch = OpenStudio::Model::ScheduleConstant.new(model) + activity_sch.setValue(activity_per_person) + activity_sch.setName(Constants::ObjectTypeOccupants + ' activity schedule') + + # Add people definition for the occ + occ_def = OpenStudio::Model::PeopleDefinition.new(model) + occ = OpenStudio::Model::People.new(occ_def) + occ.setName(Constants::ObjectTypeOccupants) + occ.setSpace(space) + occ_def.setName(Constants::ObjectTypeOccupants) + occ_def.setNumberofPeople(num_occ) + occ_def.setFractionRadiant(occ_rad) + occ_def.setSensibleHeatFraction(occ_sens) + occ_def.setMeanRadiantTemperatureCalculationType('ZoneAveraged') + occ_def.setCarbonDioxideGenerationRate(0) + occ_def.setEnableASHRAE55ComfortWarnings(false) + occ.setActivityLevelSchedule(activity_sch) + occ.setNumberofPeopleSchedule(people_sch) + end + + # Table 4.2.2(3). Internal Gains for Reference Homes + # + # @return [Array] TODO + def self.get_occupancy_default_values() + hrs_per_day = 16.5 # hrs/day + sens_gains = 3716.0 # Btu/person/day + lat_gains = 2884.0 # Btu/person/day + tot_gains = sens_gains + lat_gains + heat_gain = tot_gains / hrs_per_day # Btu/person/hr + sens_frac = sens_gains / tot_gains + lat_frac = lat_gains / tot_gains + return heat_gain, hrs_per_day, sens_frac, lat_frac + end + + # Set calendar year on the OpenStudio YearDescription object. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param apply_ashrae140_assumptions [Boolean] TODO + # @return [nil] + def self.apply_general_water_use(model, runner, hpxml_bldg, space, schedules_file, unavailable_periods, apply_ashrae140_assumptions) + general_water_use_usage_multiplier = hpxml_bldg.building_occupancy.general_water_use_usage_multiplier + nbeds_eq = hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms + + if not apply_ashrae140_assumptions + # General water use internal gains + # Floor mopping, shower evaporation, water films on showers, tubs & sinks surfaces, plant watering, etc. + water_sens_btu, water_lat_btu = get_water_gains_sens_lat(nbeds_eq, general_water_use_usage_multiplier) + + # Create schedule + water_schedule = nil + water_col_name = SchedulesFile::Columns[:GeneralWaterUse].name + water_obj_name = Constants::ObjectTypeGeneralWaterUse + if not schedules_file.nil? + water_design_level_sens = schedules_file.calc_design_level_from_daily_kwh(col_name: SchedulesFile::Columns[:GeneralWaterUse].name, daily_kwh: UnitConversions.convert(water_sens_btu, 'Btu', 'kWh') / 365.0) + water_design_level_lat = schedules_file.calc_design_level_from_daily_kwh(col_name: SchedulesFile::Columns[:GeneralWaterUse].name, daily_kwh: UnitConversions.convert(water_lat_btu, 'Btu', 'kWh') / 365.0) + water_schedule = schedules_file.create_schedule_file(model, col_name: water_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) + end + if water_schedule.nil? + water_unavailable_periods = Schedule.get_unavailable_periods(runner, water_col_name, unavailable_periods) + water_weekday_sch = hpxml_bldg.building_occupancy.general_water_use_weekday_fractions + water_weekend_sch = hpxml_bldg.building_occupancy.general_water_use_weekend_fractions + water_monthly_sch = hpxml_bldg.building_occupancy.general_water_use_monthly_multipliers + water_schedule_obj = MonthWeekdayWeekendSchedule.new(model, water_obj_name + ' schedule', water_weekday_sch, water_weekend_sch, water_monthly_sch, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: water_unavailable_periods) + water_design_level_sens = water_schedule_obj.calc_design_level_from_daily_kwh(UnitConversions.convert(water_sens_btu, 'Btu', 'kWh') / 365.0) + water_design_level_lat = water_schedule_obj.calc_design_level_from_daily_kwh(UnitConversions.convert(water_lat_btu, 'Btu', 'kWh') / 365.0) + water_schedule = water_schedule_obj.schedule + else + runner.registerWarning("Both '#{water_col_name}' schedule file and weekday fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_weekday_fractions.nil? + runner.registerWarning("Both '#{water_col_name}' schedule file and weekend fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_weekend_fractions.nil? + runner.registerWarning("Both '#{water_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_monthly_multipliers.nil? + end + HotWaterAndAppliances.add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseSensible, space, water_design_level_sens, 1.0, 0.0, water_schedule, nil) + HotWaterAndAppliances.add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseLatent, space, water_design_level_lat, 0.0, 1.0, water_schedule, nil) + end + end + + # TODO + # + # @param nbeds_eq [Integer] Number of bedrooms (or equivalent bedrooms, as adjusted by the number of occupants) in the dwelling unit + # @param general_water_use_usage_multiplier [TODO] TODO + # @return [TODO] TODO + def self.get_water_gains_sens_lat(nbeds_eq, general_water_use_usage_multiplier = 1.0) + # Table 4.2.2(3). Internal Gains for Reference Homes + sens_gains = (-1227.0 - 409.0 * nbeds_eq) * general_water_use_usage_multiplier # Btu/day + lat_gains = (1245.0 + 415.0 * nbeds_eq) * general_water_use_usage_multiplier # Btu/day + return sens_gains * 365.0, lat_gains * 365.0 + end +end diff --git a/HPXMLtoOpenStudio/resources/model.rb b/HPXMLtoOpenStudio/resources/model.rb new file mode 100644 index 0000000000..8436dbd58e --- /dev/null +++ b/HPXMLtoOpenStudio/resources/model.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +# Collection of methods related to generic OpenStudio Model object operations. +module Model + # Map of IDD objects => OSM classes + UniqueObjectsMap = { 'OS:ConvergenceLimits' => 'ConvergenceLimits', + 'OS:Foundation:Kiva:Settings' => 'FoundationKivaSettings', + 'OS:OutputControl:Files' => 'OutputControlFiles', + 'OS:Output:Diagnostics' => 'OutputDiagnostics', + 'OS:Output:JSON' => 'OutputJSON', + 'OS:PerformancePrecisionTradeoffs' => 'PerformancePrecisionTradeoffs', + 'OS:RunPeriod' => 'RunPeriod', + 'OS:RunPeriodControl:DaylightSavingTime' => 'RunPeriodControlDaylightSavingTime', + 'OS:ShadowCalculation' => 'ShadowCalculation', + 'OS:SimulationControl' => 'SimulationControl', + 'OS:Site' => 'Site', + 'OS:Site:GroundTemperature:Deep' => 'SiteGroundTemperatureDeep', + 'OS:Site:GroundTemperature:Shallow' => 'SiteGroundTemperatureShallow', + 'OS:Site:WaterMainsTemperature' => 'SiteWaterMainsTemperature', + 'OS:SurfaceConvectionAlgorithm:Inside' => 'InsideSurfaceConvectionAlgorithm', + 'OS:SurfaceConvectionAlgorithm:Outside' => 'OutsideSurfaceConvectionAlgorithm', + 'OS:Timestep' => 'Timestep' } + + # Tear down the existing model if it exists. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @return [nil] + def self.tear_down(model:, + runner:) + handles = OpenStudio::UUIDVector.new + model.objects.each do |obj| + handles << obj.handle + end + if !handles.empty? + runner.registerWarning('The model contains existing objects and is being reset.') + model.removeObjects(handles) + end + end + + # When there are multiple dwelling units, merge all unit models into a single model. + # First deal with unique objects; look for differences in values across unit models. + # Then make all unit models "unique" by shifting geometry and prefixing object names. + # Then bulk add all modified objects to the main OpenStudio Model object. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @return [nil] + def self.add_unit_model(model, hpxml_osm_map) + # Handle unique objects first: Grab one from the first model we find the + # object on (may not be the first unit). + unit_model_objects = [] + unique_handles_to_skip = [] + uuid_regex = /\{(.*?)\}/ + UniqueObjectsMap.each do |idd_obj, osm_class| + first_model_object_by_type = nil + hpxml_osm_map.values.each do |unit_model| + next if unit_model.getObjectsByType(idd_obj.to_IddObjectType).empty? + + model_object = unit_model.send("get#{osm_class}") + + if first_model_object_by_type.nil? + # Retain object for model + unit_model_objects << model_object + first_model_object_by_type = model_object + if idd_obj == 'OS:Site:WaterMainsTemperature' # Handle referenced child object too + unit_model_objects << unit_model.getObjectsByName(model_object.temperatureSchedule.get.name.to_s)[0] + end + else + # Throw error if different values between this model_object and first_model_object_by_type + if model_object.to_s.gsub(uuid_regex, '') != first_model_object_by_type.to_s.gsub(uuid_regex, '') + fail "Unique object (#{idd_obj}) has different values across dwelling units." + end + + if idd_obj == 'OS:Site:WaterMainsTemperature' # Handle referenced child object too + if model_object.temperatureSchedule.get.to_s.gsub(uuid_regex, '') != first_model_object_by_type.temperatureSchedule.get.to_s.gsub(uuid_regex, '') + fail "Unique object (#{idd_obj}) has different values across dwelling units." + end + end + end + + unique_handles_to_skip << model_object.handle.to_s + if idd_obj == 'OS:Site:WaterMainsTemperature' # Handle referenced child object too + unique_handles_to_skip << model_object.temperatureSchedule.get.handle.to_s + end + end + end + + hpxml_osm_map.values.each_with_index do |unit_model, unit_number| + Geometry.shift_surfaces(unit_model, unit_number) + prefix_objects(unit_model, unit_number) + + # Handle remaining (non-unique) objects now + unit_model.objects.each do |obj| + next if unit_number > 0 && obj.to_Building.is_initialized + next if unique_handles_to_skip.include? obj.handle.to_s + + unit_model_objects << obj + end + end + + model.addObjects(unit_model_objects, true) + end + + # Prefix all object names using using a provided unit number. + # + # @param unit_model [OpenStudio::Model::Model] OpenStudio Model object (corresponding to one of multiple dwelling units) + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [nil] + def self.prefix_objects(unit_model, unit_number) + # FUTURE: Create objects with unique names up front so we don't have to do this + + # EMS objects + ems_map = {} + + unit_model.getEnergyManagementSystemSensors.each do |sensor| + ems_map[sensor.name.to_s] = make_variable_name(sensor.name, unit_number) + sensor.setKeyName(make_variable_name(sensor.keyName, unit_number)) unless sensor.keyName.empty? || sensor.keyName.downcase == 'environment' + end + + unit_model.getEnergyManagementSystemActuators.each do |actuator| + ems_map[actuator.name.to_s] = make_variable_name(actuator.name, unit_number) + end + + unit_model.getEnergyManagementSystemInternalVariables.each do |internal_variable| + ems_map[internal_variable.name.to_s] = make_variable_name(internal_variable.name, unit_number) + internal_variable.setInternalDataIndexKeyName(make_variable_name(internal_variable.internalDataIndexKeyName, unit_number)) unless internal_variable.internalDataIndexKeyName.empty? + end + + unit_model.getEnergyManagementSystemGlobalVariables.each do |global_variable| + ems_map[global_variable.name.to_s] = make_variable_name(global_variable.name, unit_number) + end + + unit_model.getEnergyManagementSystemOutputVariables.each do |output_variable| + next if output_variable.emsVariableObject.is_initialized + + new_ems_variable_name = make_variable_name(output_variable.emsVariableName, unit_number) + ems_map[output_variable.emsVariableName.to_s] = new_ems_variable_name + output_variable.setEMSVariableName(new_ems_variable_name) + end + + unit_model.getEnergyManagementSystemSubroutines.each do |subroutine| + ems_map[subroutine.name.to_s] = make_variable_name(subroutine.name, unit_number) + end + + # variables in program lines don't get updated automatically + lhs_characters = [' ', ',', '(', ')', '+', '-', '*', '/', ';'] + rhs_characters = [''] + lhs_characters + (unit_model.getEnergyManagementSystemPrograms + unit_model.getEnergyManagementSystemSubroutines).each do |program| + new_lines = [] + program.lines.each do |line| + ems_map.each do |old_name, new_name| + next unless line.include?(old_name) + + # old_name between at least 1 character, with the exception of '' on left and ' ' on right + lhs_characters.each do |lhs| + next unless line.include?("#{lhs}#{old_name}") + + rhs_characters.each do |rhs| + next unless line.include?("#{lhs}#{old_name}#{rhs}") + next if lhs == '' && ['', ' '].include?(rhs) + + line.gsub!("#{lhs}#{old_name}#{rhs}", "#{lhs}#{new_name}#{rhs}") + end + end + end + new_lines << line + end + program.setLines(new_lines) + end + + # All model objects + unit_model.objects.each do |model_object| + next if model_object.name.nil? + + if unit_number == 0 + # OpenStudio is unhappy if these schedules are renamed + next if model_object.name.to_s == unit_model.alwaysOnContinuousSchedule.name.to_s + next if model_object.name.to_s == unit_model.alwaysOnDiscreteSchedule.name.to_s + next if model_object.name.to_s == unit_model.alwaysOffDiscreteSchedule.name.to_s + end + + model_object.setName(make_variable_name(model_object.name, unit_number)) + end + end + + # Create a new OpenStudio object name by prefixing the old with "unit" plus the unit number. + # + # @param obj_name [String] the OpenStudio object name + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [String] the new OpenStudio object name with unique unit prefix + def self.make_variable_name(obj_name, unit_number) + return "unit#{unit_number + 1}_#{obj_name}".gsub(' ', '_').gsub('-', '_') + end +end From 6f24420443842b76cd0dbe73f6f415ec0fa52a4b Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 4 Sep 2024 07:57:24 -0700 Subject: [PATCH 08/16] More method name updates. --- BuildResidentialHPXML/measure.rb | 2 +- BuildResidentialHPXML/measure.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BuildResidentialHPXML/measure.rb b/BuildResidentialHPXML/measure.rb index 9165268710..ef98bb0d0e 100644 --- a/BuildResidentialHPXML/measure.rb +++ b/BuildResidentialHPXML/measure.rb @@ -3501,7 +3501,7 @@ def run(model, runner, user_arguments) return false end - Geometry.tear_down_model(model: model, runner: runner) + Model.tear_down(model: model, runner: runner) Version.check_openstudio_version() diff --git a/BuildResidentialHPXML/measure.xml b/BuildResidentialHPXML/measure.xml index abcb0718d0..cd166b4b17 100644 --- a/BuildResidentialHPXML/measure.xml +++ b/BuildResidentialHPXML/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_hpxml a13a8983-2b01-4930-8af2-42030b6e4233 - af7b54a5-2b26-4248-9663-0a654016b33e - 2024-09-03T22:18:23Z + 328d033b-3330-4c26-a3c0-f43b5f20704b + 2024-09-04T14:56:54Z 2C38F48B BuildResidentialHPXML HPXML Builder @@ -7442,7 +7442,7 @@ measure.rb rb script - 439FE673 + 881CB91D constants.rb From cb20947186c17135ba0364b973c49247cb8fd6ae Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 16 Sep 2024 14:48:41 -0600 Subject: [PATCH 09/16] Huge pass on moving code out of measure.rb, cleanup. --- BuildResidentialHPXML/measure.rb | 3 +- BuildResidentialHPXML/measure.xml | 6 +- HPXMLtoOpenStudio/measure.rb | 2800 ++--------------- HPXMLtoOpenStudio/measure.xml | 34 +- HPXMLtoOpenStudio/resources/airflow.rb | 38 +- HPXMLtoOpenStudio/resources/battery.rb | 32 +- HPXMLtoOpenStudio/resources/generator.rb | 20 +- HPXMLtoOpenStudio/resources/geometry.rb | 1160 ++++++- .../resources/hotwater_appliances.rb | 54 +- HPXMLtoOpenStudio/resources/hpxml_defaults.rb | 4 +- HPXMLtoOpenStudio/resources/hvac.rb | 386 ++- HPXMLtoOpenStudio/resources/hvac_sizing.rb | 6 +- HPXMLtoOpenStudio/resources/internal_gains.rb | 51 +- HPXMLtoOpenStudio/resources/lighting.rb | 29 +- HPXMLtoOpenStudio/resources/location.rb | 15 +- HPXMLtoOpenStudio/resources/misc_loads.rb | 124 +- HPXMLtoOpenStudio/resources/output.rb | 820 +++++ HPXMLtoOpenStudio/resources/pv.rb | 27 +- 18 files changed, 2765 insertions(+), 2844 deletions(-) diff --git a/BuildResidentialHPXML/measure.rb b/BuildResidentialHPXML/measure.rb index 1344146496..1d9836dad0 100644 --- a/BuildResidentialHPXML/measure.rb +++ b/BuildResidentialHPXML/measure.rb @@ -3915,8 +3915,7 @@ def self.create(runner, model, args, epw_path, hpxml_path, existing_hpxml_path) return false end - eri_version = Constants::ERIVersions[-1] - HPXMLDefaults.apply(runner, hpxml, hpxml_bldg, eri_version, weather) + HPXMLDefaults.apply(runner, hpxml, hpxml_bldg, weather) hpxml_doc = hpxml.to_doc() hpxml.set_unique_hpxml_ids(hpxml_doc, true) if hpxml.buildings.size > 1 XMLHelper.write_file(hpxml_doc, hpxml_path) diff --git a/BuildResidentialHPXML/measure.xml b/BuildResidentialHPXML/measure.xml index 5866cdee1e..0a922154ed 100644 --- a/BuildResidentialHPXML/measure.xml +++ b/BuildResidentialHPXML/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_hpxml a13a8983-2b01-4930-8af2-42030b6e4233 - 3b6e8b4c-7f47-4c94-8bdb-aeafd3ae5656 - 2024-09-16T14:45:22Z + de78116f-221f-4cab-9488-203b60d0821f + 2024-09-16T19:31:14Z 2C38F48B BuildResidentialHPXML HPXML Builder @@ -7441,7 +7441,7 @@ measure.rb rb script - 876A20F6 + 58283E8D constants.rb diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 472107b050..84ca3f6837 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -155,9 +155,18 @@ def run(model, runner, user_arguments) end return false unless hpxml.errors.empty? - eri_version = hpxml.header.eri_calculation_version # Hidden feature - eri_version = 'latest' if eri_version.nil? - eri_version = Constants::ERIVersions[-1] if eri_version == 'latest' + # Hidden feature: Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions + if hpxml.header.eri_calculation_version.nil? + hpxml.header.eri_calculation_version = 'latest' + end + if hpxml.header.eri_calculation_version == 'latest' + hpxml.header.eri_calculation_version = Constants::ERIVersions[-1] + end + + # Hidden feature: Whether to override certain assumptions to better match the ASHRAE 140 specification + if hpxml.header.apply_ashrae140_assumptions.nil? + hpxml.header.apply_ashrae140_assumptions = false + end # Process weather once upfront epw_path = Location.get_epw_path(hpxml.buildings[0], args[:hpxml_path]) @@ -193,9 +202,9 @@ def run(model, runner, user_arguments) unavailable_periods: hpxml.header.unavailable_periods, output_path: File.join(args[:output_dir], in_schedules_csv), offset_db: hpxml.header.hvac_onoff_thermostat_deadband) - HPXMLDefaults.apply(runner, hpxml, hpxml_bldg, eri_version, weather, schedules_file: schedules_file, - design_load_details_output_file_path: design_load_details_output_file_path, - output_format: args[:output_format]) + HPXMLDefaults.apply(runner, hpxml, hpxml_bldg, weather, schedules_file: schedules_file, + design_load_details_output_file_path: design_load_details_output_file_path, + output_format: args[:output_format]) hpxml_sch_map[hpxml_bldg] = schedules_file end Schedule.validate_emissions_files(hpxml.header) @@ -218,10 +227,10 @@ def run(model, runner, user_arguments) if hpxml.buildings.size > 1 # Create the model for this single unit unit_model = OpenStudio::Model::Model.new - create_unit_model(hpxml, hpxml_bldg, runner, unit_model, epw_path, weather, args[:debug], schedules_file, eri_version, i + 1) + create_unit_model(hpxml, hpxml_bldg, runner, unit_model, epw_path, weather, schedules_file, i + 1) hpxml_osm_map[hpxml_bldg] = unit_model else - create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, args[:debug], schedules_file, eri_version, i + 1) + create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, schedules_file, i + 1) hpxml_osm_map[hpxml_bldg] = model end end @@ -232,16 +241,10 @@ def run(model, runner, user_arguments) end # Output - season_day_nums = add_unmet_hours_output(model, hpxml_osm_map, hpxml) - loads_data = add_total_loads_output(model, hpxml_osm_map) - if args[:add_component_loads] - add_component_loads_output(model, hpxml_osm_map, loads_data, season_day_nums) - end - add_total_airflows_output(model, hpxml_osm_map) - add_output_files(model) - add_additional_properties(model, hpxml, hpxml_osm_map, args[:hpxml_path], args[:building_id], hpxml_defaults_path) - # Uncomment to debug EMS - # add_ems_debug_output(model) + Outputs.apply_ems_programs(model, hpxml_osm_map, hpxml.header, args[:add_component_loads]) + Outputs.apply_output_files(model, args[:debug]) + Outputs.apply_additional_properties(model, hpxml, hpxml_osm_map, args[:hpxml_path], args[:building_id], hpxml_defaults_path) + # Outputs.apply_ems_debug_output(model) # Uncomment to debug EMS if args[:debug] # Write OSM file to run dir @@ -270,22 +273,10 @@ def run(model, runner, user_arguments) # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param epw_path [String] Path to the EPW weather file # @param weather [WeatherFile] Weather object containing EPW information - # @param debug [Boolean] true writes in.osm, generates additional log output, and creates all E+ output files # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param unit_num [Integer] index number corresponding to an HPXML Building object # @return [nil] - def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug, schedules_file, eri_version, unit_num) - @hpxml_header = hpxml.header - @hpxml_bldg = hpxml_bldg - @epw_path = epw_path - @debug = debug - @schedules_file = schedules_file - @eri_version = eri_version - - @apply_ashrae140_assumptions = @hpxml_header.apply_ashrae140_assumptions # Hidden feature - @apply_ashrae140_assumptions = false if @apply_ashrae140_assumptions.nil? - + def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, schedules_file, unit_num) # Here we turn off OS error-checking so that any invalid values provided # to OS SDK methods are passed along to EnergyPlus and produce errors. If # we didn't go this, we'd end up with successful EnergyPlus simulations that @@ -295,2631 +286,262 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, debug model.setStrictnessLevel('None'.to_StrictnessLevel) # Init - set_inits_and_globals(model) - set_foundation_and_walls_top() - set_hvac_seasons(runner) - set_unavailable_periods(runner) + apply_init(hpxml_bldg, hpxml.header) + + # Simulation Controls + SimControls.apply(model, hpxml.header) + # Location + Location.apply(model, weather, hpxml_bldg, hpxml.header, epw_path) + + # Geometry/Enclosure spaces = {} # Map of HPXML locations => OpenStudio Space objects - airloop_map = {} # Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil) - - # Sim controls/location - add_simulation_params(model) - add_location(model, weather) - - # Conditioned space/zone - add_conditioned_space(model, spaces) - add_setpoints(runner, model, weather, spaces) - add_internal_gains(runner, model, spaces) - - # Geometry/Envelope - add_roofs(runner, model, spaces) - add_walls(runner, model, spaces) - add_rim_joists(runner, model, spaces) - add_floors(runner, model, spaces) - add_foundation_walls_slabs(runner, model, weather, spaces) - add_windows(model, spaces) - add_doors(model, spaces) - add_skylights(model, spaces) - add_conditioned_floor_area(model, spaces) - add_thermal_mass(model, spaces) - add_geometry_other(model, spaces) - - # HVAC - add_ideal_system(model, spaces, weather) - add_cooling_system(model, runner, weather, spaces, airloop_map) - add_heating_system(runner, model, weather, spaces, airloop_map) - add_heat_pump(runner, model, weather, spaces, airloop_map) - add_dehumidifiers(runner, model, spaces) - add_ceiling_fans(runner, model, weather, spaces) + Geometry.apply_roofs(runner, model, spaces, hpxml_bldg, hpxml.header) + Geometry.apply_walls(runner, model, spaces, hpxml_bldg, hpxml.header) + Geometry.apply_rim_joists(runner, model, spaces, hpxml_bldg) + Geometry.apply_floors(runner, model, spaces, hpxml_bldg, hpxml.header) + Geometry.apply_foundation_walls_slabs(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + Geometry.apply_windows(model, spaces, hpxml_bldg, hpxml.header) + Geometry.apply_doors(model, spaces, hpxml_bldg) + Geometry.apply_skylights(model, spaces, hpxml_bldg, hpxml.header) + Geometry.apply_conditioned_floor_area(model, spaces, hpxml_bldg) + Geometry.apply_thermal_mass(model, spaces, hpxml_bldg, hpxml.header) + Geometry.set_zone_volumes(spaces, hpxml_bldg, hpxml.header) + Geometry.explode_surfaces(model, hpxml_bldg) + Geometry.apply_building_unit(model, unit_num) + + # Systems + hvac_data = HVAC.apply_hvac_systems(model, runner, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + HVAC.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml.header) + HVAC.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml.header, schedules_file) # Hot Water - add_hot_water_and_appliances(runner, model, weather, spaces) + add_hot_water_and_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + + # Lighting + Lighting.apply(runner, model, spaces, hpxml_bldg, hpxml.header, schedules_file) - # Plug Loads & Fuel Loads & Lighting - add_mels(runner, model, spaces) - add_mfls(runner, model, spaces) - add_lighting(runner, model, spaces) + # MiscLoads, Pools/Spas + MiscLoads.apply_plug_loads(runner, model, spaces, hpxml_bldg, hpxml.header, schedules_file) + MiscLoads.apply_fuel_loads(runner, model, spaces, hpxml_bldg, hpxml.header, schedules_file) + MiscLoads.apply_pools_and_permanent_spas(runner, model, spaces, hpxml_bldg, hpxml.header, schedules_file) - # Pools & Permanent Spas - add_pools_and_permanent_spas(runner, model, spaces) + # Internal Gains + InternalGains.apply_building_occupants(model, runner, hpxml_bldg, hpxml.header, spaces, schedules_file) + InternalGains.apply_general_water_use(model, runner, hpxml_bldg, hpxml.header, spaces, schedules_file) # Other - add_airflow(runner, model, weather, spaces, airloop_map) - add_photovoltaics(model) - add_generators(model) - add_batteries(runner, model, spaces) - add_building_unit(model, unit_num) + add_airflow(runner, model, weather, spaces, hpxml_bldg, hpxml.header, hvac_data, schedules_file) + PV.apply_pv_systems(model, hpxml_bldg) + Generator.apply_generators(model, hpxml_bldg) + Battery.apply_batteries(runner, model, spaces, hpxml_bldg, schedules_file) end - # Initialize heat/load frac variables ahead of HVAC system methods. - # Globalize select HPXML Building properties, e.g., conditioned floor area, number of conditioned floors, number of bedrooms, etc. - # Perform other leading operations like applying unit multipliers, collapsing like HPXML surfaces, etc. + # TODO # - # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @return [nil] - def set_inits_and_globals(model) - # Weather file - OpenStudio::Model::WeatherFile.setWeatherFile(model, OpenStudio::EpwFile.new(@epw_path)) - - # Initialize - @remaining_heat_load_frac = 1.0 - @remaining_cool_load_frac = 1.0 - - # Set globals - @cfa = @hpxml_bldg.building_construction.conditioned_floor_area - @ncfl = @hpxml_bldg.building_construction.number_of_conditioned_floors - @ncfl_ag = @hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade - @nbeds = @hpxml_bldg.building_construction.number_of_bedrooms - @default_azimuths = HPXMLDefaults.get_default_azimuths(@hpxml_bldg) - + def apply_init(hpxml_bldg, hpxml_header) # Apply unit multipliers to HVAC systems and water heaters - HVAC.apply_unit_multiplier(@hpxml_bldg, @hpxml_header) + HVAC.apply_unit_multiplier(hpxml_bldg, hpxml_header) # Ensure that no capacities/airflows are zero in order to prevent potential E+ errors. - HVAC.ensure_nonzero_sizing_values(@hpxml_bldg) + HVAC.ensure_nonzero_sizing_values(hpxml_bldg) # Make adjustments for modeling purposes - @frac_windows_operable = @hpxml_bldg.fraction_of_windows_operable() - @hpxml_bldg.collapse_enclosure_surfaces() # Speeds up simulation - @hpxml_bldg.delete_adiabatic_subsurfaces() # EnergyPlus doesn't allow this + @frac_windows_operable = hpxml_bldg.fraction_of_windows_operable() + hpxml_bldg.collapse_enclosure_surfaces() # Speeds up simulation + hpxml_bldg.delete_adiabatic_subsurfaces() # EnergyPlus doesn't allow this - if not @hpxml_bldg.building_occupancy.number_of_residents.nil? + if not hpxml_bldg.building_occupancy.number_of_residents.nil? # If zero occupants, ensure end uses of interest are zeroed out - if (@hpxml_bldg.building_occupancy.number_of_residents == 0) && (not @apply_ashrae140_assumptions) - @hpxml_header.unavailable_periods.add(column_name: 'Vacancy', - begin_month: @hpxml_header.sim_begin_month, - begin_day: @hpxml_header.sim_begin_day, - begin_hour: 0, - end_month: @hpxml_header.sim_end_month, - end_day: @hpxml_header.sim_end_day, - end_hour: 24, - natvent_availability: HPXML::ScheduleUnavailable) + if (hpxml_bldg.building_occupancy.number_of_residents == 0) && (not hpxml_header.apply_ashrae140_assumptions) + hpxml_header.unavailable_periods.add(column_name: 'Vacancy', + begin_month: hpxml_header.sim_begin_month, + begin_day: hpxml_header.sim_begin_day, + begin_hour: 0, + end_month: hpxml_header.sim_end_month, + end_day: hpxml_header.sim_end_day, + end_hour: 24, + natvent_availability: HPXML::ScheduleUnavailable) end end end - # TODO - # - # @return [nil] - def set_foundation_and_walls_top() - @foundation_top = [@hpxml_bldg.building_construction.unit_height_above_grade, 0].max - @hpxml_bldg.foundation_walls.each do |foundation_wall| - top = -1 * foundation_wall.depth_below_grade + foundation_wall.height - @foundation_top = top if top > @foundation_top - end - @walls_top = @foundation_top + @hpxml_bldg.building_construction.average_ceiling_height * @ncfl_ag - end - - # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling season begin/end month/day, respectively. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @return [nil] - def set_hvac_seasons(runner) - return if @hpxml_bldg.hvac_controls.size == 0 - - hvac_control = @hpxml_bldg.hvac_controls[0] - - htg_start_month = hvac_control.seasons_heating_begin_month - htg_start_day = hvac_control.seasons_heating_begin_day - htg_end_month = hvac_control.seasons_heating_end_month - htg_end_day = hvac_control.seasons_heating_end_day - clg_start_month = hvac_control.seasons_cooling_begin_month - clg_start_day = hvac_control.seasons_cooling_begin_day - clg_end_month = hvac_control.seasons_cooling_end_month - clg_end_day = hvac_control.seasons_cooling_end_day - - @heating_days = Calendar.get_daily_season(@hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) - @cooling_days = Calendar.get_daily_season(@hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) - - if (htg_start_month != 1) || (htg_start_day != 1) || (htg_end_month != 12) || (htg_end_day != 31) || (clg_start_month != 1) || (clg_start_day != 1) || (clg_end_month != 12) || (clg_end_day != 31) - runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.') - end - end - - # TODO + # First assign OpenStudio Space object for appliances based on HPXML Location. + # Then adds any of the following to the OpenStudio model: + # - HPXML Clothes Washers + # - HPXML Clothes Dryers + # - HPXML Dishwashers + # - HPXML Refrigerators + # - HPXML Freezers + # - HPXML Cooking Ranges / Ovens + # - HPXML Hot Water Distribution + # - HPXML Solar Thermal System + # - HPXML Water Heating Systems + # - HPXML Water Fixtures # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @return [nil] - def set_unavailable_periods(runner) - @hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, @hpxml_header.unavailable_periods) - end - - # Adds HPXML Simulation Control to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [nil] - def add_simulation_params(model) - SimControls.apply(model, @hpxml_header) - end - - # Adds HPXML Building Site to the OpenStudio model. - # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information - # @return [nil] - def add_location(model, weather) - Location.apply(model, weather, @hpxml_header, @hpxml_bldg) - end - - # Adds a conditioned space and zone to the OpenStudio model. - # - # @return [nil] - def add_conditioned_space(model, spaces) - Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg) - end - - # Adds HPXML Building Occupancy to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def add_internal_gains(runner, model, spaces) - # Occupants - if @hpxml_bldg.building_occupancy.number_of_residents.nil? # Asset calculation - num_occ = Geometry.get_occupancy_default_num(nbeds: @nbeds) - else # Operational calculation - num_occ = @hpxml_bldg.building_occupancy.number_of_residents + def add_hot_water_and_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + # Distribution + if hpxml_bldg.water_heating_systems.size > 0 + hot_water_distribution = hpxml_bldg.hot_water_distributions[0] end - InternalGains.apply(model, runner, @hpxml_bldg, num_occ, spaces[HPXML::LocationConditionedSpace], - @schedules_file, @hpxml_header.unavailable_periods, @apply_ashrae140_assumptions) - end - - # Adds any HPXML Roofs to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_roofs(runner, model, spaces) - @hpxml_bldg.roofs.each do |roof| - next if roof.net_area < 1.0 # skip modeling net surface area for surfaces comprised entirely of subsurface area - - if roof.azimuth.nil? - if roof.pitch > 0 - azimuths = @default_azimuths # Model as four directions for average exterior incident solar - else - azimuths = [@default_azimuths[0]] # Arbitrary azimuth for flat roof - end - else - azimuths = [roof.azimuth] - end - - surfaces = [] - - azimuths.each do |azimuth| - width = Math::sqrt(roof.net_area) - length = (roof.net_area / width) / azimuths.size - tilt = roof.pitch / 12.0 - z_origin = @walls_top + 0.5 * Math.sin(Math.atan(tilt)) * width - - vertices = Geometry.create_roof_vertices(length: length, width: width, z_origin: z_origin, azimuth: azimuth, tilt: tilt) - surface = OpenStudio::Model::Surface.new(vertices, model) - surfaces << surface - surface.additionalProperties.setFeature('Length', length) - surface.additionalProperties.setFeature('Width', width) - surface.additionalProperties.setFeature('Azimuth', azimuth) - surface.additionalProperties.setFeature('Tilt', tilt) - surface.additionalProperties.setFeature('SurfaceType', 'Roof') - if azimuths.size > 1 - surface.setName("#{roof.id}:#{azimuth}") - else - surface.setName(roof.id) - end - surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) - Geometry.set_surface_interior(model, spaces, surface, roof, @hpxml_bldg) - end - - next if surfaces.empty? - - # Apply construction - has_radiant_barrier = roof.radiant_barrier - if has_radiant_barrier - radiant_barrier_grade = roof.radiant_barrier_grade - end - # FUTURE: Create Constructions.get_air_film(surface) method; use in measure.rb and hpxml_translator_test.rb - inside_film = Material.AirFilmRoof(Geometry.get_roof_pitch([surfaces[0]])) - outside_film = Material.AirFilmOutside - mat_roofing = Material.RoofMaterial(roof.roof_type) - if @apply_ashrae140_assumptions - inside_film = Material.AirFilmRoofASHRAE140 - outside_film = Material.AirFilmOutsideASHRAE140 - end - mat_int_finish = Material.InteriorFinishMaterial(roof.interior_finish_type, roof.interior_finish_thickness) - if mat_int_finish.nil? - fallback_mat_int_finish = nil - else - fallback_mat_int_finish = Material.InteriorFinishMaterial(mat_int_finish.name, 0.1) # Try thin material - end - - install_grade = 1 - assembly_r = roof.insulation_assembly_r_value - - if not mat_int_finish.nil? - # Closed cavity - constr_sets = [ - WoodStudConstructionSet.new(Material.Stud2x(8.0), 0.07, 20.0, 0.75, mat_int_finish, mat_roofing), # 2x8, 24" o.c. + R20 - WoodStudConstructionSet.new(Material.Stud2x(8.0), 0.07, 10.0, 0.75, mat_int_finish, mat_roofing), # 2x8, 24" o.c. + R10 - WoodStudConstructionSet.new(Material.Stud2x(8.0), 0.07, 0.0, 0.75, mat_int_finish, mat_roofing), # 2x8, 24" o.c. - WoodStudConstructionSet.new(Material.Stud2x6, 0.07, 0.0, 0.75, mat_int_finish, mat_roofing), # 2x6, 24" o.c. - WoodStudConstructionSet.new(Material.Stud2x4, 0.07, 0.0, 0.5, mat_int_finish, mat_roofing), # 2x4, 16" o.c. - WoodStudConstructionSet.new(Material.Stud2x4, 0.01, 0.0, 0.0, fallback_mat_int_finish, mat_roofing), # Fallback - ] - match, constr_set, cavity_r = Constructions.pick_wood_stud_construction_set(assembly_r, constr_sets, inside_film, outside_film) - - Constructions.apply_closed_cavity_roof(model, surfaces, "#{roof.id} construction", - cavity_r, install_grade, - constr_set.stud.thick_in, - true, constr_set.framing_factor, - constr_set.mat_int_finish, - constr_set.osb_thick_in, constr_set.rigid_r, - constr_set.mat_ext_finish, has_radiant_barrier, - inside_film, outside_film, radiant_barrier_grade, - roof.solar_absorptance, roof.emittance) - else - # Open cavity - constr_sets = [ - GenericConstructionSet.new(10.0, 0.5, nil, mat_roofing), # w/R-10 rigid - GenericConstructionSet.new(0.0, 0.5, nil, mat_roofing), # Standard - GenericConstructionSet.new(0.0, 0.0, nil, mat_roofing), # Fallback - ] - match, constr_set, layer_r = Constructions.pick_generic_construction_set(assembly_r, constr_sets, inside_film, outside_film) - - cavity_r = 0 - cavity_ins_thick_in = 0 - framing_factor = 0 - framing_thick_in = 0 - - Constructions.apply_open_cavity_roof(model, surfaces, "#{roof.id} construction", - cavity_r, install_grade, cavity_ins_thick_in, - framing_factor, framing_thick_in, - constr_set.osb_thick_in, layer_r + constr_set.rigid_r, - constr_set.mat_ext_finish, has_radiant_barrier, - inside_film, outside_film, radiant_barrier_grade, - roof.solar_absorptance, roof.emittance) - end - Constructions.check_surface_assembly_rvalue(runner, surfaces, inside_film, outside_film, assembly_r, match) + # Solar thermal system + solar_thermal_system = nil + if hpxml_bldg.solar_thermal_systems.size > 0 + solar_thermal_system = hpxml_bldg.solar_thermal_systems[0] end - end - - # Adds any HPXML Walls to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_walls(runner, model, spaces) - @hpxml_bldg.walls.each do |wall| - next if wall.net_area < 1.0 # skip modeling net surface area for surfaces comprised entirely of subsurface area - - if wall.azimuth.nil? - if wall.is_exterior - azimuths = @default_azimuths # Model as four directions for average exterior incident solar - else - azimuths = [@default_azimuths[0]] # Arbitrary direction, doesn't receive exterior incident solar - end - else - azimuths = [wall.azimuth] - end - surfaces = [] - - azimuths.each do |azimuth| - height = 8.0 * @ncfl_ag - length = (wall.net_area / height) / azimuths.size - z_origin = @foundation_top - - vertices = Geometry.create_wall_vertices(length: length, height: height, z_origin: z_origin, azimuth: azimuth) - surface = OpenStudio::Model::Surface.new(vertices, model) - surfaces << surface - surface.additionalProperties.setFeature('Length', length) - surface.additionalProperties.setFeature('Azimuth', azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'Wall') - if azimuths.size > 1 - surface.setName("#{wall.id}:#{azimuth}") - else - surface.setName(wall.id) - end - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, wall, @hpxml_bldg) - Geometry.set_surface_exterior(model, spaces, surface, wall, @hpxml_bldg) - if wall.is_interior - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - end - end + # Water Heater + unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:WaterHeater].name, hpxml_header.unavailable_periods) + unit_multiplier = hpxml_bldg.building_construction.number_of_units + has_uncond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementUnconditioned) + has_cond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementConditioned) + nbeds = hpxml_bldg.building_construction.number_of_bedrooms + cfa = hpxml_bldg.building_construction.conditioned_floor_area + ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors + eri_version = hpxml_header.eri_calculation_version + plantloop_map = {} + hpxml_bldg.water_heating_systems.each do |water_heating_system| + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) - next if surfaces.empty? + ec_adj = HotWaterAndAppliances.get_dist_energy_consumption_adjustment(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl, water_heating_system, hot_water_distribution) - # Apply construction - # The code below constructs a reasonable wall construction based on the - # wall type while ensuring the correct assembly R-value. - has_radiant_barrier = wall.radiant_barrier - if has_radiant_barrier - radiant_barrier_grade = wall.radiant_barrier_grade - end - inside_film = Material.AirFilmVertical - if wall.is_exterior - outside_film = Material.AirFilmOutside - mat_ext_finish = Material.ExteriorFinishMaterial(wall.siding) + sys_id = water_heating_system.id + if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeStorage + plantloop_map[sys_id] = Waterheater.apply_tank(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) + elsif water_heating_system.water_heater_type == HPXML::WaterHeaterTypeTankless + plantloop_map[sys_id] = Waterheater.apply_tankless(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) + elsif water_heating_system.water_heater_type == HPXML::WaterHeaterTypeHeatPump + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + plantloop_map[sys_id] = Waterheater.apply_heatpump(model, runner, loc_space, loc_schedule, hpxml_bldg.elevation, water_heating_system, ec_adj, solar_thermal_system, conditioned_zone, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) + elsif [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? water_heating_system.water_heater_type + plantloop_map[sys_id] = Waterheater.apply_combi(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) else - outside_film = Material.AirFilmVertical - mat_ext_finish = nil - end - if @apply_ashrae140_assumptions - inside_film = Material.AirFilmVerticalASHRAE140 - outside_film = Material.AirFilmOutsideASHRAE140 + fail "Unhandled water heater (#{water_heating_system.water_heater_type})." end - mat_int_finish = Material.InteriorFinishMaterial(wall.interior_finish_type, wall.interior_finish_thickness) - - Constructions.apply_wall_construction(runner, model, surfaces, wall.id, wall.wall_type, wall.insulation_assembly_r_value, - mat_int_finish, has_radiant_barrier, inside_film, outside_film, - radiant_barrier_grade, mat_ext_finish, wall.solar_absorptance, - wall.emittance) end - end - - # Adds any HPXML RimJoists to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_rim_joists(runner, model, spaces) - @hpxml_bldg.rim_joists.each do |rim_joist| - if rim_joist.azimuth.nil? - if rim_joist.is_exterior - azimuths = @default_azimuths # Model as four directions for average exterior incident solar - else - azimuths = [@default_azimuths[0]] # Arbitrary direction, doesn't receive exterior incident solar - end - else - azimuths = [rim_joist.azimuth] - end - - surfaces = [] - - azimuths.each do |azimuth| - height = 1.0 - length = (rim_joist.area / height) / azimuths.size - z_origin = @foundation_top - - vertices = Geometry.create_wall_vertices(length: length, height: height, z_origin: z_origin, azimuth: azimuth) - surface = OpenStudio::Model::Surface.new(vertices, model) - surfaces << surface - surface.additionalProperties.setFeature('Length', length) - surface.additionalProperties.setFeature('Azimuth', azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'RimJoist') - if azimuths.size > 1 - surface.setName("#{rim_joist.id}:#{azimuth}") - else - surface.setName(rim_joist.id) - end - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, rim_joist, @hpxml_bldg) - Geometry.set_surface_exterior(model, spaces, surface, rim_joist, @hpxml_bldg) - if rim_joist.is_interior - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - end - end - - # Apply construction - inside_film = Material.AirFilmVertical - if rim_joist.is_exterior - outside_film = Material.AirFilmOutside - mat_ext_finish = Material.ExteriorFinishMaterial(rim_joist.siding) - else - outside_film = Material.AirFilmVertical - mat_ext_finish = nil - end + # Hot water fixtures and appliances + HotWaterAndAppliances.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_water_distribution, + solar_thermal_system, eri_version, schedules_file, plantloop_map, + hpxml_header.unavailable_periods, hpxml_bldg.building_construction.number_of_units) - assembly_r = rim_joist.insulation_assembly_r_value - - constr_sets = [ - WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.17, 20.0, 2.0, nil, mat_ext_finish), # 2x4 + R20 - WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.17, 10.0, 2.0, nil, mat_ext_finish), # 2x4 + R10 - WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.17, 0.0, 2.0, nil, mat_ext_finish), # 2x4 - WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.01, 0.0, 0.0, nil, mat_ext_finish), # Fallback - ] - match, constr_set, cavity_r = Constructions.pick_wood_stud_construction_set(assembly_r, constr_sets, inside_film, outside_film) - install_grade = 1 - - Constructions.apply_rim_joist(model, surfaces, "#{rim_joist.id} construction", - cavity_r, install_grade, constr_set.framing_factor, - constr_set.mat_int_finish, constr_set.osb_thick_in, - constr_set.rigid_r, constr_set.mat_ext_finish, - inside_film, outside_film, rim_joist.solar_absorptance, - rim_joist.emittance) - Constructions.check_surface_assembly_rvalue(runner, surfaces, inside_film, outside_film, assembly_r, match) + if (not solar_thermal_system.nil?) && (not solar_thermal_system.collector_area.nil?) # Detailed solar water heater + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) + Waterheater.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_system, plantloop_map, unit_multiplier) end - end - - # Adds any HPXML Floors to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_floors(runner, model, spaces) - @hpxml_bldg.floors.each do |floor| - next if floor.net_area < 1.0 # skip modeling net surface area for surfaces comprised entirely of subsurface area - - area = floor.net_area - width = Math::sqrt(area) - length = area / width - if floor.interior_adjacent_to.include?('attic') || floor.exterior_adjacent_to.include?('attic') - z_origin = @walls_top - else - z_origin = @foundation_top - end - - if floor.is_ceiling - vertices = Geometry.create_ceiling_vertices(length: length, width: width, z_origin: z_origin, default_azimuths: @default_azimuths) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('SurfaceType', 'Ceiling') - else - vertices = Geometry.create_floor_vertices(length: length, width: width, z_origin: z_origin, default_azimuths: @default_azimuths) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('SurfaceType', 'Floor') - end - surface.additionalProperties.setFeature('Tilt', 0.0) - Geometry.set_surface_interior(model, spaces, surface, floor, @hpxml_bldg) - Geometry.set_surface_exterior(model, spaces, surface, floor, @hpxml_bldg) - surface.setName(floor.id) - if floor.is_interior - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - elsif floor.is_floor - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - if floor.exterior_adjacent_to == HPXML::LocationManufacturedHomeUnderBelly - foundation = @hpxml_bldg.foundations.find { |x| x.to_location == floor.exterior_adjacent_to } - if foundation.belly_wing_skirt_present - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - end - end - end - # Apply construction - - if floor.is_ceiling - if @apply_ashrae140_assumptions - # Attic floor - inside_film = Material.AirFilmFloorASHRAE140 - outside_film = Material.AirFilmFloorASHRAE140 - else - inside_film = Material.AirFilmFloorAverage - outside_film = Material.AirFilmFloorAverage - end - mat_int_finish_or_covering = Material.InteriorFinishMaterial(floor.interior_finish_type, floor.interior_finish_thickness) - has_radiant_barrier = floor.radiant_barrier - if has_radiant_barrier - radiant_barrier_grade = floor.radiant_barrier_grade - end - else # Floor - if @apply_ashrae140_assumptions - # Raised floor - inside_film = Material.AirFilmFloorASHRAE140 - outside_film = Material.AirFilmFloorZeroWindASHRAE140 - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - mat_int_finish_or_covering = Material.CoveringBare(1.0) - else - inside_film = Material.AirFilmFloorReduced - if floor.is_exterior - outside_film = Material.AirFilmOutside - else - outside_film = Material.AirFilmFloorReduced - end - if floor.interior_adjacent_to == HPXML::LocationConditionedSpace - mat_int_finish_or_covering = Material.CoveringBare - end - end - end - - Constructions.apply_floor_ceiling_construction(runner, model, [surface], floor.id, floor.floor_type, floor.is_ceiling, floor.insulation_assembly_r_value, - mat_int_finish_or_covering, has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade) - end + # Add combi-system EMS program with water use equipment information + Waterheater.apply_combi_system_EMS(model, hpxml_bldg.water_heating_systems, plantloop_map) end - # Adds any HPXML Foundation Walls and Slabs to the OpenStudio model. + # Adds HPXML Air Infiltration and HPXML HVAC Distribution to the OpenStudio model. + # TODO for adding more description (e.g., around checks and warnings) # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param hvac_data [Array] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects, HVAC unavailable period objects + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def add_foundation_walls_slabs(runner, model, weather, spaces) - foundation_types = @hpxml_bldg.slabs.map { |s| s.interior_adjacent_to }.uniq + def add_airflow(runner, model, weather, spaces, hpxml_bldg, hpxml_header, hvac_data, schedules_file) + cfa = hpxml_bldg.building_construction.conditioned_floor_area + airloop_map, hvac_unavailable_periods = hvac_data - foundation_types.each do |foundation_type| - # Get attached slabs/foundation walls - slabs = [] - @hpxml_bldg.slabs.each do |slab| - next unless slab.interior_adjacent_to == foundation_type + # Ducts + duct_systems = {} + hpxml_bldg.hvac_distributions.each do |hvac_distribution| + next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir - slabs << slab - slab.exposed_perimeter = [slab.exposed_perimeter, 1.0].max # minimum value to prevent error if no exposed slab - end + air_ducts = Airflow.create_ducts(model, hvac_distribution, spaces) + next if air_ducts.empty? - slabs.each do |slab| - slab_frac = slab.exposed_perimeter / slabs.map { |s| s.exposed_perimeter }.sum - ext_fnd_walls = slab.connected_foundation_walls.select { |fw| fw.net_area >= 1.0 && fw.is_exterior } + # Connect AirLoopHVACs to ducts + added_ducts = false + hvac_distribution.hvac_systems.each do |hvac_system| + next if airloop_map[hvac_system.id].nil? - if ext_fnd_walls.empty? - # Slab w/o foundation walls - add_foundation_slab(model, weather, spaces, slab, -1 * slab.depth_below_grade.to_f, slab.exposed_perimeter, nil) - else - # Slab w/ foundation walls - ext_fnd_walls_length = ext_fnd_walls.map { |fw| fw.area / fw.height }.sum - remaining_exposed_length = slab.exposed_perimeter - - # Since we don't know which FoundationWalls are adjacent to which Slabs, we apportion - # each FoundationWall to each slab. - ext_fnd_walls.each do |fnd_wall| - # Both the foundation wall and slab must have same exposed length to prevent Kiva errors. - # For the foundation wall, we are effectively modeling the net *exposed* area. - fnd_wall_length = fnd_wall.area / fnd_wall.height - apportioned_exposed_length = fnd_wall_length / ext_fnd_walls_length * slab.exposed_perimeter # Slab exposed perimeter apportioned to this foundation wall - apportioned_total_length = fnd_wall_length * slab_frac # Foundation wall length apportioned to this slab - exposed_length = [apportioned_exposed_length, apportioned_total_length].min - remaining_exposed_length -= exposed_length - - kiva_foundation = add_foundation_wall(runner, model, spaces, fnd_wall, exposed_length, fnd_wall_length) - add_foundation_slab(model, weather, spaces, slab, -1 * fnd_wall.depth_below_grade, exposed_length, kiva_foundation) - end - - if remaining_exposed_length > 1 # Skip if a small length (e.g., due to rounding) - # The slab's exposed perimeter exceeds the sum of attached exterior foundation wall lengths. - # This may legitimately occur for a walkout basement, where a portion of the slab has no - # adjacent foundation wall. - add_foundation_slab(model, weather, spaces, slab, 0, remaining_exposed_length, nil) - end + object = airloop_map[hvac_system.id] + if duct_systems[air_ducts].nil? + duct_systems[air_ducts] = object + added_ducts = true + elsif duct_systems[air_ducts] != object + # Multiple air loops associated with this duct system, treat + # as separate duct systems. + air_ducts2 = Airflow.create_ducts(model, hvac_distribution, spaces) + duct_systems[air_ducts2] = object + added_ducts = true end end - - # Interzonal foundation wall surfaces - # The above-grade portion of these walls are modeled as EnergyPlus surfaces with standard adjacency. - # The below-grade portion of these walls (in contact with ground) are not modeled, as Kiva does not - # calculate heat flow between two zones through the ground. - int_fnd_walls = @hpxml_bldg.foundation_walls.select { |fw| fw.is_interior && fw.interior_adjacent_to == foundation_type } - int_fnd_walls.each do |fnd_wall| - next unless fnd_wall.is_interior - - ag_height = fnd_wall.height - fnd_wall.depth_below_grade - ag_net_area = fnd_wall.net_area * ag_height / fnd_wall.height - next if ag_net_area < 1.0 - - length = ag_net_area / ag_height - z_origin = -1 * ag_height - if fnd_wall.azimuth.nil? - azimuth = @default_azimuths[0] # Arbitrary direction, doesn't receive exterior incident solar - else - azimuth = fnd_wall.azimuth - end - - vertices = Geometry.create_wall_vertices(length: length, height: ag_height, z_origin: z_origin, azimuth: azimuth) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('Length', length) - surface.additionalProperties.setFeature('Azimuth', azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'FoundationWall') - surface.setName(fnd_wall.id) - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, fnd_wall, @hpxml_bldg) - Geometry.set_surface_exterior(model, spaces, surface, fnd_wall, @hpxml_bldg) - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - - # Apply construction - - wall_type = HPXML::WallTypeConcrete - inside_film = Material.AirFilmVertical - outside_film = Material.AirFilmVertical - assembly_r = fnd_wall.insulation_assembly_r_value - mat_int_finish = Material.InteriorFinishMaterial(fnd_wall.interior_finish_type, fnd_wall.interior_finish_thickness) - if assembly_r.nil? - concrete_thick_in = fnd_wall.thickness - int_r = fnd_wall.insulation_interior_r_value - ext_r = fnd_wall.insulation_exterior_r_value - mat_concrete = Material.Concrete(concrete_thick_in) - mat_int_finish_rvalue = mat_int_finish.nil? ? 0.0 : mat_int_finish.rvalue - assembly_r = int_r + ext_r + mat_concrete.rvalue + mat_int_finish_rvalue + inside_film.rvalue + outside_film.rvalue - end - mat_ext_finish = nil - - Constructions.apply_wall_construction(runner, - model, - [surface], - fnd_wall.id, - wall_type, - assembly_r, - mat_int_finish, - false, - inside_film, - outside_film, - nil, - mat_ext_finish, - nil, - nil) + if not added_ducts + fail 'Unexpected error adding ducts to model.' end end - end - # Adds an HPXML Foundation Wall to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param foundation_wall [HPXML::FoundationWall] HPXML Foundation Wall object - # @param exposed_length [Double] TODO - # @param fnd_wall_length [Double] TODO - # @return [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object - def add_foundation_wall(runner, model, spaces, foundation_wall, exposed_length, fnd_wall_length) - exposed_fraction = exposed_length / fnd_wall_length - net_exposed_area = foundation_wall.net_area * exposed_fraction - gross_exposed_area = foundation_wall.area * exposed_fraction - height = foundation_wall.height - height_ag = height - foundation_wall.depth_below_grade - z_origin = -1 * foundation_wall.depth_below_grade - if foundation_wall.azimuth.nil? - azimuth = @default_azimuths[0] # Arbitrary; solar incidence in Kiva is applied as an orientation average (to the above grade portion of the wall) - else - azimuth = foundation_wall.azimuth - end + # Duct leakage to outside warnings? + # Need to check here instead of in schematron in case duct locations are defaulted + hpxml_bldg.hvac_distributions.each do |hvac_distribution| + next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir + next if hvac_distribution.duct_leakage_measurements.empty? - return if exposed_length < 0.1 # Avoid Kiva error if exposed wall length is too small + units = hvac_distribution.duct_leakage_measurements[0].duct_leakage_units + lto_measurements = hvac_distribution.duct_leakage_measurements.select { |dlm| dlm.duct_leakage_total_or_to_outside == HPXML::DuctLeakageToOutside } + sum_lto = lto_measurements.map { |dlm| dlm.duct_leakage_value }.sum(0.0) - if gross_exposed_area > net_exposed_area - # Create a "notch" in the wall to account for the subsurfaces. This ensures that - # we preserve the appropriate wall height, length, and area for Kiva. - subsurface_area = gross_exposed_area - net_exposed_area - else - subsurface_area = 0 - end + if hvac_distribution.ducts.select { |d| !HPXML::conditioned_locations_this_unit.include?(d.duct_location) }.size == 0 + # If ducts completely in conditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 5%) + issue_warning = false + if units == HPXML::UnitsCFM25 + issue_warning = true if sum_lto > 0.04 * cfa + elsif units == HPXML::UnitsCFM50 + issue_warning = true if sum_lto > 0.06 * cfa + elsif units == HPXML::UnitsPercent + issue_warning = true if sum_lto > 0.05 + end + next unless issue_warning - vertices = Geometry.create_wall_vertices(length: exposed_length, height: height, z_origin: z_origin, azimuth: azimuth, subsurface_area: subsurface_area) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('Length', exposed_length) - surface.additionalProperties.setFeature('Azimuth', azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'FoundationWall') - surface.setName(foundation_wall.id) - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, foundation_wall, @hpxml_bldg) - Geometry.set_surface_exterior(model, spaces, surface, foundation_wall, @hpxml_bldg) - - assembly_r = foundation_wall.insulation_assembly_r_value - mat_int_finish = Material.InteriorFinishMaterial(foundation_wall.interior_finish_type, foundation_wall.interior_finish_thickness) - mat_wall = Material.FoundationWallMaterial(foundation_wall.type, foundation_wall.thickness) - if not assembly_r.nil? - ext_rigid_height = height - ext_rigid_offset = 0.0 - inside_film = Material.AirFilmVertical - - mat_int_finish_rvalue = mat_int_finish.nil? ? 0.0 : mat_int_finish.rvalue - ext_rigid_r = assembly_r - mat_wall.rvalue - mat_int_finish_rvalue - inside_film.rvalue - int_rigid_r = 0.0 - if ext_rigid_r < 0 # Try without interior finish - mat_int_finish = nil - ext_rigid_r = assembly_r - mat_wall.rvalue - inside_film.rvalue - end - if (ext_rigid_r > 0) && (ext_rigid_r < 0.1) - ext_rigid_r = 0.0 # Prevent tiny strip of insulation - end - if ext_rigid_r < 0 - ext_rigid_r = 0.0 - match = false + runner.registerWarning('Ducts are entirely within conditioned space but there is moderate leakage to the outside. Leakage to the outside is typically zero or near-zero in these situations, consider revising leakage values. Leakage will be modeled as heat lost to the ambient environment.') else - match = true - end - else - ext_rigid_offset = foundation_wall.insulation_exterior_distance_to_top - ext_rigid_height = foundation_wall.insulation_exterior_distance_to_bottom - ext_rigid_offset - ext_rigid_r = foundation_wall.insulation_exterior_r_value - int_rigid_offset = foundation_wall.insulation_interior_distance_to_top - int_rigid_height = foundation_wall.insulation_interior_distance_to_bottom - int_rigid_offset - int_rigid_r = foundation_wall.insulation_interior_r_value - end - - soil_k_in = UnitConversions.convert(@hpxml_bldg.site.ground_conductivity, 'ft', 'in') - - Constructions.apply_foundation_wall(model, [surface], "#{foundation_wall.id} construction", - ext_rigid_offset, int_rigid_offset, ext_rigid_height, int_rigid_height, - ext_rigid_r, int_rigid_r, mat_int_finish, mat_wall, height_ag, - soil_k_in) + # If ducts in unconditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 40%) + issue_warning = false + if units == HPXML::UnitsCFM25 + issue_warning = true if sum_lto >= 0.32 * cfa + elsif units == HPXML::UnitsCFM50 + issue_warning = true if sum_lto >= 0.48 * cfa + elsif units == HPXML::UnitsPercent + issue_warning = true if sum_lto >= 0.4 + end + next unless issue_warning - if not assembly_r.nil? - Constructions.check_surface_assembly_rvalue(runner, [surface], inside_film, nil, assembly_r, match) + runner.registerWarning('Very high sum of supply + return duct leakage to the outside; double-check inputs.') + end end - return surface.adjacentFoundation.get - end + # Create HVAC availability sensor + hvac_availability_sensor = nil + if not hvac_unavailable_periods.empty? + avail_sch = ScheduleConstant.new(model, SchedulesFile::Columns[:HVAC].name, 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: hvac_unavailable_periods) - # Adds an HPXML Slab to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param slab [HPXML::Slab] HPXML Slab object - # @param z_origin [Double] The z-coordinate for which the slab is relative (ft) - # @param exposed_length [Double] TODO - # @param kiva_foundation [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object - # @return [nil] - def add_foundation_slab(model, weather, spaces, slab, z_origin, exposed_length, kiva_foundation) - exposed_fraction = exposed_length / slab.exposed_perimeter - slab_tot_perim = exposed_length - slab_area = slab.area * exposed_fraction - if slab_tot_perim**2 - 16.0 * slab_area <= 0 - # Cannot construct rectangle with this perimeter/area. Some of the - # perimeter is presumably not exposed, so bump up perimeter value. - slab_tot_perim = Math.sqrt(16.0 * slab_area) - end - sqrt_term = [slab_tot_perim**2 - 16.0 * slab_area, 0.0].max - slab_length = slab_tot_perim / 4.0 + Math.sqrt(sqrt_term) / 4.0 - slab_width = slab_tot_perim / 4.0 - Math.sqrt(sqrt_term) / 4.0 - - vertices = Geometry.create_floor_vertices(length: slab_length, width: slab_width, z_origin: z_origin, default_azimuths: @default_azimuths) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.setName(slab.id) - surface.setSurfaceType(EPlus::SurfaceTypeFloor) - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionFoundation) - surface.additionalProperties.setFeature('SurfaceType', 'Slab') - Geometry.set_surface_interior(model, spaces, surface, slab, @hpxml_bldg) - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - - slab_perim_r = slab.perimeter_insulation_r_value - slab_perim_depth = slab.perimeter_insulation_depth - if (slab_perim_r == 0) || (slab_perim_depth == 0) - slab_perim_r = 0 - slab_perim_depth = 0 + hvac_availability_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + hvac_availability_sensor.setName('hvac availability s') + hvac_availability_sensor.setKeyName(avail_sch.schedule.name.to_s) + hvac_availability_sensor.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeHVACAvailabilitySensor) end - if slab.under_slab_insulation_spans_entire_slab - slab_whole_r = slab.under_slab_insulation_r_value - slab_under_r = 0 - slab_under_width = 0 - else - slab_under_r = slab.under_slab_insulation_r_value - slab_under_width = slab.under_slab_insulation_width - if (slab_under_r == 0) || (slab_under_width == 0) - slab_under_r = 0 - slab_under_width = 0 - end - slab_whole_r = 0 - end - slab_gap_r = slab.gap_insulation_r_value - - mat_carpet = nil - if (slab.carpet_fraction > 0) && (slab.carpet_r_value > 0) - mat_carpet = Material.CoveringBare(slab.carpet_fraction, - slab.carpet_r_value) - end - soil_k_in = UnitConversions.convert(@hpxml_bldg.site.ground_conductivity, 'ft', 'in') - - ext_horiz_r = slab.exterior_horizontal_insulation_r_value - ext_horiz_width = slab.exterior_horizontal_insulation_width - ext_horiz_depth = slab.exterior_horizontal_insulation_depth_below_grade - - Constructions.apply_foundation_slab(model, surface, "#{slab.id} construction", - slab_under_r, slab_under_width, slab_gap_r, slab_perim_r, - slab_perim_depth, slab_whole_r, slab.thickness, - exposed_length, mat_carpet, soil_k_in, kiva_foundation, ext_horiz_r, ext_horiz_width, ext_horiz_depth) - - kiva_foundation = surface.adjacentFoundation.get - - foundation_walls_insulated = false - foundation_ceiling_insulated = false - @hpxml_bldg.foundation_walls.each do |fnd_wall| - next unless fnd_wall.interior_adjacent_to == slab.interior_adjacent_to - next unless fnd_wall.exterior_adjacent_to == HPXML::LocationGround - - if fnd_wall.insulation_assembly_r_value.to_f > 5 - foundation_walls_insulated = true - elsif fnd_wall.insulation_exterior_r_value.to_f + fnd_wall.insulation_interior_r_value.to_f > 0 - foundation_walls_insulated = true - end - end - @hpxml_bldg.floors.each do |floor| - next unless floor.interior_adjacent_to == HPXML::LocationConditionedSpace - next unless floor.exterior_adjacent_to == slab.interior_adjacent_to - - if floor.insulation_assembly_r_value > 5 - foundation_ceiling_insulated = true - end - end - - Constructions.apply_kiva_initial_temp(kiva_foundation, slab, weather, - spaces[HPXML::LocationConditionedSpace].thermalZone.get, - @hpxml_header.sim_begin_month, @hpxml_header.sim_begin_day, - @hpxml_header.sim_calendar_year, @schedules_file, - foundation_walls_insulated, foundation_ceiling_insulated) - - return kiva_foundation - end - - # Check if we need to add floors between conditioned spaces (e.g., between first - # and second story or conditioned basement ceiling). - # This ensures that the E+ reported Conditioned Floor Area is correct. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_conditioned_floor_area(model, spaces) - sum_cfa = 0.0 - @hpxml_bldg.floors.each do |floor| - next unless floor.is_floor - next unless [HPXML::LocationConditionedSpace, HPXML::LocationBasementConditioned].include?(floor.interior_adjacent_to) || - [HPXML::LocationConditionedSpace, HPXML::LocationBasementConditioned].include?(floor.exterior_adjacent_to) - - sum_cfa += floor.area - end - @hpxml_bldg.slabs.each do |slab| - next unless [HPXML::LocationConditionedSpace, HPXML::LocationBasementConditioned].include? slab.interior_adjacent_to - - sum_cfa += slab.area - end - - addtl_cfa = @cfa - sum_cfa - - fail if addtl_cfa < -1.0 # Allow some rounding; EPvalidator.xml should prevent this - - return unless addtl_cfa > 1.0 # Allow some rounding - - floor_width = Math::sqrt(addtl_cfa) - floor_length = addtl_cfa / floor_width - z_origin = @foundation_top + 8.0 * (@ncfl_ag - 1) - - # Add floor surface - vertices = Geometry.create_floor_vertices(length: floor_length, width: floor_width, z_origin: z_origin, default_azimuths: @default_azimuths) - floor_surface = OpenStudio::Model::Surface.new(vertices, model) - - floor_surface.setSunExposure(EPlus::SurfaceSunExposureNo) - floor_surface.setWindExposure(EPlus::SurfaceWindExposureNo) - floor_surface.setName('inferred conditioned floor') - floor_surface.setSurfaceType(EPlus::SurfaceTypeFloor) - floor_surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) - floor_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) - floor_surface.additionalProperties.setFeature('SurfaceType', 'InferredFloor') - floor_surface.additionalProperties.setFeature('Tilt', 0.0) - - # Add ceiling surface - vertices = Geometry.create_ceiling_vertices(length: floor_length, width: floor_width, z_origin: z_origin, default_azimuths: @default_azimuths) - ceiling_surface = OpenStudio::Model::Surface.new(vertices, model) - - ceiling_surface.setSunExposure(EPlus::SurfaceSunExposureNo) - ceiling_surface.setWindExposure(EPlus::SurfaceWindExposureNo) - ceiling_surface.setName('inferred conditioned ceiling') - ceiling_surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) - ceiling_surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) - ceiling_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) - ceiling_surface.additionalProperties.setFeature('SurfaceType', 'InferredCeiling') - ceiling_surface.additionalProperties.setFeature('Tilt', 0.0) - - # Apply Construction - Constructions.apply_adiabatic_construction(model, [floor_surface, ceiling_surface], 'floor') - end - - # Calls construction methods for applying partition walls and furniture to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_thermal_mass(model, spaces) - if @apply_ashrae140_assumptions - # 1024 ft2 of interior partition wall mass, no furniture mass - mat_int_finish = Material.InteriorFinishMaterial(HPXML::InteriorFinishGypsumBoard, 0.5) - partition_wall_area = 1024.0 * 2 # Exposed partition wall area (both sides) - Constructions.apply_partition_walls(model, 'PartitionWallConstruction', mat_int_finish, partition_wall_area, spaces) - else - mat_int_finish = Material.InteriorFinishMaterial(@hpxml_bldg.partition_wall_mass.interior_finish_type, @hpxml_bldg.partition_wall_mass.interior_finish_thickness) - partition_wall_area = @hpxml_bldg.partition_wall_mass.area_fraction * @cfa # Exposed partition wall area (both sides) - Constructions.apply_partition_walls(model, 'PartitionWallConstruction', mat_int_finish, partition_wall_area, spaces) - - Constructions.apply_furniture(model, @hpxml_bldg.furniture_mass, spaces) - end - end - - # Adds any HPXML Windows to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_windows(model, spaces) - # We already stored @fraction_of_windows_operable, so lets remove the - # fraction_operable properties from windows and re-collapse the enclosure - # so as to prevent potentially modeling multiple identical windows in E+, - # which can increase simulation runtime. - @hpxml_bldg.windows.each do |window| - window.fraction_operable = nil - end - @hpxml_bldg.collapse_enclosure_surfaces() - - shading_schedules = {} - - surfaces = [] - @hpxml_bldg.windows.each do |window| - window_height = 4.0 # ft, default - - overhang_depth = nil - if (not window.overhangs_depth.nil?) && (window.overhangs_depth > 0) - overhang_depth = window.overhangs_depth - overhang_distance_to_top = window.overhangs_distance_to_top_of_window - overhang_distance_to_bottom = window.overhangs_distance_to_bottom_of_window - window_height = overhang_distance_to_bottom - overhang_distance_to_top - end - - window_length = window.area / window_height - z_origin = @foundation_top - - ufactor, shgc = Constructions.get_ufactor_shgc_adjusted_by_storms(window.storm_type, window.ufactor, window.shgc) - - if window.is_exterior - - # Create parent surface slightly bigger than window - vertices = Geometry.create_wall_vertices(length: window_length, height: window_height, z_origin: z_origin, azimuth: window.azimuth, add_buffer: true) - surface = OpenStudio::Model::Surface.new(vertices, model) - - surface.additionalProperties.setFeature('Length', window_length) - surface.additionalProperties.setFeature('Azimuth', window.azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'Window') - surface.setName("surface #{window.id}") - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, window.wall, @hpxml_bldg) - - vertices = Geometry.create_wall_vertices(length: window_length, height: window_height, z_origin: z_origin, azimuth: window.azimuth) - sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) - sub_surface.setName(window.id) - sub_surface.setSurface(surface) - sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeWindow) - - Geometry.set_subsurface_exterior(surface, spaces, model, window.wall, @hpxml_bldg) - surfaces << surface - - if not overhang_depth.nil? - overhang = sub_surface.addOverhang(UnitConversions.convert(overhang_depth, 'ft', 'm'), UnitConversions.convert(overhang_distance_to_top, 'ft', 'm')) - overhang.get.setName("#{sub_surface.name} overhangs") - end - - # Apply construction - Constructions.apply_window(model, sub_surface, 'WindowConstruction', ufactor, shgc) - - # Apply interior/exterior shading (as needed) - Constructions.apply_window_skylight_shading(model, window, sub_surface, shading_schedules, @hpxml_header, @hpxml_bldg) - else - # Window is on an interior surface, which E+ does not allow. Model - # as a door instead so that we can get the appropriate conduction - # heat transfer; there is no solar gains anyway. - - # Create parent surface slightly bigger than window - vertices = Geometry.create_wall_vertices(length: window_length, height: window_height, z_origin: z_origin, azimuth: window.azimuth, add_buffer: true) - surface = OpenStudio::Model::Surface.new(vertices, model) - - surface.additionalProperties.setFeature('Length', window_length) - surface.additionalProperties.setFeature('Azimuth', window.azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'Door') - surface.setName("surface #{window.id}") - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, window.wall, @hpxml_bldg) - - vertices = Geometry.create_wall_vertices(length: window_length, height: window_height, z_origin: z_origin, azimuth: window.azimuth) - sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) - sub_surface.setName(window.id) - sub_surface.setSurface(surface) - sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeDoor) - - Geometry.set_subsurface_exterior(surface, spaces, model, window.wall, @hpxml_bldg) - surfaces << surface - - # Apply construction - inside_film = Material.AirFilmVertical - outside_film = Material.AirFilmVertical - Constructions.apply_door(model, [sub_surface], 'Window', ufactor, inside_film, outside_film) - end - end - - Constructions.apply_adiabatic_construction(model, surfaces, 'wall') - end - - # Adds any HPXML Skylights to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_skylights(model, spaces) - surfaces = [] - shading_schedules = {} - - @hpxml_bldg.skylights.each do |skylight| - if not skylight.is_conditioned - fail "Skylight '#{skylight.id}' not connected to conditioned space; if it's a skylight with a shaft, use AttachedToFloor to connect it to conditioned space." - end - - tilt = skylight.roof.pitch / 12.0 - width = Math::sqrt(skylight.area) - length = skylight.area / width - z_origin = @walls_top + 0.5 * Math.sin(Math.atan(tilt)) * width - - ufactor, shgc = Constructions.get_ufactor_shgc_adjusted_by_storms(skylight.storm_type, skylight.ufactor, skylight.shgc) - - if not skylight.curb_area.nil? - # Create parent surface that includes curb heat transfer - total_area = skylight.area + skylight.curb_area - total_width = Math::sqrt(total_area) - total_length = total_area / total_width - vertices = Geometry.create_roof_vertices(length: total_length, width: total_width, z_origin: z_origin, azimuth: skylight.azimuth, tilt: tilt, add_buffer: true) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('Length', total_length) - surface.additionalProperties.setFeature('Width', total_width) - - # Assign curb construction - curb_assembly_r_value = [skylight.curb_assembly_r_value - Material.AirFilmVertical.rvalue - Material.AirFilmOutside.rvalue, 0.1].max - curb_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Rough', UnitConversions.convert(curb_assembly_r_value, 'hr*ft^2*f/btu', 'm^2*k/w')) - curb_mat.setName('SkylightCurbMaterial') - curb_const = OpenStudio::Model::Construction.new(model) - curb_const.setName('SkylightCurbConstruction') - curb_const.insertLayer(0, curb_mat) - surface.setConstruction(curb_const) - else - # Create parent surface slightly bigger than skylight - vertices = Geometry.create_roof_vertices(length: length, width: width, z_origin: z_origin, azimuth: skylight.azimuth, tilt: tilt, add_buffer: true) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('Length', length) - surface.additionalProperties.setFeature('Width', width) - surfaces << surface # Add to surfaces list so it's assigned an adiabatic construction - end - surface.additionalProperties.setFeature('Azimuth', skylight.azimuth) - surface.additionalProperties.setFeature('Tilt', tilt) - surface.additionalProperties.setFeature('SurfaceType', 'Skylight') - surface.setName("surface #{skylight.id}") - surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) - surface.setSpace(Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, @hpxml_bldg)) - surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) # cannot be adiabatic because subsurfaces won't be created - - vertices = Geometry.create_roof_vertices(length: length, width: width, z_origin: z_origin, azimuth: skylight.azimuth, tilt: tilt) - sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) - sub_surface.setName(skylight.id) - sub_surface.setSurface(surface) - sub_surface.setSubSurfaceType('Skylight') - - # Apply construction - Constructions.apply_skylight(model, sub_surface, 'SkylightConstruction', ufactor, shgc) - - # Apply interior/exterior shading (as needed) - Constructions.apply_window_skylight_shading(model, skylight, sub_surface, shading_schedules, @hpxml_header, @hpxml_bldg) - - next unless (not skylight.shaft_area.nil?) && (not skylight.floor.nil?) - - # Add skylight shaft heat transfer, similar to attic knee walls - - shaft_height = Math::sqrt(skylight.shaft_area) - shaft_width = skylight.shaft_area / shaft_height - shaft_azimuth = @default_azimuths[0] # Arbitrary direction, doesn't receive exterior incident solar - shaft_z_origin = @walls_top - shaft_height - - vertices = Geometry.create_wall_vertices(length: shaft_width, height: shaft_height, z_origin: shaft_z_origin, azimuth: shaft_azimuth) - surface = OpenStudio::Model::Surface.new(vertices, model) - surface.additionalProperties.setFeature('Length', shaft_width) - surface.additionalProperties.setFeature('Width', shaft_height) - surface.additionalProperties.setFeature('Azimuth', shaft_azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'Skylight') - surface.setName("surface #{skylight.id} shaft") - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, skylight.floor, @hpxml_bldg) - Geometry.set_surface_exterior(model, spaces, surface, skylight.floor, @hpxml_bldg) - surface.setSunExposure(EPlus::SurfaceSunExposureNo) - surface.setWindExposure(EPlus::SurfaceWindExposureNo) - - # Apply construction - shaft_assembly_r_value = [skylight.shaft_assembly_r_value - 2 * Material.AirFilmVertical.rvalue, 0.1].max - shaft_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Rough', UnitConversions.convert(shaft_assembly_r_value, 'hr*ft^2*f/btu', 'm^2*k/w')) - shaft_mat.setName('SkylightShaftMaterial') - shaft_const = OpenStudio::Model::Construction.new(model) - shaft_const.setName('SkylightShaftConstruction') - shaft_const.insertLayer(0, shaft_mat) - surface.setConstruction(shaft_const) - end - - Constructions.apply_adiabatic_construction(model, surfaces, 'roof') - end - - # Adds any HPXML Doors to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_doors(model, spaces) - surfaces = [] - @hpxml_bldg.doors.each do |door| - door_height = 6.67 # ft - door_length = door.area / door_height - z_origin = @foundation_top - - # Create parent surface slightly bigger than door - vertices = Geometry.create_wall_vertices(length: door_length, height: door_height, z_origin: z_origin, azimuth: door.azimuth, add_buffer: true) - surface = OpenStudio::Model::Surface.new(vertices, model) - - surface.additionalProperties.setFeature('Length', door_length) - surface.additionalProperties.setFeature('Azimuth', door.azimuth) - surface.additionalProperties.setFeature('Tilt', 90.0) - surface.additionalProperties.setFeature('SurfaceType', 'Door') - surface.setName("surface #{door.id}") - surface.setSurfaceType(EPlus::SurfaceTypeWall) - Geometry.set_surface_interior(model, spaces, surface, door.wall, @hpxml_bldg) - - vertices = Geometry.create_wall_vertices(length: door_length, height: door_height, z_origin: z_origin, azimuth: door.azimuth) - sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) - sub_surface.setName(door.id) - sub_surface.setSurface(surface) - sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeDoor) - - Geometry.set_subsurface_exterior(surface, spaces, model, door.wall, @hpxml_bldg) - surfaces << surface - - # Apply construction - ufactor = 1.0 / door.r_value - inside_film = Material.AirFilmVertical - if door.wall.is_exterior - outside_film = Material.AirFilmOutside - else - outside_film = Material.AirFilmVertical - end - Constructions.apply_door(model, [sub_surface], 'Door', ufactor, inside_film, outside_film) - end - - Constructions.apply_adiabatic_construction(model, surfaces, 'wall') - end - - # Adds other geometry properties like space/zone volumes and re-positioned surface coordinates. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_geometry_other(model, spaces) - Geometry.set_zone_volumes(spaces: spaces, hpxml_bldg: @hpxml_bldg, apply_ashrae140_assumptions: @apply_ashrae140_assumptions) - Geometry.explode_surfaces(model: model, hpxml_bldg: @hpxml_bldg, walls_top: @walls_top) - end - - # First assign OpenStudio Space object for appliances based on HPXML Location. - # Then adds any of the following to the OpenStudio model: - # - HPXML Clothes Washers - # - HPXML Clothes Dryers - # - HPXML Dishwashers - # - HPXML Refrigerators - # - HPXML Freezers - # - HPXML Cooking Ranges / Ovens - # - HPXML Hot Water Distribution - # - HPXML Solar Thermal System - # - HPXML Water Heating Systems - # - HPXML Water Fixtures - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_hot_water_and_appliances(runner, model, weather, spaces) - # Assign spaces - @hpxml_bldg.clothes_washers.each do |clothes_washer| - clothes_washer.additional_properties.space = Geometry.get_space_from_location(clothes_washer.location, spaces) - end - @hpxml_bldg.clothes_dryers.each do |clothes_dryer| - clothes_dryer.additional_properties.space = Geometry.get_space_from_location(clothes_dryer.location, spaces) - end - @hpxml_bldg.dishwashers.each do |dishwasher| - dishwasher.additional_properties.space = Geometry.get_space_from_location(dishwasher.location, spaces) - end - @hpxml_bldg.refrigerators.each do |refrigerator| - loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(refrigerator.location, model, spaces) - refrigerator.additional_properties.loc_space = loc_space - refrigerator.additional_properties.loc_schedule = loc_schedule - end - @hpxml_bldg.freezers.each do |freezer| - loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(freezer.location, model, spaces) - freezer.additional_properties.loc_space = loc_space - freezer.additional_properties.loc_schedule = loc_schedule - end - @hpxml_bldg.cooking_ranges.each do |cooking_range| - cooking_range.additional_properties.space = Geometry.get_space_from_location(cooking_range.location, spaces) - end - - # Distribution - if @hpxml_bldg.water_heating_systems.size > 0 - hot_water_distribution = @hpxml_bldg.hot_water_distributions[0] - end - - # Solar thermal system - solar_thermal_system = nil - if @hpxml_bldg.solar_thermal_systems.size > 0 - solar_thermal_system = @hpxml_bldg.solar_thermal_systems[0] - end - - # Water Heater - unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:WaterHeater].name, @hpxml_header.unavailable_periods) - unit_multiplier = @hpxml_bldg.building_construction.number_of_units - has_uncond_bsmnt = @hpxml_bldg.has_location(HPXML::LocationBasementUnconditioned) - has_cond_bsmnt = @hpxml_bldg.has_location(HPXML::LocationBasementConditioned) - plantloop_map = {} - @hpxml_bldg.water_heating_systems.each do |water_heating_system| - loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) - - ec_adj = HotWaterAndAppliances.get_dist_energy_consumption_adjustment(has_uncond_bsmnt, has_cond_bsmnt, @cfa, @ncfl, water_heating_system, hot_water_distribution) - - sys_id = water_heating_system.id - if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeStorage - plantloop_map[sys_id] = Waterheater.apply_tank(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, @eri_version, @schedules_file, unavailable_periods, unit_multiplier, @nbeds) - elsif water_heating_system.water_heater_type == HPXML::WaterHeaterTypeTankless - plantloop_map[sys_id] = Waterheater.apply_tankless(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, @eri_version, @schedules_file, unavailable_periods, unit_multiplier, @nbeds) - elsif water_heating_system.water_heater_type == HPXML::WaterHeaterTypeHeatPump - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - plantloop_map[sys_id] = Waterheater.apply_heatpump(model, runner, loc_space, loc_schedule, @hpxml_bldg.elevation, water_heating_system, ec_adj, solar_thermal_system, conditioned_zone, @eri_version, @schedules_file, unavailable_periods, unit_multiplier, @nbeds) - elsif [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? water_heating_system.water_heater_type - plantloop_map[sys_id] = Waterheater.apply_combi(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, @eri_version, @schedules_file, unavailable_periods, unit_multiplier, @nbeds) - else - fail "Unhandled water heater (#{water_heating_system.water_heater_type})." - end - end - - # Hot water fixtures and appliances - HotWaterAndAppliances.apply(model, runner, @hpxml_header, @hpxml_bldg, weather, spaces, hot_water_distribution, - solar_thermal_system, @eri_version, @schedules_file, plantloop_map, - @hpxml_header.unavailable_periods, @hpxml_bldg.building_construction.number_of_units) - - if (not solar_thermal_system.nil?) && (not solar_thermal_system.collector_area.nil?) # Detailed solar water heater - loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) - Waterheater.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_system, plantloop_map, unit_multiplier) - end - - # Add combi-system EMS program with water use equipment information - Waterheater.apply_combi_system_EMS(model, @hpxml_bldg.water_heating_systems, plantloop_map) - end - - # Adds any HPXML Cooling Systems to the OpenStudio model. - # TODO for adding more description (e.g., around sequential load fractions) - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects - # @return [nil] - def add_cooling_system(model, runner, weather, spaces, airloop_map) - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - - HVAC.get_hpxml_hvac_systems(@hpxml_bldg).each do |hvac_system| - next if hvac_system[:cooling].nil? - next unless hvac_system[:cooling].is_a? HPXML::CoolingSystem - - cooling_system = hvac_system[:cooling] - heating_system = hvac_system[:heating] - - HVAC.check_distribution_system(cooling_system.distribution_system, cooling_system.cooling_system_type) - - # Calculate cooling sequential load fractions - sequential_cool_load_fracs = HVAC.calc_sequential_load_fractions(cooling_system.fraction_cool_load_served.to_f, @remaining_cool_load_frac, @cooling_days) - @remaining_cool_load_frac -= cooling_system.fraction_cool_load_served.to_f - - # Calculate heating sequential load fractions - if not heating_system.nil? - sequential_heat_load_fracs = HVAC.calc_sequential_load_fractions(heating_system.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= heating_system.fraction_heat_load_served - elsif cooling_system.has_integrated_heating - sequential_heat_load_fracs = HVAC.calc_sequential_load_fractions(cooling_system.integrated_heating_system_fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= cooling_system.integrated_heating_system_fraction_heat_load_served - else - sequential_heat_load_fracs = [0] - end - - sys_id = cooling_system.id - if [HPXML::HVACTypeCentralAirConditioner, - HPXML::HVACTypeRoomAirConditioner, - HPXML::HVACTypeMiniSplitAirConditioner, - HPXML::HVACTypePTAC].include? cooling_system.cooling_system_type - - airloop_map[sys_id] = HVAC.apply_air_source_hvac_systems(model, runner, cooling_system, heating_system, sequential_cool_load_fracs, sequential_heat_load_fracs, - weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, - conditioned_zone, @hvac_unavailable_periods, @schedules_file, @hpxml_bldg, - @hpxml_header) - - elsif [HPXML::HVACTypeEvaporativeCooler].include? cooling_system.cooling_system_type - - airloop_map[sys_id] = HVAC.apply_evaporative_cooler(model, cooling_system, sequential_cool_load_fracs, - conditioned_zone, @hvac_unavailable_periods, - @hpxml_bldg.building_construction.number_of_units) - end - end - end - - # Adds any HPXML Heating Systems to the OpenStudio model. - # TODO for adding more description (e.g., around sequential load fractions) - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects - # @return [nil] - def add_heating_system(runner, model, weather, spaces, airloop_map) - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - - HVAC.get_hpxml_hvac_systems(@hpxml_bldg).each do |hvac_system| - next if hvac_system[:heating].nil? - next unless hvac_system[:heating].is_a? HPXML::HeatingSystem - - cooling_system = hvac_system[:cooling] - heating_system = hvac_system[:heating] - - HVAC.check_distribution_system(heating_system.distribution_system, heating_system.heating_system_type) - - if (heating_system.heating_system_type == HPXML::HVACTypeFurnace) && (not cooling_system.nil?) - next # Already processed combined AC+furnace - end - - # Calculate heating sequential load fractions - if heating_system.is_heat_pump_backup_system - # Heating system will be last in the EquipmentList and should meet entirety of - # remaining load during the heating season. - sequential_heat_load_fracs = @heating_days.map(&:to_f) - if not heating_system.fraction_heat_load_served.nil? - fail 'Heat pump backup system cannot have a fraction heat load served specified.' - end - else - sequential_heat_load_fracs = HVAC.calc_sequential_load_fractions(heating_system.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= heating_system.fraction_heat_load_served - end - - sys_id = heating_system.id - if [HPXML::HVACTypeFurnace].include? heating_system.heating_system_type - - airloop_map[sys_id] = HVAC.apply_air_source_hvac_systems(model, runner, nil, heating_system, [0], sequential_heat_load_fracs, - weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, - conditioned_zone, @hvac_unavailable_periods, @schedules_file, @hpxml_bldg, - @hpxml_header) - - elsif [HPXML::HVACTypeBoiler].include? heating_system.heating_system_type - - airloop_map[sys_id] = HVAC.apply_boiler(model, runner, heating_system, sequential_heat_load_fracs, conditioned_zone, - @hvac_unavailable_periods) - - elsif [HPXML::HVACTypeElectricResistance].include? heating_system.heating_system_type - - HVAC.apply_electric_baseboard(model, heating_system, - sequential_heat_load_fracs, conditioned_zone, @hvac_unavailable_periods) - - elsif [HPXML::HVACTypeStove, - HPXML::HVACTypeSpaceHeater, - HPXML::HVACTypeWallFurnace, - HPXML::HVACTypeFloorFurnace, - HPXML::HVACTypeFireplace].include? heating_system.heating_system_type - - HVAC.apply_unit_heater(model, heating_system, - sequential_heat_load_fracs, conditioned_zone, @hvac_unavailable_periods) - end - - next unless heating_system.is_heat_pump_backup_system - - # Store OS object for later use - equipment_list = model.getZoneHVACEquipmentLists.find { |el| el.thermalZone == conditioned_zone } - @heat_pump_backup_system_object = equipment_list.equipment[-1] - end - end - - # Adds any HPXML Heat Pumps to the OpenStudio model. - # TODO for adding more description (e.g., around sequential load fractions) - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects - # @return [nil] - def add_heat_pump(runner, model, weather, spaces, airloop_map) - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - - HVAC.get_hpxml_hvac_systems(@hpxml_bldg).each do |hvac_system| - next if hvac_system[:cooling].nil? - next unless hvac_system[:cooling].is_a? HPXML::HeatPump - - heat_pump = hvac_system[:cooling] - - HVAC.check_distribution_system(heat_pump.distribution_system, heat_pump.heat_pump_type) - - # Calculate heating sequential load fractions - sequential_heat_load_fracs = HVAC.calc_sequential_load_fractions(heat_pump.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= heat_pump.fraction_heat_load_served - - # Calculate cooling sequential load fractions - sequential_cool_load_fracs = HVAC.calc_sequential_load_fractions(heat_pump.fraction_cool_load_served, @remaining_cool_load_frac, @cooling_days) - @remaining_cool_load_frac -= heat_pump.fraction_cool_load_served - - sys_id = heat_pump.id - if [HPXML::HVACTypeHeatPumpWaterLoopToAir].include? heat_pump.heat_pump_type - - airloop_map[sys_id] = HVAC.apply_water_loop_to_air_heat_pump(model, heat_pump, - sequential_heat_load_fracs, sequential_cool_load_fracs, - conditioned_zone, @hvac_unavailable_periods) - elsif [HPXML::HVACTypeHeatPumpAirToAir, - HPXML::HVACTypeHeatPumpMiniSplit, - HPXML::HVACTypeHeatPumpPTHP, - HPXML::HVACTypeHeatPumpRoom].include? heat_pump.heat_pump_type - airloop_map[sys_id] = HVAC.apply_air_source_hvac_systems(model, runner, heat_pump, heat_pump, sequential_cool_load_fracs, sequential_heat_load_fracs, - weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, - conditioned_zone, @hvac_unavailable_periods, @schedules_file, @hpxml_bldg, - @hpxml_header) - elsif [HPXML::HVACTypeHeatPumpGroundToAir].include? heat_pump.heat_pump_type - - airloop_map[sys_id] = HVAC.apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, - sequential_heat_load_fracs, sequential_cool_load_fracs, - conditioned_zone, @hpxml_bldg.site.ground_conductivity, @hpxml_bldg.site.ground_diffusivity, - @hvac_unavailable_periods, @hpxml_bldg.building_construction.number_of_units) - - end - - next if heat_pump.backup_system.nil? - - equipment_list = model.getZoneHVACEquipmentLists.find { |el| el.thermalZone == conditioned_zone } - - # Set priority to be last (i.e., after the heat pump that it is backup for) - equipment_list.setHeatingPriority(@heat_pump_backup_system_object, 99) - equipment_list.setCoolingPriority(@heat_pump_backup_system_object, 99) - end - end - - # Adds an ideal air system as needed to meet the load under certain circumstances: - # 1. the sum of fractions load served is less than 1 and greater than 0 (e.g., room ACs serving a portion of the home's load), - # in which case we need the ideal system to help fully condition the thermal zone to prevent incorrect heat transfers, or - # 2. ASHRAE 140 tests where we need heating/cooling loads. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param weather [WeatherFile] Weather object containing EPW information - # @return [nil] - def add_ideal_system(model, spaces, weather) - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - - if @apply_ashrae140_assumptions && (@hpxml_bldg.total_fraction_heat_load_served + @hpxml_bldg.total_fraction_heat_load_served == 0.0) - cooling_load_frac = 1.0 - heating_load_frac = 1.0 - if @apply_ashrae140_assumptions - if weather.header.StateProvinceRegion.downcase == 'co' - cooling_load_frac = 0.0 - elsif weather.header.StateProvinceRegion.downcase == 'nv' - heating_load_frac = 0.0 - else - fail 'Unexpected weather file for ASHRAE 140 run.' - end - end - HVAC.apply_ideal_air_loads(model, [cooling_load_frac], [heating_load_frac], - conditioned_zone, @hvac_unavailable_periods) - return - end - - if (@hpxml_bldg.total_fraction_heat_load_served < 1.0) && (@hpxml_bldg.total_fraction_heat_load_served > 0.0) - sequential_heat_load_fracs = HVAC.calc_sequential_load_fractions(@remaining_heat_load_frac - @hpxml_bldg.total_fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= (1.0 - @hpxml_bldg.total_fraction_heat_load_served) - else - sequential_heat_load_fracs = [0.0] - end - - if (@hpxml_bldg.total_fraction_cool_load_served < 1.0) && (@hpxml_bldg.total_fraction_cool_load_served > 0.0) - sequential_cool_load_fracs = HVAC.calc_sequential_load_fractions(@remaining_cool_load_frac - @hpxml_bldg.total_fraction_cool_load_served, @remaining_cool_load_frac, @cooling_days) - @remaining_cool_load_frac -= (1.0 - @hpxml_bldg.total_fraction_cool_load_served) - else - sequential_cool_load_fracs = [0.0] - end - - if (sequential_heat_load_fracs.sum > 0.0) || (sequential_cool_load_fracs.sum > 0.0) - HVAC.apply_ideal_air_loads(model, sequential_cool_load_fracs, sequential_heat_load_fracs, - conditioned_zone, @hvac_unavailable_periods) - end - end - - # Adds an HPXML HVAC Control to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_setpoints(runner, model, weather, spaces) - return if @hpxml_bldg.hvac_controls.size == 0 - - hvac_control = @hpxml_bldg.hvac_controls[0] - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - has_ceiling_fan = (@hpxml_bldg.ceiling_fans.size > 0) - - HVAC.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone, has_ceiling_fan, @heating_days, @cooling_days, @hpxml_header, @schedules_file) - end - - # Adds an HPXML Ceiling Fan to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_ceiling_fans(runner, model, weather, spaces) - return if @hpxml_bldg.ceiling_fans.size == 0 - - ceiling_fan = @hpxml_bldg.ceiling_fans[0] - HVAC.apply_ceiling_fans(model, runner, weather, ceiling_fan, spaces[HPXML::LocationConditionedSpace], - @schedules_file, @hpxml_header.unavailable_periods) - end - - # Adds any HPXML Dehumidifiers to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_dehumidifiers(runner, model, spaces) - return if @hpxml_bldg.dehumidifiers.size == 0 - - HVAC.apply_dehumidifiers(runner, model, @hpxml_bldg.dehumidifiers, spaces[HPXML::LocationConditionedSpace], @hpxml_header.unavailable_periods, - @hpxml_bldg.building_construction.number_of_units) - end - - # Adds any HPXML Plug Loads to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_mels(runner, model, spaces) - # Misc - @hpxml_bldg.plug_loads.each do |plug_load| - if plug_load.plug_load_type == HPXML::PlugLoadTypeOther - obj_name = Constants::ObjectTypeMiscPlugLoads - elsif plug_load.plug_load_type == HPXML::PlugLoadTypeTelevision - obj_name = Constants::ObjectTypeMiscTelevision - elsif plug_load.plug_load_type == HPXML::PlugLoadTypeElectricVehicleCharging - obj_name = Constants::ObjectTypeMiscElectricVehicleCharging - elsif plug_load.plug_load_type == HPXML::PlugLoadTypeWellPump - obj_name = Constants::ObjectTypeMiscWellPump - end - if obj_name.nil? - runner.registerWarning("Unexpected plug load type '#{plug_load.plug_load_type}'. The plug load will not be modeled.") - next - end - - MiscLoads.apply_plug(model, runner, plug_load, obj_name, spaces[HPXML::LocationConditionedSpace], @apply_ashrae140_assumptions, - @schedules_file, @hpxml_header.unavailable_periods) - end - end - - # Adds any HPXML Fuel Loads to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_mfls(runner, model, spaces) - # Misc - @hpxml_bldg.fuel_loads.each do |fuel_load| - if fuel_load.fuel_load_type == HPXML::FuelLoadTypeGrill - obj_name = Constants::ObjectTypeMiscGrill - elsif fuel_load.fuel_load_type == HPXML::FuelLoadTypeLighting - obj_name = Constants::ObjectTypeMiscLighting - elsif fuel_load.fuel_load_type == HPXML::FuelLoadTypeFireplace - obj_name = Constants::ObjectTypeMiscFireplace - end - if obj_name.nil? - runner.registerWarning("Unexpected fuel load type '#{fuel_load.fuel_load_type}'. The fuel load will not be modeled.") - next - end - - MiscLoads.apply_fuel(model, runner, fuel_load, obj_name, spaces[HPXML::LocationConditionedSpace], - @schedules_file, @hpxml_header.unavailable_periods) - end - end - - # Adds any HPXML Lighting Groups and Lighting to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_lighting(runner, model, spaces) - Lighting.apply(runner, model, spaces, @hpxml_bldg.lighting_groups, @hpxml_bldg.lighting, @eri_version, - @schedules_file, @cfa, @hpxml_header.unavailable_periods, @hpxml_bldg.building_construction.number_of_units) - end - - # Adds any HPXML Pools and Permanent Spas to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_pools_and_permanent_spas(runner, model, spaces) - (@hpxml_bldg.pools + @hpxml_bldg.permanent_spas).each do |pool_or_spa| - next if pool_or_spa.type == HPXML::TypeNone - - MiscLoads.apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, spaces[HPXML::LocationConditionedSpace], - @schedules_file, @hpxml_header.unavailable_periods) - next if pool_or_spa.pump_type == HPXML::TypeNone - - MiscLoads.apply_pool_or_permanent_spa_pump(runner, model, pool_or_spa, spaces[HPXML::LocationConditionedSpace], - @schedules_file, @hpxml_header.unavailable_periods) - end - end - - # Adds HPXML Air Infiltration and HPXML HVAC Distribution to the OpenStudio model. - # TODO for adding more description (e.g., around checks and warnings) - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects - # @return [nil] - def add_airflow(runner, model, weather, spaces, airloop_map) - # Ducts - duct_systems = {} - @hpxml_bldg.hvac_distributions.each do |hvac_distribution| - next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir - - air_ducts = Airflow.create_ducts(model, hvac_distribution, spaces) - next if air_ducts.empty? - - # Connect AirLoopHVACs to ducts - added_ducts = false - hvac_distribution.hvac_systems.each do |hvac_system| - next if airloop_map[hvac_system.id].nil? - - object = airloop_map[hvac_system.id] - if duct_systems[air_ducts].nil? - duct_systems[air_ducts] = object - added_ducts = true - elsif duct_systems[air_ducts] != object - # Multiple air loops associated with this duct system, treat - # as separate duct systems. - air_ducts2 = Airflow.create_ducts(model, hvac_distribution, spaces) - duct_systems[air_ducts2] = object - added_ducts = true - end - end - if not added_ducts - fail 'Unexpected error adding ducts to model.' - end - end - - # Duct leakage to outside warnings? - # Need to check here instead of in schematron in case duct locations are defaulted - @hpxml_bldg.hvac_distributions.each do |hvac_distribution| - next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir - next if hvac_distribution.duct_leakage_measurements.empty? - - units = hvac_distribution.duct_leakage_measurements[0].duct_leakage_units - lto_measurements = hvac_distribution.duct_leakage_measurements.select { |dlm| dlm.duct_leakage_total_or_to_outside == HPXML::DuctLeakageToOutside } - sum_lto = lto_measurements.map { |dlm| dlm.duct_leakage_value }.sum(0.0) - - if hvac_distribution.ducts.select { |d| !HPXML::conditioned_locations_this_unit.include?(d.duct_location) }.size == 0 - # If ducts completely in conditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 5%) - issue_warning = false - if units == HPXML::UnitsCFM25 - issue_warning = true if sum_lto > 0.04 * @cfa - elsif units == HPXML::UnitsCFM50 - issue_warning = true if sum_lto > 0.06 * @cfa - elsif units == HPXML::UnitsPercent - issue_warning = true if sum_lto > 0.05 - end - next unless issue_warning - - runner.registerWarning('Ducts are entirely within conditioned space but there is moderate leakage to the outside. Leakage to the outside is typically zero or near-zero in these situations, consider revising leakage values. Leakage will be modeled as heat lost to the ambient environment.') - else - # If ducts in unconditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 40%) - issue_warning = false - if units == HPXML::UnitsCFM25 - issue_warning = true if sum_lto >= 0.32 * @cfa - elsif units == HPXML::UnitsCFM50 - issue_warning = true if sum_lto >= 0.48 * @cfa - elsif units == HPXML::UnitsPercent - issue_warning = true if sum_lto >= 0.4 - end - next unless issue_warning - - runner.registerWarning('Very high sum of supply + return duct leakage to the outside; double-check inputs.') - end - end - - # Create HVAC availability sensor - hvac_availability_sensor = nil - if not @hvac_unavailable_periods.empty? - avail_sch = ScheduleConstant.new(model, SchedulesFile::Columns[:HVAC].name, 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: @hvac_unavailable_periods) - - hvac_availability_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - hvac_availability_sensor.setName('hvac availability s') - hvac_availability_sensor.setKeyName(avail_sch.schedule.name.to_s) - hvac_availability_sensor.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeHVACAvailabilitySensor) - end - - Airflow.apply(model, runner, weather, spaces, @hpxml_header, @hpxml_bldg, @cfa, - @ncfl_ag, duct_systems, airloop_map, @eri_version, - @frac_windows_operable, @apply_ashrae140_assumptions, @schedules_file, - @hpxml_header.unavailable_periods, hvac_availability_sensor) - end - - # Adds any HPXML Photovoltaics to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [nil] - def add_photovoltaics(model) - @hpxml_bldg.pv_systems.each do |pv_system| - next if pv_system.inverter.inverter_efficiency == @hpxml_bldg.pv_systems[0].inverter.inverter_efficiency - - fail 'Expected all InverterEfficiency values to be equal.' - end - @hpxml_bldg.pv_systems.each do |pv_system| - PV.apply(model, @nbeds, pv_system, @hpxml_bldg.building_construction.number_of_units) - end - end - - # Adds any HPXML Generators to the OpenStudio model. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [nil] - def add_generators(model) - @hpxml_bldg.generators.each do |generator| - Generator.apply(model, @nbeds, generator, @hpxml_bldg.building_construction.number_of_units) - end - end - - # Adds any HPXML Batteries to the OpenStudio model. - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @return [nil] - def add_batteries(runner, model, spaces) - @hpxml_bldg.batteries.each do |battery| - # Assign space - battery.additional_properties.space = Geometry.get_space_from_location(battery.location, spaces) - Battery.apply(runner, model, @nbeds, @hpxml_bldg.pv_systems, battery, @schedules_file, @hpxml_bldg.building_construction.number_of_units) - end - end - - # Store the HPXML Building object unit number for use in reporting measure. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param unit_number [Integer] index number corresponding to an HPXML Building object - # @return [nil] - def add_building_unit(model, unit_num) - return if unit_num.nil? - - unit = OpenStudio::Model::BuildingUnit.new(model) - unit.additionalProperties.setFeature('unit_num', unit_num) - model.getSpaces.each do |s| - s.setBuildingUnit(unit) - end - end - - # Store some data for use in reporting measure. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml [HPXML] HPXML object - # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @param hpxml_path [String] Path to the HPXML file - # @param building_id [String] HPXML Building ID - # @param hpxml_defaults_path [TODO] TODO - # @return [nil] - def add_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_id, hpxml_defaults_path) - additionalProperties = model.getBuilding.additionalProperties - additionalProperties.setFeature('hpxml_path', hpxml_path) - additionalProperties.setFeature('hpxml_defaults_path', hpxml_defaults_path) - additionalProperties.setFeature('building_id', building_id.to_s) - additionalProperties.setFeature('emissions_scenario_names', hpxml.header.emissions_scenarios.map { |s| s.name }.to_s) - additionalProperties.setFeature('emissions_scenario_types', hpxml.header.emissions_scenarios.map { |s| s.emissions_type }.to_s) - heated_zones, cooled_zones = [], [] - hpxml_osm_map.each do |hpxml_bldg, unit_model| - conditioned_zone_name = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace }.name.to_s - - heated_zones << conditioned_zone_name if hpxml_bldg.total_fraction_heat_load_served > 0 - cooled_zones << conditioned_zone_name if hpxml_bldg.total_fraction_cool_load_served > 0 - end - additionalProperties.setFeature('heated_zones', heated_zones.to_s) - additionalProperties.setFeature('cooled_zones', cooled_zones.to_s) - additionalProperties.setFeature('is_southern_hemisphere', hpxml_osm_map.keys[0].latitude < 0) - end - - # We do our own unmet hours calculation via EMS so that we can incorporate, - # e.g., heating/cooling seasons into the logic. The calculation layers on top - # of the built-in EnergyPlus unmet hours output. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @param hpxml [HPXML] HPXML object - # @return [Hash] TODO - def add_unmet_hours_output(model, hpxml_osm_map, hpxml) - # Create sensors and gather data - htg_sensors, clg_sensors = {}, {} - zone_air_temp_sensors, htg_spt_sensors, clg_spt_sensors = {}, {}, {} - total_heat_load_serveds, total_cool_load_serveds = {}, {} - season_day_nums = {} - onoff_deadbands = hpxml.header.hvac_onoff_thermostat_deadband.to_f - hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit| - conditioned_zone = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace } - conditioned_zone_name = conditioned_zone.name.to_s - - # EMS sensors - htg_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Heating Setpoint Not Met Time') - htg_sensors[unit].setName("#{conditioned_zone_name} htg unmet s") - htg_sensors[unit].setKeyName(conditioned_zone_name) - - clg_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Cooling Setpoint Not Met Time') - clg_sensors[unit].setName("#{conditioned_zone_name} clg unmet s") - clg_sensors[unit].setKeyName(conditioned_zone_name) - - total_heat_load_serveds[unit] = hpxml_bldg.total_fraction_heat_load_served - total_cool_load_serveds[unit] = hpxml_bldg.total_fraction_cool_load_served - - hvac_control = hpxml_bldg.hvac_controls[0] - next if hvac_control.nil? - - if (onoff_deadbands > 0) - zone_air_temp_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Temperature') - zone_air_temp_sensors[unit].setName("#{conditioned_zone_name} space temp") - zone_air_temp_sensors[unit].setKeyName(conditioned_zone_name) - - htg_sch = conditioned_zone.thermostatSetpointDualSetpoint.get.heatingSetpointTemperatureSchedule.get - htg_spt_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - htg_spt_sensors[unit].setName("#{htg_sch.name} sch value") - htg_spt_sensors[unit].setKeyName(htg_sch.name.to_s) - - clg_sch = conditioned_zone.thermostatSetpointDualSetpoint.get.coolingSetpointTemperatureSchedule.get - clg_spt_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - clg_spt_sensors[unit].setName("#{clg_sch.name} sch value") - clg_spt_sensors[unit].setKeyName(clg_sch.name.to_s) - end - - sim_year = @hpxml_header.sim_calendar_year - season_day_nums[unit] = { - htg_start: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_heating_begin_month, hvac_control.seasons_heating_begin_day), - htg_end: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_heating_end_month, hvac_control.seasons_heating_end_day), - clg_start: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_cooling_begin_month, hvac_control.seasons_cooling_begin_day), - clg_end: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_cooling_end_month, hvac_control.seasons_cooling_end_day) - } - end - - hvac_availability_sensor = model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeHVACAvailabilitySensor } - - # EMS program - clg_hrs = 'clg_unmet_hours' - htg_hrs = 'htg_unmet_hours' - unit_clg_hrs = 'unit_clg_unmet_hours' - unit_htg_hrs = 'unit_htg_unmet_hours' - program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) - program.setName('unmet hours program') - program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeUnmetHoursProgram) - program.addLine("Set #{htg_hrs} = 0") - program.addLine("Set #{clg_hrs} = 0") - for unit in 0..hpxml_osm_map.size - 1 - if total_heat_load_serveds[unit] > 0 - program.addLine("Set #{unit_htg_hrs} = 0") - if season_day_nums[unit][:htg_end] >= season_day_nums[unit][:htg_start] - line = "If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) && (DayOfYear <= #{season_day_nums[unit][:htg_end]}))" - else - line = "If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) || (DayOfYear <= #{season_day_nums[unit][:htg_end]}))" - end - line += " && (#{hvac_availability_sensor.name} == 1)" if not hvac_availability_sensor.nil? - program.addLine(line) - if zone_air_temp_sensors.keys.include? unit # on off deadband - program.addLine(" If #{zone_air_temp_sensors[unit].name} < (#{htg_spt_sensors[unit].name} - #{UnitConversions.convert(onoff_deadbands, 'deltaF', 'deltaC')})") - program.addLine(" Set #{unit_htg_hrs} = #{unit_htg_hrs} + #{htg_sensors[unit].name}") - program.addLine(' EndIf') - else - program.addLine(" Set #{unit_htg_hrs} = #{unit_htg_hrs} + #{htg_sensors[unit].name}") - end - program.addLine(" If #{unit_htg_hrs} > #{htg_hrs}") # Use max hourly value across all units - program.addLine(" Set #{htg_hrs} = #{unit_htg_hrs}") - program.addLine(' EndIf') - program.addLine('EndIf') - end - next unless total_cool_load_serveds[unit] > 0 - - program.addLine("Set #{unit_clg_hrs} = 0") - if season_day_nums[unit][:clg_end] >= season_day_nums[unit][:clg_start] - line = "If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) && (DayOfYear <= #{season_day_nums[unit][:clg_end]}))" - else - line = "If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) || (DayOfYear <= #{season_day_nums[unit][:clg_end]}))" - end - line += " && (#{hvac_availability_sensor.name} == 1)" if not hvac_availability_sensor.nil? - program.addLine(line) - if zone_air_temp_sensors.keys.include? unit # on off deadband - program.addLine(" If #{zone_air_temp_sensors[unit].name} > (#{clg_spt_sensors[unit].name} + #{UnitConversions.convert(onoff_deadbands, 'deltaF', 'deltaC')})") - program.addLine(" Set #{unit_clg_hrs} = #{unit_clg_hrs} + #{clg_sensors[unit].name}") - program.addLine(' EndIf') - else - program.addLine(" Set #{unit_clg_hrs} = #{unit_clg_hrs} + #{clg_sensors[unit].name}") - end - program.addLine(" If #{unit_clg_hrs} > #{clg_hrs}") # Use max hourly value across all units - program.addLine(" Set #{clg_hrs} = #{unit_clg_hrs}") - program.addLine(' EndIf') - program.addLine('EndIf') - end - - # EMS calling manager - program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - program_calling_manager.setName("#{program.name} calling manager") - program_calling_manager.setCallingPoint('EndOfZoneTimestepBeforeZoneReporting') - program_calling_manager.addProgram(program) - - return season_day_nums - end - - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @return [TODO] TODO - def add_total_loads_output(model, hpxml_osm_map) - # Create sensors and gather data - htg_cond_load_sensors, clg_cond_load_sensors = {}, {} - htg_duct_load_sensors, clg_duct_load_sensors = {}, {} - total_heat_load_serveds, total_cool_load_serveds = {}, {} - dehumidifier_global_vars, dehumidifier_sensors = {}, {} - - hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit| - # Retrieve objects - conditioned_zone_name = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace }.name.to_s - duct_zone_names = unit_model.getThermalZones.select { |z| z.isPlenum }.map { |z| z.name.to_s } - dehumidifier = unit_model.getZoneHVACDehumidifierDXs - dehumidifier_name = dehumidifier[0].name.to_s unless dehumidifier.empty? - - # Fraction heat/cool load served - if @hpxml_header.apply_ashrae140_assumptions - total_heat_load_serveds[unit] = 1.0 - total_cool_load_serveds[unit] = 1.0 - else - total_heat_load_serveds[unit] = hpxml_bldg.total_fraction_heat_load_served - total_cool_load_serveds[unit] = hpxml_bldg.total_fraction_cool_load_served - end - - # Energy transferred in conditioned zone, used for determining heating (winter) vs cooling (summer) - htg_cond_load_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Heating:EnergyTransfer:Zone:#{conditioned_zone_name.upcase}") - htg_cond_load_sensors[unit].setName('htg_load_cond') - clg_cond_load_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Cooling:EnergyTransfer:Zone:#{conditioned_zone_name.upcase}") - clg_cond_load_sensors[unit].setName('clg_load_cond') - - # Energy transferred in duct zone(s) - htg_duct_load_sensors[unit] = [] - clg_duct_load_sensors[unit] = [] - duct_zone_names.each do |duct_zone_name| - htg_duct_load_sensors[unit] << OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Heating:EnergyTransfer:Zone:#{duct_zone_name.upcase}") - htg_duct_load_sensors[unit][-1].setName('htg_load_duct') - clg_duct_load_sensors[unit] << OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Cooling:EnergyTransfer:Zone:#{duct_zone_name.upcase}") - clg_duct_load_sensors[unit][-1].setName('clg_load_duct') - end - - next if dehumidifier_name.nil? - - # Need to adjust E+ EnergyTransfer meters for dehumidifier internal gains. - # We also offset the dehumidifier load by one timestep so that it aligns with the EnergyTransfer meters. - - # Global Variable - dehumidifier_global_vars[unit] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "prev_#{dehumidifier_name}") - - # Initialization Program - timestep_offset_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) - timestep_offset_program.setName("#{dehumidifier_name} timestep offset init program") - timestep_offset_program.addLine("Set #{dehumidifier_global_vars[unit].name} = 0") - - # calling managers - manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - manager.setName("#{timestep_offset_program.name} calling manager") - manager.setCallingPoint('BeginNewEnvironment') - manager.addProgram(timestep_offset_program) - manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - manager.setName("#{timestep_offset_program.name} calling manager2") - manager.setCallingPoint('AfterNewEnvironmentWarmUpIsComplete') - manager.addProgram(timestep_offset_program) - - dehumidifier_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Dehumidifier Sensible Heating Energy') - dehumidifier_sensors[unit].setName('ig_dehumidifier') - dehumidifier_sensors[unit].setKeyName(dehumidifier_name) - end - - # EMS program - program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) - program.setName('total loads program') - program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeTotalLoadsProgram) - program.addLine('Set loads_htg_tot = 0') - program.addLine('Set loads_clg_tot = 0') - for unit in 0..hpxml_osm_map.size - 1 - program.addLine("If #{htg_cond_load_sensors[unit].name} > 0") - program.addLine(" Set loads_htg_tot = loads_htg_tot + (#{htg_cond_load_sensors[unit].name} - #{clg_cond_load_sensors[unit].name}) * #{total_heat_load_serveds[unit]}") - for i in 0..htg_duct_load_sensors[unit].size - 1 - program.addLine(" Set loads_htg_tot = loads_htg_tot + (#{htg_duct_load_sensors[unit][i].name} - #{clg_duct_load_sensors[unit][i].name}) * #{total_heat_load_serveds[unit]}") - end - if not dehumidifier_global_vars[unit].nil? - program.addLine(" Set loads_htg_tot = loads_htg_tot - #{dehumidifier_global_vars[unit].name}") - end - program.addLine('EndIf') - end - program.addLine('Set loads_htg_tot = (@Max loads_htg_tot 0)') - for unit in 0..hpxml_osm_map.size - 1 - program.addLine("If #{clg_cond_load_sensors[unit].name} > 0") - program.addLine(" Set loads_clg_tot = loads_clg_tot + (#{clg_cond_load_sensors[unit].name} - #{htg_cond_load_sensors[unit].name}) * #{total_cool_load_serveds[unit]}") - for i in 0..clg_duct_load_sensors[unit].size - 1 - program.addLine(" Set loads_clg_tot = loads_clg_tot + (#{clg_duct_load_sensors[unit][i].name} - #{htg_duct_load_sensors[unit][i].name}) * #{total_cool_load_serveds[unit]}") - end - if not dehumidifier_global_vars[unit].nil? - program.addLine(" Set loads_clg_tot = loads_clg_tot + #{dehumidifier_global_vars[unit].name}") - end - program.addLine('EndIf') - end - program.addLine('Set loads_clg_tot = (@Max loads_clg_tot 0)') - for unit in 0..hpxml_osm_map.size - 1 - if not dehumidifier_global_vars[unit].nil? - # Store dehumidifier internal gain, will be used in EMS program next timestep - program.addLine("Set #{dehumidifier_global_vars[unit].name} = #{dehumidifier_sensors[unit].name}") - end - end - - # EMS calling manager - program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - program_calling_manager.setName("#{program.name} calling manager") - program_calling_manager.setCallingPoint('EndOfZoneTimestepAfterZoneReporting') - program_calling_manager.addProgram(program) - - return htg_cond_load_sensors, clg_cond_load_sensors, total_heat_load_serveds, total_cool_load_serveds, dehumidifier_sensors - end - - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @param loads_data [TODO] TODO - # @param season_day_nums [TODO] TODO - # @return [nil] - def add_component_loads_output(model, hpxml_osm_map, loads_data, season_day_nums) - htg_cond_load_sensors, clg_cond_load_sensors, total_heat_load_serveds, total_cool_load_serveds, dehumidifier_sensors = loads_data - - # Output diagnostics needed for some output variables used below - output_diagnostics = model.getOutputDiagnostics - output_diagnostics.addKey('DisplayAdvancedReportVariables') - - area_tolerance = UnitConversions.convert(1.0, 'ft^2', 'm^2') - - nonsurf_names = ['intgains', 'lighting', 'infil', 'mechvent', 'natvent', 'whf', 'ducts'] - surf_names = ['walls', 'rim_joists', 'foundation_walls', 'floors', 'slabs', 'ceilings', - 'roofs', 'windows_conduction', 'windows_solar', 'doors', 'skylights_conduction', - 'skylights_solar', 'internal_mass'] - - # EMS program - program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) - program.setName('component loads program') - program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeComponentLoadsProgram) - - # Initialize - [:htg, :clg].each do |mode| - surf_names.each do |surf_name| - program.addLine("Set loads_#{mode}_#{surf_name} = 0") - end - nonsurf_names.each do |nonsurf_name| - program.addLine("Set loads_#{mode}_#{nonsurf_name} = 0") - end - end - - hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit| - conditioned_zone = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace } - - # Prevent certain objects (e.g., OtherEquipment) from being counted towards both, e.g., ducts and internal gains - objects_already_processed = [] - - # EMS Sensors: Surfaces, SubSurfaces, InternalMass - surfaces_sensors = {} - surf_names.each do |surf_name| - surfaces_sensors[surf_name.to_sym] = [] - end - - unit_model.getSurfaces.sort.each do |s| - next unless s.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - surface_type = s.additionalProperties.getFeatureAsString('SurfaceType') - if not surface_type.is_initialized - fail "Could not identify surface type for surface: '#{s.name}'." - end - - surface_type = surface_type.get - - s.subSurfaces.each do |ss| - # Conduction (windows, skylights, doors) - key = { 'Window' => :windows_conduction, - 'Door' => :doors, - 'Skylight' => :skylights_conduction }[surface_type] - fail "Unexpected subsurface for component loads: '#{ss.name}'." if key.nil? - - if (surface_type == 'Window') || (surface_type == 'Skylight') - vars = { 'Surface Inside Face Convection Heat Gain Energy' => 'ss_conv', - 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 'ss_ig', - 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 'ss_surf' } - else - vars = { 'Surface Inside Face Solar Radiation Heat Gain Energy' => 'ss_sol', - 'Surface Inside Face Lights Radiation Heat Gain Energy' => 'ss_lgt', - 'Surface Inside Face Convection Heat Gain Energy' => 'ss_conv', - 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 'ss_ig', - 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 'ss_surf' } - end - - vars.each do |var, name| - surfaces_sensors[key] << [] - sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - sensor.setName(name) - sensor.setKeyName(ss.name.to_s) - surfaces_sensors[key][-1] << sensor - end - - # Solar (windows, skylights) - next unless (surface_type == 'Window') || (surface_type == 'Skylight') - - key = { 'Window' => :windows_solar, - 'Skylight' => :skylights_solar }[surface_type] - vars = { 'Surface Window Transmitted Solar Radiation Energy' => 'ss_trans_in', - 'Surface Window Shortwave from Zone Back Out Window Heat Transfer Rate' => 'ss_back_out', - 'Surface Window Total Glazing Layers Absorbed Shortwave Radiation Rate' => 'ss_sw_abs', - 'Surface Window Total Glazing Layers Absorbed Solar Radiation Energy' => 'ss_sol_abs', - 'Surface Inside Face Initial Transmitted Diffuse Transmitted Out Window Solar Radiation Rate' => 'ss_trans_out' } - - surfaces_sensors[key] << [] - vars.each do |var, name| - sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - sensor.setName(name) - sensor.setKeyName(ss.name.to_s) - surfaces_sensors[key][-1] << sensor - end - end - - next if s.netArea < area_tolerance # Skip parent surfaces (of subsurfaces) that have near zero net area - - key = { 'FoundationWall' => :foundation_walls, - 'RimJoist' => :rim_joists, - 'Wall' => :walls, - 'Slab' => :slabs, - 'Floor' => :floors, - 'Ceiling' => :ceilings, - 'Roof' => :roofs, - 'Skylight' => :skylights_conduction, # Skylight curb/shaft - 'InferredCeiling' => :internal_mass, - 'InferredFloor' => :internal_mass }[surface_type] - fail "Unexpected surface for component loads: '#{s.name}'." if key.nil? - - surfaces_sensors[key] << [] - { 'Surface Inside Face Convection Heat Gain Energy' => 's_conv', - 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 's_ig', - 'Surface Inside Face Solar Radiation Heat Gain Energy' => 's_sol', - 'Surface Inside Face Lights Radiation Heat Gain Energy' => 's_lgt', - 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 's_surf' }.each do |var, name| - sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - sensor.setName(name) - sensor.setKeyName(s.name.to_s) - surfaces_sensors[key][-1] << sensor - end - end - - unit_model.getInternalMasss.sort.each do |m| - next unless m.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - surfaces_sensors[:internal_mass] << [] - { 'Surface Inside Face Convection Heat Gain Energy' => 'im_conv', - 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 'im_ig', - 'Surface Inside Face Solar Radiation Heat Gain Energy' => 'im_sol', - 'Surface Inside Face Lights Radiation Heat Gain Energy' => 'im_lgt', - 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 'im_surf' }.each do |var, name| - sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - sensor.setName(name) - sensor.setKeyName(m.name.to_s) - surfaces_sensors[:internal_mass][-1] << sensor - end - end - - # EMS Sensors: Infiltration, Natural Ventilation, Whole House Fan - infil_sensors, natvent_sensors, whf_sensors = [], [], [] - unit_model.getSpaceInfiltrationDesignFlowRates.sort.each do |i| - next unless i.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - object_type = i.additionalProperties.getFeatureAsString('ObjectType').get - - { 'Infiltration Sensible Heat Gain Energy' => 'airflow_gain', - 'Infiltration Sensible Heat Loss Energy' => 'airflow_loss' }.each do |var, name| - airflow_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - airflow_sensor.setName(name) - airflow_sensor.setKeyName(i.name.to_s) - if object_type == Constants::ObjectTypeInfiltration - infil_sensors << airflow_sensor - elsif object_type == Constants::ObjectTypeNaturalVentilation - natvent_sensors << airflow_sensor - elsif object_type == Constants::ObjectTypeWholeHouseFan - whf_sensors << airflow_sensor - end - end - end - - # EMS Sensors: Mechanical Ventilation - mechvents_sensors = [] - unit_model.getElectricEquipments.sort.each do |o| - next unless o.endUseSubcategory == Constants::ObjectTypeMechanicalVentilation - - objects_already_processed << o - { 'Electric Equipment Convective Heating Energy' => 'mv_conv', - 'Electric Equipment Radiant Heating Energy' => 'mv_rad' }.each do |var, name| - mechvent_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - mechvent_sensor.setName(name) - mechvent_sensor.setKeyName(o.name.to_s) - mechvents_sensors << mechvent_sensor - end - end - unit_model.getOtherEquipments.sort.each do |o| - next unless o.endUseSubcategory == Constants::ObjectTypeMechanicalVentilationHouseFan - - objects_already_processed << o - { 'Other Equipment Convective Heating Energy' => 'mv_conv', - 'Other Equipment Radiant Heating Energy' => 'mv_rad' }.each do |var, name| - mechvent_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - mechvent_sensor.setName(name) - mechvent_sensor.setKeyName(o.name.to_s) - mechvents_sensors << mechvent_sensor - end - end - - # EMS Sensors: Ducts - ducts_sensors = [] - ducts_mix_gain_sensor = nil - ducts_mix_loss_sensor = nil - conditioned_zone.zoneMixing.each do |zone_mix| - object_type = zone_mix.additionalProperties.getFeatureAsString('ObjectType').to_s - next unless object_type == Constants::ObjectTypeDuctLoad - - ducts_mix_gain_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mixing Sensible Heat Gain Energy') - ducts_mix_gain_sensor.setName('duct_mix_gain') - ducts_mix_gain_sensor.setKeyName(conditioned_zone.name.to_s) - - ducts_mix_loss_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mixing Sensible Heat Loss Energy') - ducts_mix_loss_sensor.setName('duct_mix_loss') - ducts_mix_loss_sensor.setKeyName(conditioned_zone.name.to_s) - end - unit_model.getOtherEquipments.sort.each do |o| - next if objects_already_processed.include? o - next unless o.endUseSubcategory == Constants::ObjectTypeDuctLoad - - objects_already_processed << o - { 'Other Equipment Convective Heating Energy' => 'ducts_conv', - 'Other Equipment Radiant Heating Energy' => 'ducts_rad' }.each do |var, name| - ducts_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - ducts_sensor.setName(name) - ducts_sensor.setKeyName(o.name.to_s) - ducts_sensors << ducts_sensor - end - end - - # EMS Sensors: Lighting - lightings_sensors = [] - unit_model.getLightss.sort.each do |e| - next unless e.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - { 'Lights Convective Heating Energy' => 'ig_lgt_conv', - 'Lights Radiant Heating Energy' => 'ig_lgt_rad', - 'Lights Visible Radiation Heating Energy' => 'ig_lgt_vis' }.each do |var, name| - intgains_lights_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - intgains_lights_sensor.setName(name) - intgains_lights_sensor.setKeyName(e.name.to_s) - lightings_sensors << intgains_lights_sensor - end - end - - # EMS Sensors: Internal Gains - intgains_sensors = [] - unit_model.getElectricEquipments.sort.each do |o| - next if objects_already_processed.include? o - next unless o.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - { 'Electric Equipment Convective Heating Energy' => 'ig_ee_conv', - 'Electric Equipment Radiant Heating Energy' => 'ig_ee_rad' }.each do |var, name| - intgains_elec_equip_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - intgains_elec_equip_sensor.setName(name) - intgains_elec_equip_sensor.setKeyName(o.name.to_s) - intgains_sensors << intgains_elec_equip_sensor - end - end - - unit_model.getOtherEquipments.sort.each do |o| - next if objects_already_processed.include? o - next unless o.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - { 'Other Equipment Convective Heating Energy' => 'ig_oe_conv', - 'Other Equipment Radiant Heating Energy' => 'ig_oe_rad' }.each do |var, name| - intgains_other_equip_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - intgains_other_equip_sensor.setName(name) - intgains_other_equip_sensor.setKeyName(o.name.to_s) - intgains_sensors << intgains_other_equip_sensor - end - end - - unit_model.getPeoples.sort.each do |e| - next unless e.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s - - { 'People Convective Heating Energy' => 'ig_ppl_conv', - 'People Radiant Heating Energy' => 'ig_ppl_rad' }.each do |var, name| - intgains_people = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) - intgains_people.setName(name) - intgains_people.setKeyName(e.name.to_s) - intgains_sensors << intgains_people - end - end - - if not dehumidifier_sensors[unit].nil? - intgains_sensors << dehumidifier_sensors[unit] - end - - intgains_dhw_sensors = {} - - (unit_model.getWaterHeaterMixeds + unit_model.getWaterHeaterStratifieds).sort.each do |wh| - next unless wh.ambientTemperatureThermalZone.is_initialized - next unless wh.ambientTemperatureThermalZone.get.name.to_s == conditioned_zone.name.to_s - - dhw_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Heat Loss Energy') - dhw_sensor.setName('dhw_loss') - dhw_sensor.setKeyName(wh.name.to_s) - - if wh.is_a? OpenStudio::Model::WaterHeaterMixed - oncycle_loss = wh.onCycleLossFractiontoThermalZone - offcycle_loss = wh.offCycleLossFractiontoThermalZone - else - oncycle_loss = wh.skinLossFractiontoZone - offcycle_loss = wh.offCycleFlueLossFractiontoZone - end - - dhw_rtf_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Runtime Fraction') - dhw_rtf_sensor.setName('dhw_rtf') - dhw_rtf_sensor.setKeyName(wh.name.to_s) - - intgains_dhw_sensors[dhw_sensor] = [offcycle_loss, oncycle_loss, dhw_rtf_sensor] - end - - # EMS program: Surfaces - surfaces_sensors.each do |k, surface_sensors| - program.addLine("Set hr_#{k} = 0") - surface_sensors.each do |sensors| - s = "Set hr_#{k} = hr_#{k}" - sensors.each do |sensor| - # remove ss_net if switch - if sensor.name.to_s.start_with?('ss_net', 'ss_sol_abs', 'ss_trans_in') - s += " - #{sensor.name}" - elsif sensor.name.to_s.start_with?('ss_sw_abs', 'ss_trans_out', 'ss_back_out') - s += " + #{sensor.name} * ZoneTimestep * 3600" - else - s += " + #{sensor.name}" - end - end - program.addLine(s) if sensors.size > 0 - end - end - - # EMS program: Internal Gains, Lighting, Infiltration, Natural Ventilation, Mechanical Ventilation, Ducts - { 'intgains' => intgains_sensors, - 'lighting' => lightings_sensors, - 'infil' => infil_sensors, - 'natvent' => natvent_sensors, - 'whf' => whf_sensors, - 'mechvent' => mechvents_sensors, - 'ducts' => ducts_sensors }.each do |loadtype, sensors| - program.addLine("Set hr_#{loadtype} = 0") - next if sensors.empty? - - s = "Set hr_#{loadtype} = hr_#{loadtype}" - sensors.each do |sensor| - if ['intgains', 'lighting', 'mechvent', 'ducts'].include? loadtype - s += " - #{sensor.name}" - elsif sensor.name.to_s.include? 'gain' - s += " - #{sensor.name}" - elsif sensor.name.to_s.include? 'loss' - s += " + #{sensor.name}" - end - end - program.addLine(s) - end - intgains_dhw_sensors.each do |sensor, vals| - off_loss, on_loss, rtf_sensor = vals - program.addLine("Set hr_intgains = hr_intgains + #{sensor.name} * (#{off_loss}*(1-#{rtf_sensor.name}) + #{on_loss}*#{rtf_sensor.name})") # Water heater tank losses to zone - end - if (not ducts_mix_loss_sensor.nil?) && (not ducts_mix_gain_sensor.nil?) - program.addLine("Set hr_ducts = hr_ducts + (#{ducts_mix_loss_sensor.name} - #{ducts_mix_gain_sensor.name})") - end - - # EMS Sensors: Indoor temperature, setpoints - tin_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mean Air Temperature') - tin_sensor.setName('tin s') - tin_sensor.setKeyName(conditioned_zone.name.to_s) - thermostat = nil - if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized - thermostat = conditioned_zone.thermostatSetpointDualSetpoint.get - - htg_sp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - htg_sp_sensor.setName('htg sp s') - htg_sp_sensor.setKeyName(thermostat.heatingSetpointTemperatureSchedule.get.name.to_s) - - clg_sp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - clg_sp_sensor.setName('clg sp s') - clg_sp_sensor.setKeyName(thermostat.coolingSetpointTemperatureSchedule.get.name.to_s) - end - - # EMS program: Heating vs Cooling logic - program.addLine('Set htg_mode = 0') - program.addLine('Set clg_mode = 0') - program.addLine("If (#{htg_cond_load_sensors[unit].name} > 0)") # Assign hour to heating if heating load - program.addLine(" Set htg_mode = #{total_heat_load_serveds[unit]}") - program.addLine("ElseIf (#{clg_cond_load_sensors[unit].name} > 0)") # Assign hour to cooling if cooling load - program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") - program.addLine('Else') - program.addLine(' Set htg_season = 0') - program.addLine(' Set clg_season = 0') - if not season_day_nums[unit].nil? - # Determine whether we're in the heating and/or cooling season - if season_day_nums[unit][:clg_end] >= season_day_nums[unit][:clg_start] - program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) && (DayOfYear <= #{season_day_nums[unit][:clg_end]}))") - else - program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) || (DayOfYear <= #{season_day_nums[unit][:clg_end]}))") - end - program.addLine(' Set clg_season = 1') - program.addLine(' EndIf') - if season_day_nums[unit][:htg_end] >= season_day_nums[unit][:htg_start] - program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) && (DayOfYear <= #{season_day_nums[unit][:htg_end]}))") - else - program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) || (DayOfYear <= #{season_day_nums[unit][:htg_end]}))") - end - program.addLine(' Set htg_season = 1') - program.addLine(' EndIf') - end - program.addLine(" If ((#{natvent_sensors[0].name} <> 0) || (#{natvent_sensors[1].name} <> 0)) && (clg_season == 1)") # Assign hour to cooling if natural ventilation is operating - program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") - program.addLine(" ElseIf ((#{whf_sensors[0].name} <> 0) || (#{whf_sensors[1].name} <> 0)) && (clg_season == 1)") # Assign hour to cooling if whole house fan is operating - program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") - if not thermostat.nil? - program.addLine(' Else') # Indoor temperature floating between setpoints; determine assignment by comparing to average of heating/cooling setpoints - program.addLine(" Set Tmid_setpoint = (#{htg_sp_sensor.name} + #{clg_sp_sensor.name}) / 2") - program.addLine(" If (#{tin_sensor.name} > Tmid_setpoint) && (clg_season == 1)") - program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") - program.addLine(" ElseIf (#{tin_sensor.name} < Tmid_setpoint) && (htg_season == 1)") - program.addLine(" Set htg_mode = #{total_heat_load_serveds[unit]}") - program.addLine(' EndIf') - end - program.addLine(' EndIf') - program.addLine('EndIf') - - unit_multiplier = hpxml_bldg.building_construction.number_of_units - [:htg, :clg].each do |mode| - if mode == :htg - sign = '' - else - sign = '-' - end - surf_names.each do |surf_name| - program.addLine("Set loads_#{mode}_#{surf_name} = loads_#{mode}_#{surf_name} + (#{sign}hr_#{surf_name} * #{mode}_mode * #{unit_multiplier})") - end - nonsurf_names.each do |nonsurf_name| - program.addLine("Set loads_#{mode}_#{nonsurf_name} = loads_#{mode}_#{nonsurf_name} + (#{sign}hr_#{nonsurf_name} * #{mode}_mode * #{unit_multiplier})") - end - end - end - - # EMS calling manager - program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - program_calling_manager.setName("#{program.name} calling manager") - program_calling_manager.setCallingPoint('EndOfZoneTimestepAfterZoneReporting') - program_calling_manager.addProgram(program) - end - - # Creates airflow outputs (for infiltration, ventilation, etc.) that sum across all individual dwelling - # units for output reporting. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit - # @return [nil] - def add_total_airflows_output(model, hpxml_osm_map) - # Retrieve objects - infil_vars = [] - mechvent_vars = [] - natvent_vars = [] - whf_vars = [] - unit_multipliers = [] - hpxml_osm_map.each do |hpxml_bldg, unit_model| - infil_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeInfiltration } - mechvent_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeMechanicalVentilation } - natvent_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeNaturalVentilation } - whf_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeWholeHouseFan } - unit_multipliers << hpxml_bldg.building_construction.number_of_units - end - - # EMS program - program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) - program.setName('total airflows program') - program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeTotalAirflowsProgram) - program.addLine('Set total_infil_flow_rate = 0') - program.addLine('Set total_mechvent_flow_rate = 0') - program.addLine('Set total_natvent_flow_rate = 0') - program.addLine('Set total_whf_flow_rate = 0') - infil_vars.each_with_index do |infil_var, i| - program.addLine("Set total_infil_flow_rate = total_infil_flow_rate + (#{infil_var.name} * #{unit_multipliers[i]})") - end - mechvent_vars.each_with_index do |mechvent_var, i| - program.addLine("Set total_mechvent_flow_rate = total_mechvent_flow_rate + (#{mechvent_var.name} * #{unit_multipliers[i]})") - end - natvent_vars.each_with_index do |natvent_var, i| - program.addLine("Set total_natvent_flow_rate = total_natvent_flow_rate + (#{natvent_var.name} * #{unit_multipliers[i]})") - end - whf_vars.each_with_index do |whf_var, i| - program.addLine("Set total_whf_flow_rate = total_whf_flow_rate + (#{whf_var.name} * #{unit_multipliers[i]})") - end - - # EMS calling manager - program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) - program_calling_manager.setName("#{program.name} calling manager") - program_calling_manager.setCallingPoint('EndOfZoneTimestepAfterZoneReporting') - program_calling_manager.addProgram(program) - end - - # Populate fields of both unique OpenStudio objects OutputJSON and OutputControlFiles based on the debug argument. - # Always request MessagePack output. - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [nil] - def add_output_files(model) - oj = model.getOutputJSON - oj.setOptionType('TimeSeriesAndTabular') - oj.setOutputJSON(@debug) - oj.setOutputMessagePack(true) # Used by ReportSimulationOutput reporting measure - - ocf = model.getOutputControlFiles - ocf.setOutputAUDIT(@debug) - ocf.setOutputCSV(@debug) - ocf.setOutputBND(@debug) - ocf.setOutputEIO(@debug) - ocf.setOutputESO(@debug) - ocf.setOutputMDD(@debug) - ocf.setOutputMTD(@debug) - ocf.setOutputMTR(@debug) - ocf.setOutputRDD(@debug) - ocf.setOutputSHD(@debug) - ocf.setOutputCSV(@debug) - ocf.setOutputSQLite(@debug) - ocf.setOutputPerfLog(@debug) - end - - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @return [nil] - def add_ems_debug_output(model) - oems = model.getOutputEnergyManagementSystem - oems.setActuatorAvailabilityDictionaryReporting('Verbose') - oems.setInternalVariableAvailabilityDictionaryReporting('Verbose') - oems.setEMSRuntimeLanguageDebugOutputLevel('Verbose') + Airflow.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_systems, + airloop_map, @frac_windows_operable, schedules_file, hvac_availability_sensor) end end diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 6926399884..475ae78767 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 5a145ea9-f949-4aac-80ab-b6925c428957 - 2024-09-16T14:45:24Z + 735921d7-e5e7-46fa-969c-87cafd760d8a + 2024-09-16T20:47:51Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,19 +183,19 @@ measure.rb rb script - 25D005AC + 3B5C6261 airflow.rb rb resource - B363F803 + 5929C59D battery.rb rb resource - 520825A4 + 1F96B42D calendar.rb @@ -333,19 +333,19 @@ generator.rb rb resource - A4B07257 + 6952CD2A geometry.rb rb resource - 3CF58696 + B1B13531 hotwater_appliances.rb rb resource - B8578044 + 14725DD9 hpxml.rb @@ -357,7 +357,7 @@ hpxml_defaults.rb rb resource - 834FF6AE + 18DD0B67 hpxml_schema/HPXML.xsd @@ -387,31 +387,31 @@ hvac.rb rb resource - 93926A7F + 7BB9BD6F hvac_sizing.rb rb resource - BA28B74A + 9E3F6A59 internal_gains.rb rb resource - 808BF9F9 + FD08F8A8 lighting.rb rb resource - 65744C8E + 3851C46B location.rb rb resource - 2CB41E3C + BC2B3366 materials.rb @@ -441,7 +441,7 @@ misc_loads.rb rb resource - 8E650A1B + 9A5914F8 model.rb @@ -453,7 +453,7 @@ output.rb rb resource - 1AF3410C + AAF299D9 psychrometrics.rb @@ -465,7 +465,7 @@ pv.rb rb resource - 6ACFD5FF + FF6B391A schedule_files/battery.csv diff --git a/HPXMLtoOpenStudio/resources/airflow.rb b/HPXMLtoOpenStudio/resources/airflow.rb index e7c162f791..5dda4d43ac 100644 --- a/HPXMLtoOpenStudio/resources/airflow.rb +++ b/HPXMLtoOpenStudio/resources/airflow.rb @@ -16,21 +16,14 @@ module Airflow # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) - # @param ncfl_ag [Double] Number of conditioned floors above grade in the dwelling unit # @param duct_systems [TODO] TODO # @param airloop_map [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param frac_windows_operable [TODO] TODO - # @param apply_ashrae140_assumptions [TODO] TODO # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @param hvac_availability_sensor [TODO] TODO # @return [TODO] TODO - def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, cfa, - ncfl_ag, duct_systems, airloop_map, eri_version, - frac_windows_operable, apply_ashrae140_assumptions, schedules_file, - unavailable_periods, hvac_availability_sensor) + def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_systems, + airloop_map, frac_windows_operable, schedules_file, hvac_availability_sensor) # Global variables @@ -39,13 +32,14 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, cfa, @year = hpxml_header.sim_calendar_year @conditioned_space = spaces[HPXML::LocationConditionedSpace] @conditioned_zone = @conditioned_space.thermalZone.get - @ncfl_ag = ncfl_ag - @eri_version = eri_version - @apply_ashrae140_assumptions = apply_ashrae140_assumptions - @cfa = cfa + @ncfl_ag = hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade + @eri_version = hpxml_header.eri_calculation_version + @apply_ashrae140_assumptions = hpxml_header.apply_ashrae140_assumptions @cooking_range_in_cond_space = hpxml_bldg.cooking_ranges.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml_bldg.cooking_ranges[0].location) @clothes_dryer_in_cond_space = hpxml_bldg.clothes_dryers.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml_bldg.clothes_dryers[0].location) @hvac_availability_sensor = hvac_availability_sensor + cfa = hpxml_bldg.building_construction.conditioned_floor_area + unavailable_periods = hpxml_header.unavailable_periods # Global sensors @@ -164,7 +158,7 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, cfa, apply_infiltration_ventilation_to_conditioned(model, hpxml_bldg.site, vent_fans_mech, conditioned_ach50, conditioned_const_ach, infil_values[:volume], infil_values[:height], weather, vent_fans_kitchen, vent_fans_bath, vented_dryers, has_flue_chimney_in_cond_space, clg_ssn_sensor, schedules_file, vent_fans_cfis_suppl, unavailable_periods, hpxml_bldg.elevation, duct_lk_imbals, - unit_height_above_grade) + unit_height_above_grade, cfa) end # TODO @@ -1138,7 +1132,7 @@ def self.apply_ducts(model, ducts, object, vent_fans_mech, unit_multiplier, duct equip_act_infos = [] if duct_location.is_a? OpenStudio::Model::ScheduleConstant - space_values = Geometry.get_temperature_scheduled_space_values(location: duct_location.name.to_s) + space_values = Geometry.get_temperature_scheduled_space_values(duct_location.name.to_s) f_regain = space_values[:f_regain] else f_regain = 0.0 @@ -2294,10 +2288,11 @@ def self.calculate_precond_loads(model, infil_program, vent_mech_preheat, vent_m # @param elevation [Double] Elevation of the building site (ft) # @param duct_lk_imbals [TODO] TODO # @param unit_height_above_grade [TODO] TODO + # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) # @return [TODO] TODO def self.apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_mech, conditioned_ach50, conditioned_const_ach, infil_volume, infil_height, weather, vent_fans_kitchen, vent_fans_bath, vented_dryers, has_flue_chimney_in_cond_space, clg_ssn_sensor, schedules_file, - vent_fans_cfis_suppl, unavailable_periods, elevation, duct_lk_imbals, unit_height_above_grade) + vent_fans_cfis_suppl, unavailable_periods, elevation, duct_lk_imbals, unit_height_above_grade, cfa) # Categorize fans into different types vent_mech_preheat = vent_fans_mech.select { |vent_mech| (not vent_mech.preheating_efficiency_cop.nil?) } vent_mech_precool = vent_fans_mech.select { |vent_mech| (not vent_mech.precooling_efficiency_cop.nil?) } @@ -2344,7 +2339,7 @@ def self.apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_me # Calculate infiltration without adjustment by ventilation apply_infiltration_to_conditioned(site, conditioned_ach50, conditioned_const_ach, infil_program, weather, has_flue_chimney_in_cond_space, infil_volume, - infil_height, unit_height_above_grade, elevation) + infil_height, unit_height_above_grade, elevation, cfa) # Common variable and load actuators across multiple mech vent calculations, create only once fan_sens_load_actuator, fan_lat_load_actuator = setup_mech_vent_vars_actuators(model: model, program: infil_program) @@ -2391,9 +2386,10 @@ def self.apply_infiltration_ventilation_to_conditioned(model, site, vent_fans_me # @param infil_height [Double] Vertical distance between the lowest and highest above-grade points within the pressure boundary, per ASHRAE 62.2 (ft2) # @param unit_height_above_grade [TODO] TODO # @param elevation [Double] Elevation of the building site (ft) + # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) # @return [nil] def self.apply_infiltration_to_conditioned(site, conditioned_ach50, conditioned_const_ach, infil_program, weather, has_flue_chimney_in_cond_space, infil_volume, - infil_height, unit_height_above_grade, elevation) + infil_height, unit_height_above_grade, elevation, cfa) site_ap = site.additional_properties if conditioned_ach50.to_f > 0 @@ -2404,8 +2400,8 @@ def self.apply_infiltration_to_conditioned(site, conditioned_ach50, conditioned_ outside_air_density = UnitConversions.convert(p_atm, 'atm', 'Btu/ft^3') / (Gas.Air.r * UnitConversions.convert(weather.data.AnnualAvgDrybulb, 'F', 'R')) n_i = InfilPressureExponent - conditioned_sla = get_infiltration_SLA_from_ACH50(conditioned_ach50, n_i, @cfa, infil_volume) # Calculate SLA - a_o = conditioned_sla * @cfa # Effective Leakage Area (ft2) + conditioned_sla = get_infiltration_SLA_from_ACH50(conditioned_ach50, n_i, cfa, infil_volume) # Calculate SLA + a_o = conditioned_sla * cfa # Effective Leakage Area (ft2) # Flow Coefficient (cfm/inH2O^n) (based on ASHRAE HoF) inf_conv_factor = 776.25 # [ft/min]/[inH2O^(1/2)*ft^(3/2)/lbm^(1/2)] @@ -2512,7 +2508,7 @@ def self.calc_wind_stack_coeffs(site, hor_lk_frac, neutral_level, space, space_h if space_height.nil? space_height = Geometry.get_height_of_spaces(spaces: [space]) end - coord_z = Geometry.get_z_origin_for_zone(zone: space.thermalZone.get) + coord_z = Geometry.get_z_origin_for_zone(space.thermalZone.get) f_t_SG = site_ap.site_terrain_multiplier * ((space_height + coord_z) / 32.8)**site_ap.site_terrain_exponent / (site_ap.terrain_multiplier * (site_ap.height / 32.8)**site_ap.terrain_exponent) f_s_SG = 2.0 / 3.0 * (1 + hor_lk_frac / 2.0) * (2.0 * neutral_level * (1.0 - neutral_level))**0.5 / (neutral_level**0.5 + (1.0 - neutral_level)**0.5) f_w_SG = site_ap.s_g_shielding_coef * (1.0 - hor_lk_frac)**(1.0 / 3.0) * f_t_SG diff --git a/HPXMLtoOpenStudio/resources/battery.rb b/HPXMLtoOpenStudio/resources/battery.rb index 2ff2a16938..9181e9d2b1 100644 --- a/HPXMLtoOpenStudio/resources/battery.rb +++ b/HPXMLtoOpenStudio/resources/battery.rb @@ -2,6 +2,22 @@ # Collection of methods for adding battery-related OpenStudio objects. module Battery + # Adds any HPXML Batteries to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @return [nil] + def self.apply_batteries(runner, model, spaces, hpxml_bldg, schedules_file) + hpxml_bldg.batteries.each do |battery| + apply_battery(runner, model, spaces, hpxml_bldg, battery, schedules_file) + end + end + + # Add the HPXML Battery to the OpenStudio model. + # # Apply a home battery to the model using OpenStudio ElectricLoadCenterStorageLiIonNMCBattery, ElectricLoadCenterDistribution, ElectricLoadCenterStorageConverter, OtherEquipment, and EMS objects. # Battery without PV specified, and no charging/discharging schedule provided; battery is assumed to operate as backup and will not be modeled. # The system may be shared, in which case nominal/usable capacity (kWh) and usable fraction are apportioned to the dwelling unit by total number of bedrooms served. @@ -10,13 +26,16 @@ module Battery # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @param pv_systems [HPXML::PVSystems] Object that defines each solar electric photovoltaic (PV) system + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param battery [HPXML::Battery] Object that defines a single home battery # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param unit_multiplier [Integer] Number of similar dwelling units # @return [nil] for unscheduled battery w/out PV; in this case battery is not modeled - def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_multiplier) + def self.apply_battery(runner, model, spaces, hpxml_bldg, battery, schedules_file) + nbeds = hpxml_bldg.building_construction.number_of_bedrooms + unit_multiplier = hpxml_bldg.building_construction.number_of_units + pv_systems = hpxml_bldg.pv_systems + charging_schedule = nil discharging_schedule = nil if not schedules_file.nil? @@ -31,6 +50,8 @@ def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_m obj_name = battery.id + space = Geometry.get_space_from_location(battery.location, spaces) + rated_power_output = battery.rated_power_output # W if not battery.nominal_capacity_kwh.nil? if battery.usable_capacity_kwh.nil? @@ -94,7 +115,7 @@ def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_m elcs = OpenStudio::Model::ElectricLoadCenterStorageLiIonNMCBattery.new(model, number_of_cells_in_series, number_of_strings_in_parallel, battery_mass, battery_surface_area) elcs.setName("#{obj_name} li ion") if not is_outside - elcs.setThermalZone(battery.additional_properties.space.thermalZone.get) + elcs.setThermalZone(space.thermalZone.get) end elcs.setRadiativeFraction(0.9 * frac_sens) # elcs.setLifetimeModel(battery.lifetime_model) @@ -148,7 +169,6 @@ def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_m end frac_lost = 0.0 - space = battery.additional_properties.space if space.nil? space = model.getSpaces[0] frac_lost = 1.0 diff --git a/HPXMLtoOpenStudio/resources/generator.rb b/HPXMLtoOpenStudio/resources/generator.rb index b5be879640..82a7a87a83 100644 --- a/HPXMLtoOpenStudio/resources/generator.rb +++ b/HPXMLtoOpenStudio/resources/generator.rb @@ -2,16 +2,30 @@ # Collection of methods for adding generator-related OpenStudio objects. module Generator + # Adds any HPXML Generators to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [nil] + def self.apply_generators(model, hpxml_bldg) + hpxml_bldg.generators.each do |generator| + apply_generator(model, hpxml_bldg, generator) + end + end + + # Adds the HPXML Generator to the OpenStudio model. + # # Apply a on-site power generator to the model using OpenStudio GeneratorMicroTurbine and ElectricLoadCenterDistribution objects. # The system may be shared, in which case annual consumption (kBtu) and output (kWh) are apportioned to the dwelling unit by total number of bedrooms served. # A new ElectricLoadCenterDistribution object is created for each generator. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param nbeds [Integer] Number of bedrooms in the dwelling unit + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param generator [HPXML::Generator] Object that defines a single generator that provides on-site power - # @param unit_multiplier [Integer] Number of similar dwelling units # @return [nil] - def self.apply(model, nbeds, generator, unit_multiplier) + def self.apply_generator(model, hpxml_bldg, generator) + nbeds = hpxml_bldg.building_construction.number_of_bedrooms + unit_multiplier = hpxml_bldg.building_construction.number_of_units obj_name = generator.id # Apply unit multiplier diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index ea56f7b86b..0db151fb25 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -2,6 +2,1069 @@ # Collection of methods to get, add, assign, create, etc. geometry-related OpenStudio objects. module Geometry + # Adds any HPXML Roofs to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_roofs(runner, model, spaces, hpxml_bldg, hpxml_header) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + walls_top, _foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + hpxml_bldg.roofs.each do |roof| + next if roof.net_area < 1.0 # skip modeling net surface area for surfaces comprised entirely of subsurface area + + if roof.azimuth.nil? + if roof.pitch > 0 + azimuths = default_azimuths # Model as four directions for average exterior incident solar + else + azimuths = [default_azimuths[0]] # Arbitrary azimuth for flat roof + end + else + azimuths = [roof.azimuth] + end + + surfaces = [] + + azimuths.each do |azimuth| + width = Math::sqrt(roof.net_area) + length = (roof.net_area / width) / azimuths.size + tilt = roof.pitch / 12.0 + z_origin = walls_top + 0.5 * Math.sin(Math.atan(tilt)) * width + + vertices = create_roof_vertices(length, width, z_origin, azimuth, tilt) + surface = OpenStudio::Model::Surface.new(vertices, model) + surfaces << surface + surface.additionalProperties.setFeature('Length', length) + surface.additionalProperties.setFeature('Width', width) + surface.additionalProperties.setFeature('Azimuth', azimuth) + surface.additionalProperties.setFeature('Tilt', tilt) + surface.additionalProperties.setFeature('SurfaceType', 'Roof') + if azimuths.size > 1 + surface.setName("#{roof.id}:#{azimuth}") + else + surface.setName(roof.id) + end + surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) + set_surface_interior(model, spaces, surface, roof, hpxml_bldg) + end + + next if surfaces.empty? + + # Apply construction + has_radiant_barrier = roof.radiant_barrier + if has_radiant_barrier + radiant_barrier_grade = roof.radiant_barrier_grade + end + # FUTURE: Create Constructions.get_air_film(surface) method; use in measure.rb and hpxml_translator_test.rb + inside_film = Material.AirFilmRoof(get_roof_pitch([surfaces[0]])) + outside_film = Material.AirFilmOutside + mat_roofing = Material.RoofMaterial(roof.roof_type) + if hpxml_header.apply_ashrae140_assumptions + inside_film = Material.AirFilmRoofASHRAE140 + outside_film = Material.AirFilmOutsideASHRAE140 + end + mat_int_finish = Material.InteriorFinishMaterial(roof.interior_finish_type, roof.interior_finish_thickness) + if mat_int_finish.nil? + fallback_mat_int_finish = nil + else + fallback_mat_int_finish = Material.InteriorFinishMaterial(mat_int_finish.name, 0.1) # Try thin material + end + + install_grade = 1 + assembly_r = roof.insulation_assembly_r_value + + if not mat_int_finish.nil? + # Closed cavity + constr_sets = [ + WoodStudConstructionSet.new(Material.Stud2x(8.0), 0.07, 20.0, 0.75, mat_int_finish, mat_roofing), # 2x8, 24" o.c. + R20 + WoodStudConstructionSet.new(Material.Stud2x(8.0), 0.07, 10.0, 0.75, mat_int_finish, mat_roofing), # 2x8, 24" o.c. + R10 + WoodStudConstructionSet.new(Material.Stud2x(8.0), 0.07, 0.0, 0.75, mat_int_finish, mat_roofing), # 2x8, 24" o.c. + WoodStudConstructionSet.new(Material.Stud2x6, 0.07, 0.0, 0.75, mat_int_finish, mat_roofing), # 2x6, 24" o.c. + WoodStudConstructionSet.new(Material.Stud2x4, 0.07, 0.0, 0.5, mat_int_finish, mat_roofing), # 2x4, 16" o.c. + WoodStudConstructionSet.new(Material.Stud2x4, 0.01, 0.0, 0.0, fallback_mat_int_finish, mat_roofing), # Fallback + ] + match, constr_set, cavity_r = Constructions.pick_wood_stud_construction_set(assembly_r, constr_sets, inside_film, outside_film) + + Constructions.apply_closed_cavity_roof(model, surfaces, "#{roof.id} construction", + cavity_r, install_grade, + constr_set.stud.thick_in, + true, constr_set.framing_factor, + constr_set.mat_int_finish, + constr_set.osb_thick_in, constr_set.rigid_r, + constr_set.mat_ext_finish, has_radiant_barrier, + inside_film, outside_film, radiant_barrier_grade, + roof.solar_absorptance, roof.emittance) + else + # Open cavity + constr_sets = [ + GenericConstructionSet.new(10.0, 0.5, nil, mat_roofing), # w/R-10 rigid + GenericConstructionSet.new(0.0, 0.5, nil, mat_roofing), # Standard + GenericConstructionSet.new(0.0, 0.0, nil, mat_roofing), # Fallback + ] + match, constr_set, layer_r = Constructions.pick_generic_construction_set(assembly_r, constr_sets, inside_film, outside_film) + + cavity_r = 0 + cavity_ins_thick_in = 0 + framing_factor = 0 + framing_thick_in = 0 + + Constructions.apply_open_cavity_roof(model, surfaces, "#{roof.id} construction", + cavity_r, install_grade, cavity_ins_thick_in, + framing_factor, framing_thick_in, + constr_set.osb_thick_in, layer_r + constr_set.rigid_r, + constr_set.mat_ext_finish, has_radiant_barrier, + inside_film, outside_film, radiant_barrier_grade, + roof.solar_absorptance, roof.emittance) + end + Constructions.check_surface_assembly_rvalue(runner, surfaces, inside_film, outside_film, assembly_r, match) + end + end + + # Adds any HPXML Walls to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_walls(runner, model, spaces, hpxml_bldg, hpxml_header) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + _walls_top, foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + hpxml_bldg.walls.each do |wall| + next if wall.net_area < 1.0 # skip modeling net surface area for surfaces comprised entirely of subsurface area + + if wall.azimuth.nil? + if wall.is_exterior + azimuths = default_azimuths # Model as four directions for average exterior incident solar + else + azimuths = [default_azimuths[0]] # Arbitrary direction, doesn't receive exterior incident solar + end + else + azimuths = [wall.azimuth] + end + + surfaces = [] + + azimuths.each do |azimuth| + height = 8.0 * hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade + length = (wall.net_area / height) / azimuths.size + z_origin = foundation_top + + vertices = create_wall_vertices(length, height, z_origin, azimuth) + surface = OpenStudio::Model::Surface.new(vertices, model) + surfaces << surface + surface.additionalProperties.setFeature('Length', length) + surface.additionalProperties.setFeature('Azimuth', azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'Wall') + if azimuths.size > 1 + surface.setName("#{wall.id}:#{azimuth}") + else + surface.setName(wall.id) + end + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, wall, hpxml_bldg) + set_surface_exterior(model, spaces, surface, wall, hpxml_bldg) + if wall.is_interior + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + end + end + + next if surfaces.empty? + + # Apply construction + # The code below constructs a reasonable wall construction based on the + # wall type while ensuring the correct assembly R-value. + has_radiant_barrier = wall.radiant_barrier + if has_radiant_barrier + radiant_barrier_grade = wall.radiant_barrier_grade + end + inside_film = Material.AirFilmVertical + if wall.is_exterior + outside_film = Material.AirFilmOutside + mat_ext_finish = Material.ExteriorFinishMaterial(wall.siding) + else + outside_film = Material.AirFilmVertical + mat_ext_finish = nil + end + if hpxml_header.apply_ashrae140_assumptions + inside_film = Material.AirFilmVerticalASHRAE140 + outside_film = Material.AirFilmOutsideASHRAE140 + end + mat_int_finish = Material.InteriorFinishMaterial(wall.interior_finish_type, wall.interior_finish_thickness) + + Constructions.apply_wall_construction(runner, model, surfaces, wall.id, wall.wall_type, wall.insulation_assembly_r_value, + mat_int_finish, has_radiant_barrier, inside_film, outside_film, + radiant_barrier_grade, mat_ext_finish, wall.solar_absorptance, + wall.emittance) + end + end + + # Adds any HPXML RimJoists to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [nil] + def self.apply_rim_joists(runner, model, spaces, hpxml_bldg) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + _walls_top, foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + hpxml_bldg.rim_joists.each do |rim_joist| + if rim_joist.azimuth.nil? + if rim_joist.is_exterior + azimuths = default_azimuths # Model as four directions for average exterior incident solar + else + azimuths = [default_azimuths[0]] # Arbitrary direction, doesn't receive exterior incident solar + end + else + azimuths = [rim_joist.azimuth] + end + + surfaces = [] + + azimuths.each do |azimuth| + height = 1.0 + length = (rim_joist.area / height) / azimuths.size + z_origin = foundation_top + + vertices = create_wall_vertices(length, height, z_origin, azimuth) + surface = OpenStudio::Model::Surface.new(vertices, model) + surfaces << surface + surface.additionalProperties.setFeature('Length', length) + surface.additionalProperties.setFeature('Azimuth', azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'RimJoist') + if azimuths.size > 1 + surface.setName("#{rim_joist.id}:#{azimuth}") + else + surface.setName(rim_joist.id) + end + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, rim_joist, hpxml_bldg) + set_surface_exterior(model, spaces, surface, rim_joist, hpxml_bldg) + if rim_joist.is_interior + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + end + end + + # Apply construction + + inside_film = Material.AirFilmVertical + if rim_joist.is_exterior + outside_film = Material.AirFilmOutside + mat_ext_finish = Material.ExteriorFinishMaterial(rim_joist.siding) + else + outside_film = Material.AirFilmVertical + mat_ext_finish = nil + end + + assembly_r = rim_joist.insulation_assembly_r_value + + constr_sets = [ + WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.17, 20.0, 2.0, nil, mat_ext_finish), # 2x4 + R20 + WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.17, 10.0, 2.0, nil, mat_ext_finish), # 2x4 + R10 + WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.17, 0.0, 2.0, nil, mat_ext_finish), # 2x4 + WoodStudConstructionSet.new(Material.Stud2x(2.0), 0.01, 0.0, 0.0, nil, mat_ext_finish), # Fallback + ] + match, constr_set, cavity_r = Constructions.pick_wood_stud_construction_set(assembly_r, constr_sets, inside_film, outside_film) + install_grade = 1 + + Constructions.apply_rim_joist(model, surfaces, "#{rim_joist.id} construction", + cavity_r, install_grade, constr_set.framing_factor, + constr_set.mat_int_finish, constr_set.osb_thick_in, + constr_set.rigid_r, constr_set.mat_ext_finish, + inside_film, outside_film, rim_joist.solar_absorptance, + rim_joist.emittance) + Constructions.check_surface_assembly_rvalue(runner, surfaces, inside_film, outside_film, assembly_r, match) + end + end + + # Adds any HPXML Floors to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_floors(runner, model, spaces, hpxml_bldg, hpxml_header) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + walls_top, foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + hpxml_bldg.floors.each do |floor| + next if floor.net_area < 1.0 # skip modeling net surface area for surfaces comprised entirely of subsurface area + + area = floor.net_area + width = Math::sqrt(area) + length = area / width + if floor.interior_adjacent_to.include?('attic') || floor.exterior_adjacent_to.include?('attic') + z_origin = walls_top + else + z_origin = foundation_top + end + + if floor.is_ceiling + vertices = create_ceiling_vertices(length, width, z_origin, default_azimuths) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('SurfaceType', 'Ceiling') + else + vertices = create_floor_vertices(length, width, z_origin, default_azimuths) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('SurfaceType', 'Floor') + end + surface.additionalProperties.setFeature('Tilt', 0.0) + set_surface_interior(model, spaces, surface, floor, hpxml_bldg) + set_surface_exterior(model, spaces, surface, floor, hpxml_bldg) + surface.setName(floor.id) + if floor.is_interior + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + elsif floor.is_floor + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + if floor.exterior_adjacent_to == HPXML::LocationManufacturedHomeUnderBelly + foundation = hpxml_bldg.foundations.find { |x| x.to_location == floor.exterior_adjacent_to } + if foundation.belly_wing_skirt_present + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + end + end + end + + # Apply construction + + if floor.is_ceiling + if hpxml_header.apply_ashrae140_assumptions + # Attic floor + inside_film = Material.AirFilmFloorASHRAE140 + outside_film = Material.AirFilmFloorASHRAE140 + else + inside_film = Material.AirFilmFloorAverage + outside_film = Material.AirFilmFloorAverage + end + mat_int_finish_or_covering = Material.InteriorFinishMaterial(floor.interior_finish_type, floor.interior_finish_thickness) + has_radiant_barrier = floor.radiant_barrier + if has_radiant_barrier + radiant_barrier_grade = floor.radiant_barrier_grade + end + else # Floor + if hpxml_header.apply_ashrae140_assumptions + # Raised floor + inside_film = Material.AirFilmFloorASHRAE140 + outside_film = Material.AirFilmFloorZeroWindASHRAE140 + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + mat_int_finish_or_covering = Material.CoveringBare(1.0) + else + inside_film = Material.AirFilmFloorReduced + if floor.is_exterior + outside_film = Material.AirFilmOutside + else + outside_film = Material.AirFilmFloorReduced + end + if floor.interior_adjacent_to == HPXML::LocationConditionedSpace + mat_int_finish_or_covering = Material.CoveringBare + end + end + end + + Constructions.apply_floor_ceiling_construction(runner, model, [surface], floor.id, floor.floor_type, floor.is_ceiling, floor.insulation_assembly_r_value, + mat_int_finish_or_covering, has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade) + end + end + + # Adds any HPXML Foundation Walls and Slabs to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @return [nil] + def self.apply_foundation_walls_slabs(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + + foundation_types = hpxml_bldg.slabs.map { |s| s.interior_adjacent_to }.uniq + foundation_types.each do |foundation_type| + # Get attached slabs/foundation walls + slabs = [] + hpxml_bldg.slabs.each do |slab| + next unless slab.interior_adjacent_to == foundation_type + + slabs << slab + slab.exposed_perimeter = [slab.exposed_perimeter, 1.0].max # minimum value to prevent error if no exposed slab + end + + slabs.each do |slab| + slab_frac = slab.exposed_perimeter / slabs.map { |s| s.exposed_perimeter }.sum + ext_fnd_walls = slab.connected_foundation_walls.select { |fw| fw.net_area >= 1.0 && fw.is_exterior } + + if ext_fnd_walls.empty? + # Slab w/o foundation walls + apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, -1 * slab.depth_below_grade.to_f, slab.exposed_perimeter, nil, schedules_file, default_azimuths) + else + # Slab w/ foundation walls + ext_fnd_walls_length = ext_fnd_walls.map { |fw| fw.area / fw.height }.sum + remaining_exposed_length = slab.exposed_perimeter + + # Since we don't know which FoundationWalls are adjacent to which Slabs, we apportion + # each FoundationWall to each slab. + ext_fnd_walls.each do |fnd_wall| + # Both the foundation wall and slab must have same exposed length to prevent Kiva errors. + # For the foundation wall, we are effectively modeling the net *exposed* area. + fnd_wall_length = fnd_wall.area / fnd_wall.height + apportioned_exposed_length = fnd_wall_length / ext_fnd_walls_length * slab.exposed_perimeter # Slab exposed perimeter apportioned to this foundation wall + apportioned_total_length = fnd_wall_length * slab_frac # Foundation wall length apportioned to this slab + exposed_length = [apportioned_exposed_length, apportioned_total_length].min + remaining_exposed_length -= exposed_length + + kiva_foundation = apply_foundation_wall(runner, model, spaces, hpxml_bldg, fnd_wall, exposed_length, fnd_wall_length, default_azimuths) + apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, -1 * fnd_wall.depth_below_grade, exposed_length, kiva_foundation, schedules_file, default_azimuths) + end + + if remaining_exposed_length > 1 # Skip if a small length (e.g., due to rounding) + # The slab's exposed perimeter exceeds the sum of attached exterior foundation wall lengths. + # This may legitimately occur for a walkout basement, where a portion of the slab has no + # adjacent foundation wall. + apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, 0, remaining_exposed_length, nil, schedules_file, default_azimuths) + end + end + end + + # Interzonal foundation wall surfaces + # The above-grade portion of these walls are modeled as EnergyPlus surfaces with standard adjacency. + # The below-grade portion of these walls (in contact with ground) are not modeled, as Kiva does not + # calculate heat flow between two zones through the ground. + int_fnd_walls = hpxml_bldg.foundation_walls.select { |fw| fw.is_interior && fw.interior_adjacent_to == foundation_type } + int_fnd_walls.each do |fnd_wall| + next unless fnd_wall.is_interior + + ag_height = fnd_wall.height - fnd_wall.depth_below_grade + ag_net_area = fnd_wall.net_area * ag_height / fnd_wall.height + next if ag_net_area < 1.0 + + length = ag_net_area / ag_height + z_origin = -1 * ag_height + if fnd_wall.azimuth.nil? + azimuth = default_azimuths[0] # Arbitrary direction, doesn't receive exterior incident solar + else + azimuth = fnd_wall.azimuth + end + + vertices = create_wall_vertices(length, ag_height, z_origin, azimuth) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('Length', length) + surface.additionalProperties.setFeature('Azimuth', azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'FoundationWall') + surface.setName(fnd_wall.id) + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, fnd_wall, hpxml_bldg) + set_surface_exterior(model, spaces, surface, fnd_wall, hpxml_bldg) + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + + # Apply construction + + wall_type = HPXML::WallTypeConcrete + inside_film = Material.AirFilmVertical + outside_film = Material.AirFilmVertical + assembly_r = fnd_wall.insulation_assembly_r_value + mat_int_finish = Material.InteriorFinishMaterial(fnd_wall.interior_finish_type, fnd_wall.interior_finish_thickness) + if assembly_r.nil? + concrete_thick_in = fnd_wall.thickness + int_r = fnd_wall.insulation_interior_r_value + ext_r = fnd_wall.insulation_exterior_r_value + mat_concrete = Material.Concrete(concrete_thick_in) + mat_int_finish_rvalue = mat_int_finish.nil? ? 0.0 : mat_int_finish.rvalue + assembly_r = int_r + ext_r + mat_concrete.rvalue + mat_int_finish_rvalue + inside_film.rvalue + outside_film.rvalue + end + mat_ext_finish = nil + + Constructions.apply_wall_construction(runner, model, [surface], fnd_wall.id, wall_type, assembly_r, mat_int_finish, + false, inside_film, outside_film, nil, mat_ext_finish, nil, nil) + end + end + end + + # Adds an HPXML Foundation Wall to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param foundation_wall [HPXML::FoundationWall] HPXML Foundation Wall object + # @param exposed_length [Double] TODO + # @param fnd_wall_length [Double] TODO + # @param default_azimuths [TODO] TODO + # @return [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object + def self.apply_foundation_wall(runner, model, spaces, hpxml_bldg, foundation_wall, exposed_length, fnd_wall_length, default_azimuths) + exposed_fraction = exposed_length / fnd_wall_length + net_exposed_area = foundation_wall.net_area * exposed_fraction + gross_exposed_area = foundation_wall.area * exposed_fraction + height = foundation_wall.height + height_ag = height - foundation_wall.depth_below_grade + z_origin = -1 * foundation_wall.depth_below_grade + if foundation_wall.azimuth.nil? + azimuth = default_azimuths[0] # Arbitrary; solar incidence in Kiva is applied as an orientation average (to the above grade portion of the wall) + else + azimuth = foundation_wall.azimuth + end + + return if exposed_length < 0.1 # Avoid Kiva error if exposed wall length is too small + + if gross_exposed_area > net_exposed_area + # Create a "notch" in the wall to account for the subsurfaces. This ensures that + # we preserve the appropriate wall height, length, and area for Kiva. + subsurface_area = gross_exposed_area - net_exposed_area + else + subsurface_area = 0 + end + + vertices = create_wall_vertices(exposed_length, height, z_origin, azimuth, subsurface_area: subsurface_area) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('Length', exposed_length) + surface.additionalProperties.setFeature('Azimuth', azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'FoundationWall') + surface.setName(foundation_wall.id) + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, foundation_wall, hpxml_bldg) + set_surface_exterior(model, spaces, surface, foundation_wall, hpxml_bldg) + + assembly_r = foundation_wall.insulation_assembly_r_value + mat_int_finish = Material.InteriorFinishMaterial(foundation_wall.interior_finish_type, foundation_wall.interior_finish_thickness) + mat_wall = Material.FoundationWallMaterial(foundation_wall.type, foundation_wall.thickness) + if not assembly_r.nil? + ext_rigid_height = height + ext_rigid_offset = 0.0 + inside_film = Material.AirFilmVertical + + mat_int_finish_rvalue = mat_int_finish.nil? ? 0.0 : mat_int_finish.rvalue + ext_rigid_r = assembly_r - mat_wall.rvalue - mat_int_finish_rvalue - inside_film.rvalue + int_rigid_r = 0.0 + if ext_rigid_r < 0 # Try without interior finish + mat_int_finish = nil + ext_rigid_r = assembly_r - mat_wall.rvalue - inside_film.rvalue + end + if (ext_rigid_r > 0) && (ext_rigid_r < 0.1) + ext_rigid_r = 0.0 # Prevent tiny strip of insulation + end + if ext_rigid_r < 0 + ext_rigid_r = 0.0 + match = false + else + match = true + end + else + ext_rigid_offset = foundation_wall.insulation_exterior_distance_to_top + ext_rigid_height = foundation_wall.insulation_exterior_distance_to_bottom - ext_rigid_offset + ext_rigid_r = foundation_wall.insulation_exterior_r_value + int_rigid_offset = foundation_wall.insulation_interior_distance_to_top + int_rigid_height = foundation_wall.insulation_interior_distance_to_bottom - int_rigid_offset + int_rigid_r = foundation_wall.insulation_interior_r_value + end + + soil_k_in = UnitConversions.convert(hpxml_bldg.site.ground_conductivity, 'ft', 'in') + + Constructions.apply_foundation_wall(model, [surface], "#{foundation_wall.id} construction", + ext_rigid_offset, int_rigid_offset, ext_rigid_height, int_rigid_height, + ext_rigid_r, int_rigid_r, mat_int_finish, mat_wall, height_ag, + soil_k_in) + + if not assembly_r.nil? + Constructions.check_surface_assembly_rvalue(runner, [surface], inside_film, nil, assembly_r, match) + end + + return surface.adjacentFoundation.get + end + + # Adds an HPXML Slab to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param slab [HPXML::Slab] HPXML Slab object + # @param z_origin [Double] The z-coordinate for which the slab is relative (ft) + # @param exposed_length [Double] TODO + # @param kiva_foundation [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param default_azimuths [TODO] TODO + # @return [nil] + def self.apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, z_origin, exposed_length, kiva_foundation, schedules_file, default_azimuths) + exposed_fraction = exposed_length / slab.exposed_perimeter + slab_tot_perim = exposed_length + slab_area = slab.area * exposed_fraction + if slab_tot_perim**2 - 16.0 * slab_area <= 0 + # Cannot construct rectangle with this perimeter/area. Some of the + # perimeter is presumably not exposed, so bump up perimeter value. + slab_tot_perim = Math.sqrt(16.0 * slab_area) + end + sqrt_term = [slab_tot_perim**2 - 16.0 * slab_area, 0.0].max + slab_length = slab_tot_perim / 4.0 + Math.sqrt(sqrt_term) / 4.0 + slab_width = slab_tot_perim / 4.0 - Math.sqrt(sqrt_term) / 4.0 + + vertices = create_floor_vertices(slab_length, slab_width, z_origin, default_azimuths) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.setName(slab.id) + surface.setSurfaceType(EPlus::SurfaceTypeFloor) + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionFoundation) + surface.additionalProperties.setFeature('SurfaceType', 'Slab') + set_surface_interior(model, spaces, surface, slab, hpxml_bldg) + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + + slab_perim_r = slab.perimeter_insulation_r_value + slab_perim_depth = slab.perimeter_insulation_depth + if (slab_perim_r == 0) || (slab_perim_depth == 0) + slab_perim_r = 0 + slab_perim_depth = 0 + end + + if slab.under_slab_insulation_spans_entire_slab + slab_whole_r = slab.under_slab_insulation_r_value + slab_under_r = 0 + slab_under_width = 0 + else + slab_under_r = slab.under_slab_insulation_r_value + slab_under_width = slab.under_slab_insulation_width + if (slab_under_r == 0) || (slab_under_width == 0) + slab_under_r = 0 + slab_under_width = 0 + end + slab_whole_r = 0 + end + slab_gap_r = slab.gap_insulation_r_value + + mat_carpet = nil + if (slab.carpet_fraction > 0) && (slab.carpet_r_value > 0) + mat_carpet = Material.CoveringBare(slab.carpet_fraction, + slab.carpet_r_value) + end + soil_k_in = UnitConversions.convert(hpxml_bldg.site.ground_conductivity, 'ft', 'in') + + ext_horiz_r = slab.exterior_horizontal_insulation_r_value + ext_horiz_width = slab.exterior_horizontal_insulation_width + ext_horiz_depth = slab.exterior_horizontal_insulation_depth_below_grade + + Constructions.apply_foundation_slab(model, surface, "#{slab.id} construction", + slab_under_r, slab_under_width, slab_gap_r, slab_perim_r, + slab_perim_depth, slab_whole_r, slab.thickness, + exposed_length, mat_carpet, soil_k_in, kiva_foundation, ext_horiz_r, ext_horiz_width, ext_horiz_depth) + + kiva_foundation = surface.adjacentFoundation.get + + foundation_walls_insulated = false + foundation_ceiling_insulated = false + hpxml_bldg.foundation_walls.each do |fnd_wall| + next unless fnd_wall.interior_adjacent_to == slab.interior_adjacent_to + next unless fnd_wall.exterior_adjacent_to == HPXML::LocationGround + + if fnd_wall.insulation_assembly_r_value.to_f > 5 + foundation_walls_insulated = true + elsif fnd_wall.insulation_exterior_r_value.to_f + fnd_wall.insulation_interior_r_value.to_f > 0 + foundation_walls_insulated = true + end + end + hpxml_bldg.floors.each do |floor| + next unless floor.interior_adjacent_to == HPXML::LocationConditionedSpace + next unless floor.exterior_adjacent_to == slab.interior_adjacent_to + + if floor.insulation_assembly_r_value > 5 + foundation_ceiling_insulated = true + end + end + + Constructions.apply_kiva_initial_temp(kiva_foundation, slab, weather, + spaces[HPXML::LocationConditionedSpace].thermalZone.get, + hpxml_header.sim_begin_month, hpxml_header.sim_begin_day, + hpxml_header.sim_calendar_year, schedules_file, + foundation_walls_insulated, foundation_ceiling_insulated) + + return kiva_foundation + end + + # Adds any HPXML Windows to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_windows(model, spaces, hpxml_bldg, hpxml_header) + # We already stored @fraction_of_windows_operable, so lets remove the + # fraction_operable properties from windows and re-collapse the enclosure + # so as to prevent potentially modeling multiple identical windows in E+, + # which can increase simulation runtime. + hpxml_bldg.windows.each do |window| + window.fraction_operable = nil + end + hpxml_bldg.collapse_enclosure_surfaces() + + _walls_top, foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + shading_schedules = {} + + surfaces = [] + hpxml_bldg.windows.each do |window| + window_height = 4.0 # ft, default + + overhang_depth = nil + if (not window.overhangs_depth.nil?) && (window.overhangs_depth > 0) + overhang_depth = window.overhangs_depth + overhang_distance_to_top = window.overhangs_distance_to_top_of_window + overhang_distance_to_bottom = window.overhangs_distance_to_bottom_of_window + window_height = overhang_distance_to_bottom - overhang_distance_to_top + end + + window_length = window.area / window_height + z_origin = foundation_top + + ufactor, shgc = Constructions.get_ufactor_shgc_adjusted_by_storms(window.storm_type, window.ufactor, window.shgc) + + if window.is_exterior + + # Create parent surface slightly bigger than window + vertices = create_wall_vertices(window_length, window_height, z_origin, window.azimuth, add_buffer: true) + surface = OpenStudio::Model::Surface.new(vertices, model) + + surface.additionalProperties.setFeature('Length', window_length) + surface.additionalProperties.setFeature('Azimuth', window.azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'Window') + surface.setName("surface #{window.id}") + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, window.wall, hpxml_bldg) + + vertices = create_wall_vertices(window_length, window_height, z_origin, window.azimuth) + sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) + sub_surface.setName(window.id) + sub_surface.setSurface(surface) + sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeWindow) + + set_subsurface_exterior(surface, spaces, model, window.wall, hpxml_bldg) + surfaces << surface + + if not overhang_depth.nil? + overhang = sub_surface.addOverhang(UnitConversions.convert(overhang_depth, 'ft', 'm'), UnitConversions.convert(overhang_distance_to_top, 'ft', 'm')) + overhang.get.setName("#{sub_surface.name} overhangs") + end + + # Apply construction + Constructions.apply_window(model, sub_surface, 'WindowConstruction', ufactor, shgc) + + # Apply interior/exterior shading (as needed) + Constructions.apply_window_skylight_shading(model, window, sub_surface, shading_schedules, hpxml_header, hpxml_bldg) + else + # Window is on an interior surface, which E+ does not allow. Model + # as a door instead so that we can get the appropriate conduction + # heat transfer; there is no solar gains anyway. + + # Create parent surface slightly bigger than window + vertices = create_wall_vertices(window_length, window_height, z_origin, window.azimuth, add_buffer: true) + surface = OpenStudio::Model::Surface.new(vertices, model) + + surface.additionalProperties.setFeature('Length', window_length) + surface.additionalProperties.setFeature('Azimuth', window.azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'Door') + surface.setName("surface #{window.id}") + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, window.wall, hpxml_bldg) + + vertices = create_wall_vertices(window_length, window_height, z_origin, window.azimuth) + sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) + sub_surface.setName(window.id) + sub_surface.setSurface(surface) + sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeDoor) + + set_subsurface_exterior(surface, spaces, model, window.wall, hpxml_bldg) + surfaces << surface + + # Apply construction + inside_film = Material.AirFilmVertical + outside_film = Material.AirFilmVertical + Constructions.apply_door(model, [sub_surface], 'Window', ufactor, inside_film, outside_film) + end + end + + Constructions.apply_adiabatic_construction(model, surfaces, 'wall') + end + + # Adds any HPXML Doors to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [nil] + def self.apply_doors(model, spaces, hpxml_bldg) + _walls_top, foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + surfaces = [] + hpxml_bldg.doors.each do |door| + door_height = 6.67 # ft + door_length = door.area / door_height + z_origin = foundation_top + + # Create parent surface slightly bigger than door + vertices = create_wall_vertices(door_length, door_height, z_origin, door.azimuth, add_buffer: true) + surface = OpenStudio::Model::Surface.new(vertices, model) + + surface.additionalProperties.setFeature('Length', door_length) + surface.additionalProperties.setFeature('Azimuth', door.azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'Door') + surface.setName("surface #{door.id}") + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, door.wall, hpxml_bldg) + + vertices = create_wall_vertices(door_length, door_height, z_origin, door.azimuth) + sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) + sub_surface.setName(door.id) + sub_surface.setSurface(surface) + sub_surface.setSubSurfaceType(EPlus::SubSurfaceTypeDoor) + + set_subsurface_exterior(surface, spaces, model, door.wall, hpxml_bldg) + surfaces << surface + + # Apply construction + ufactor = 1.0 / door.r_value + inside_film = Material.AirFilmVertical + if door.wall.is_exterior + outside_film = Material.AirFilmOutside + else + outside_film = Material.AirFilmVertical + end + Constructions.apply_door(model, [sub_surface], 'Door', ufactor, inside_film, outside_film) + end + + Constructions.apply_adiabatic_construction(model, surfaces, 'wall') + end + + # Adds any HPXML Skylights to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_skylights(model, spaces, hpxml_bldg, hpxml_header) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + walls_top, _foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + surfaces = [] + shading_schedules = {} + + hpxml_bldg.skylights.each do |skylight| + if not skylight.is_conditioned + fail "Skylight '#{skylight.id}' not connected to conditioned space; if it's a skylight with a shaft, use AttachedToFloor to connect it to conditioned space." + end + + tilt = skylight.roof.pitch / 12.0 + width = Math::sqrt(skylight.area) + length = skylight.area / width + z_origin = walls_top + 0.5 * Math.sin(Math.atan(tilt)) * width + + ufactor, shgc = Constructions.get_ufactor_shgc_adjusted_by_storms(skylight.storm_type, skylight.ufactor, skylight.shgc) + + if not skylight.curb_area.nil? + # Create parent surface that includes curb heat transfer + total_area = skylight.area + skylight.curb_area + total_width = Math::sqrt(total_area) + total_length = total_area / total_width + vertices = create_roof_vertices(total_length, total_width, z_origin, skylight.azimuth, tilt, add_buffer: true) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('Length', total_length) + surface.additionalProperties.setFeature('Width', total_width) + + # Assign curb construction + curb_assembly_r_value = [skylight.curb_assembly_r_value - Material.AirFilmVertical.rvalue - Material.AirFilmOutside.rvalue, 0.1].max + curb_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Rough', UnitConversions.convert(curb_assembly_r_value, 'hr*ft^2*f/btu', 'm^2*k/w')) + curb_mat.setName('SkylightCurbMaterial') + curb_const = OpenStudio::Model::Construction.new(model) + curb_const.setName('SkylightCurbConstruction') + curb_const.insertLayer(0, curb_mat) + surface.setConstruction(curb_const) + else + # Create parent surface slightly bigger than skylight + vertices = create_roof_vertices(length, width, z_origin, skylight.azimuth, tilt, add_buffer: true) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('Length', length) + surface.additionalProperties.setFeature('Width', width) + surfaces << surface # Add to surfaces list so it's assigned an adiabatic construction + end + surface.additionalProperties.setFeature('Azimuth', skylight.azimuth) + surface.additionalProperties.setFeature('Tilt', tilt) + surface.additionalProperties.setFeature('SurfaceType', 'Skylight') + surface.setName("surface #{skylight.id}") + surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) + surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, hpxml_bldg)) + surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionOutdoors) # cannot be adiabatic because subsurfaces won't be created + + vertices = create_roof_vertices(length, width, z_origin, skylight.azimuth, tilt) + sub_surface = OpenStudio::Model::SubSurface.new(vertices, model) + sub_surface.setName(skylight.id) + sub_surface.setSurface(surface) + sub_surface.setSubSurfaceType('Skylight') + + # Apply construction + Constructions.apply_skylight(model, sub_surface, 'SkylightConstruction', ufactor, shgc) + + # Apply interior/exterior shading (as needed) + Constructions.apply_window_skylight_shading(model, skylight, sub_surface, shading_schedules, hpxml_header, hpxml_bldg) + + next unless (not skylight.shaft_area.nil?) && (not skylight.floor.nil?) + + # Add skylight shaft heat transfer, similar to attic knee walls + + shaft_height = Math::sqrt(skylight.shaft_area) + shaft_width = skylight.shaft_area / shaft_height + shaft_azimuth = default_azimuths[0] # Arbitrary direction, doesn't receive exterior incident solar + shaft_z_origin = walls_top - shaft_height + + vertices = create_wall_vertices(shaft_width, shaft_height, shaft_z_origin, shaft_azimuth) + surface = OpenStudio::Model::Surface.new(vertices, model) + surface.additionalProperties.setFeature('Length', shaft_width) + surface.additionalProperties.setFeature('Width', shaft_height) + surface.additionalProperties.setFeature('Azimuth', shaft_azimuth) + surface.additionalProperties.setFeature('Tilt', 90.0) + surface.additionalProperties.setFeature('SurfaceType', 'Skylight') + surface.setName("surface #{skylight.id} shaft") + surface.setSurfaceType(EPlus::SurfaceTypeWall) + set_surface_interior(model, spaces, surface, skylight.floor, hpxml_bldg) + set_surface_exterior(model, spaces, surface, skylight.floor, hpxml_bldg) + surface.setSunExposure(EPlus::SurfaceSunExposureNo) + surface.setWindExposure(EPlus::SurfaceWindExposureNo) + + # Apply construction + shaft_assembly_r_value = [skylight.shaft_assembly_r_value - 2 * Material.AirFilmVertical.rvalue, 0.1].max + shaft_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(model, 'Rough', UnitConversions.convert(shaft_assembly_r_value, 'hr*ft^2*f/btu', 'm^2*k/w')) + shaft_mat.setName('SkylightShaftMaterial') + shaft_const = OpenStudio::Model::Construction.new(model) + shaft_const.setName('SkylightShaftConstruction') + shaft_const.insertLayer(0, shaft_mat) + surface.setConstruction(shaft_const) + end + + Constructions.apply_adiabatic_construction(model, surfaces, 'roof') + end + + # Check if we need to add floors between conditioned spaces (e.g., between first + # and second story or conditioned basement ceiling). + # This ensures that the E+ reported Conditioned Floor Area is correct. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [nil] + def self.apply_conditioned_floor_area(model, spaces, hpxml_bldg) + default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) + _walls_top, foundation_top = get_foundation_and_walls_top(hpxml_bldg) + + sum_cfa = 0.0 + hpxml_bldg.floors.each do |floor| + next unless floor.is_floor + next unless [HPXML::LocationConditionedSpace, HPXML::LocationBasementConditioned].include?(floor.interior_adjacent_to) || + [HPXML::LocationConditionedSpace, HPXML::LocationBasementConditioned].include?(floor.exterior_adjacent_to) + + sum_cfa += floor.area + end + hpxml_bldg.slabs.each do |slab| + next unless [HPXML::LocationConditionedSpace, HPXML::LocationBasementConditioned].include? slab.interior_adjacent_to + + sum_cfa += slab.area + end + + addtl_cfa = hpxml_bldg.building_construction.conditioned_floor_area - sum_cfa + + fail if addtl_cfa < -1.0 # Allow some rounding; EPvalidator.xml should prevent this + + return unless addtl_cfa > 1.0 # Allow some rounding + + floor_width = Math::sqrt(addtl_cfa) + floor_length = addtl_cfa / floor_width + z_origin = foundation_top + 8.0 * (hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade - 1) + + # Add floor surface + vertices = create_floor_vertices(floor_length, floor_width, z_origin, default_azimuths) + floor_surface = OpenStudio::Model::Surface.new(vertices, model) + + floor_surface.setSunExposure(EPlus::SurfaceSunExposureNo) + floor_surface.setWindExposure(EPlus::SurfaceWindExposureNo) + floor_surface.setName('inferred conditioned floor') + floor_surface.setSurfaceType(EPlus::SurfaceTypeFloor) + floor_surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, hpxml_bldg)) + floor_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) + floor_surface.additionalProperties.setFeature('SurfaceType', 'InferredFloor') + floor_surface.additionalProperties.setFeature('Tilt', 0.0) + + # Add ceiling surface + vertices = create_ceiling_vertices(floor_length, floor_width, z_origin, default_azimuths) + ceiling_surface = OpenStudio::Model::Surface.new(vertices, model) + + ceiling_surface.setSunExposure(EPlus::SurfaceSunExposureNo) + ceiling_surface.setWindExposure(EPlus::SurfaceWindExposureNo) + ceiling_surface.setName('inferred conditioned ceiling') + ceiling_surface.setSurfaceType(EPlus::SurfaceTypeRoofCeiling) + ceiling_surface.setSpace(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, hpxml_bldg)) + ceiling_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic) + ceiling_surface.additionalProperties.setFeature('SurfaceType', 'InferredCeiling') + ceiling_surface.additionalProperties.setFeature('Tilt', 0.0) + + # Apply Construction + Constructions.apply_adiabatic_construction(model, [floor_surface, ceiling_surface], 'floor') + end + + # Calls construction methods for applying partition walls and furniture to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_thermal_mass(model, spaces, hpxml_bldg, hpxml_header) + if hpxml_header.apply_ashrae140_assumptions + # 1024 ft2 of interior partition wall mass, no furniture mass + mat_int_finish = Material.InteriorFinishMaterial(HPXML::InteriorFinishGypsumBoard, 0.5) + partition_wall_area = 1024.0 * 2 # Exposed partition wall area (both sides) + Constructions.apply_partition_walls(model, 'PartitionWallConstruction', mat_int_finish, partition_wall_area, spaces) + else + mat_int_finish = Material.InteriorFinishMaterial(hpxml_bldg.partition_wall_mass.interior_finish_type, hpxml_bldg.partition_wall_mass.interior_finish_thickness) + partition_wall_area = hpxml_bldg.partition_wall_mass.area_fraction * hpxml_bldg.building_construction.conditioned_floor_area # Exposed partition wall area (both sides) + Constructions.apply_partition_walls(model, 'PartitionWallConstruction', mat_int_finish, partition_wall_area, spaces) + + Constructions.apply_furniture(model, hpxml_bldg.furniture_mass, spaces) + end + end + + # Calculates the assumed above-grade height of the top of the dwelling unit's walls and foundation walls. + # + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [Array] Top of the walls (ft), top of the foundation walls (ft) + def self.get_foundation_and_walls_top(hpxml_bldg) + foundation_top = [hpxml_bldg.building_construction.unit_height_above_grade, 0].max + hpxml_bldg.foundation_walls.each do |foundation_wall| + top = -1 * foundation_wall.depth_below_grade + foundation_wall.height + foundation_top = top if top > foundation_top + end + ncfl_ag = hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade + walls_top = foundation_top + hpxml_bldg.building_construction.average_ceiling_height * ncfl_ag + + return walls_top, foundation_top + end + # Get the largest z difference for a surface. # # @param surface [OpenStudio::Model::Surface] an OpenStudio::Model::Surface object @@ -77,10 +1140,7 @@ def self.get_occupancy_default_num(nbeds:) # @param location [String] HPXML location # @param zone_multiplier [Integer] the number of similar zones represented # @return [OpenStudio::Model::Space, nil] updated spaces hash if location is not already a key - def self.create_space_and_zone(model:, - spaces:, - location:, - zone_multiplier:) + def self.create_space_and_zone(model, spaces, location, zone_multiplier) if not spaces.keys.include? location thermal_zone = OpenStudio::Model::ThermalZone.new(model) thermal_zone.setName(location) @@ -104,18 +1164,13 @@ def self.create_space_and_zone(model:, # @param tilt [TODO] TODO # @param add_buffer [TODO] TODO # @return [TODO] TODO - def self.create_roof_vertices(length:, - width:, - z_origin:, - azimuth:, - tilt:, - add_buffer: false) + def self.create_roof_vertices(length, width, z_origin, azimuth, tilt, add_buffer: false) length = UnitConversions.convert(length, 'ft', 'm') width = UnitConversions.convert(width, 'ft', 'm') z_origin = UnitConversions.convert(z_origin, 'ft', 'm') if add_buffer - buffer = calculate_subsurface_parent_buffer(length: length, width: width) + buffer = calculate_subsurface_parent_buffer(length, width) buffer /= 2.0 # Buffer on each side else buffer = 0 @@ -184,18 +1239,13 @@ def self.get_roof_pitch(surfaces) # @param add_buffer [Boolean] whether to use a buffer on each side of a subsurface # @param subsurface_area [Double] the area of a subsurface within the parent surface (ft2) # @return [OpenStudio::Point3dVector] an array of points - def self.create_wall_vertices(length:, - height:, - z_origin:, - azimuth:, - add_buffer: false, - subsurface_area: 0) + def self.create_wall_vertices(length, height, z_origin, azimuth, add_buffer: false, subsurface_area: 0) length = UnitConversions.convert(length, 'ft', 'm') height = UnitConversions.convert(height, 'ft', 'm') z_origin = UnitConversions.convert(z_origin, 'ft', 'm') if add_buffer - buffer = calculate_subsurface_parent_buffer(length: length, width: height) + buffer = calculate_subsurface_parent_buffer(length, height) buffer /= 2.0 # Buffer on each side else buffer = 0 @@ -241,11 +1291,8 @@ def self.create_wall_vertices(length:, # @param z_origin [Double] The z-coordinate for which the length and width are relative (ft) # @param default_azimuths [TODO] TODO # @return [TODO] TODO - def self.create_ceiling_vertices(length:, - width:, - z_origin:, - default_azimuths:) - return OpenStudio::reverse(create_floor_vertices(length: length, width: width, z_origin: z_origin, default_azimuths: default_azimuths)) + def self.create_ceiling_vertices(length, width, z_origin, default_azimuths) + return OpenStudio::reverse(create_floor_vertices(length, width, z_origin, default_azimuths)) end # TODO @@ -255,10 +1302,7 @@ def self.create_ceiling_vertices(length:, # @param z_origin [Double] The z-coordinate for which the length and width are relative (ft) # @param default_azimuths [TODO] TODO # @return [TODO] TODO - def self.create_floor_vertices(length:, - width:, - z_origin:, - default_azimuths:) + def self.create_floor_vertices(length, width, z_origin, default_azimuths) length = UnitConversions.convert(length, 'ft', 'm') width = UnitConversions.convert(width, 'ft', 'm') z_origin = UnitConversions.convert(z_origin, 'ft', 'm') @@ -290,11 +1334,11 @@ def self.create_floor_vertices(length:, # # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param apply_ashrae140_assumptions [Boolean] TODO + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @return [nil] - def self.set_zone_volumes(spaces:, - hpxml_bldg:, - apply_ashrae140_assumptions:) + def self.set_zone_volumes(spaces, hpxml_bldg, hpxml_header) + apply_ashrae140_assumptions = hpxml_header.apply_ashrae140_assumptions + # Conditioned space volume = UnitConversions.convert(hpxml_bldg.building_construction.conditioned_building_volume, 'ft^3', 'm^3') spaces[HPXML::LocationConditionedSpace].thermalZone.get.setVolume(volume) @@ -304,7 +1348,7 @@ def self.set_zone_volumes(spaces:, spaces.keys.each do |location| next unless [HPXML::LocationBasementUnconditioned, HPXML::LocationCrawlspaceUnvented, HPXML::LocationCrawlspaceVented, HPXML::LocationGarage].include? location - volume = UnitConversions.convert(calculate_zone_volume(hpxml_bldg: hpxml_bldg, location: location), 'ft^3', 'm^3') + volume = UnitConversions.convert(calculate_zone_volume(hpxml_bldg, location), 'ft^3', 'm^3') spaces[location].thermalZone.get.setVolume(volume) spaces[location].setVolume(volume) end @@ -316,7 +1360,7 @@ def self.set_zone_volumes(spaces:, if apply_ashrae140_assumptions volume = UnitConversions.convert(3463, 'ft^3', 'm^3') # Hardcode the attic volume to match ASHRAE 140 Table 7-2 specification else - volume = UnitConversions.convert(calculate_zone_volume(hpxml_bldg: hpxml_bldg, location: location), 'ft^3', 'm^3') + volume = UnitConversions.convert(calculate_zone_volume(hpxml_bldg, location), 'ft^3', 'm^3') end spaces[location].thermalZone.get.setVolume(volume) @@ -329,11 +1373,8 @@ def self.set_zone_volumes(spaces:, # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param walls_top [Double] the total height of the dwelling unit # @return [nil] - def self.explode_surfaces(model:, - hpxml_bldg:, - walls_top:) + def self.explode_surfaces(model, hpxml_bldg) gap_distance = UnitConversions.convert(10.0, 'ft', 'm') # distance between surfaces of the same azimuth rad90 = UnitConversions.convert(90, 'deg', 'rad') @@ -376,7 +1417,7 @@ def self.explode_surfaces(model:, end explode_distance = max_azimuth_length / (2.0 * Math.tan(UnitConversions.convert(180.0 / nsides, 'deg', 'rad'))) - add_neighbor_shading(model: model, length: max_azimuth_length, hpxml_bldg: hpxml_bldg, walls_top: walls_top) + add_neighbor_shading(model, max_azimuth_length, hpxml_bldg) # Initial distance of shifts at 90-degrees to horizontal outward azimuth_side_shifts = {} @@ -505,7 +1546,7 @@ def self.shift_surfaces(model, unit_number) # # @param zone [TODO] TODO # @return [TODO] TODO - def self.get_z_origin_for_zone(zone:) + def self.get_z_origin_for_zone(zone) z_origins = [] zone.spaces.each do |space| z_origins << UnitConversions.convert(space.zOrigin, 'm', 'ft') @@ -521,10 +1562,7 @@ def self.get_z_origin_for_zone(zone:) # @param y [Double] the y-coordinate of the translation vector # @param z [Double] the z-coordinate of the translation vector # @return [OpenStudio::Transformation] the OpenStudio transformation object - def self.get_surface_transformation(offset:, - x:, - y:, - z:) + def self.get_surface_transformation(offset:, x:, y:, z:) x = UnitConversions.convert(x, 'ft', 'm') y = UnitConversions.convert(y, 'ft', 'm') z = UnitConversions.convert(z, 'ft', 'm') @@ -546,19 +1584,16 @@ def self.get_surface_transformation(offset:, # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param length [TODO] TODO # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param walls_top [TODO] TODO # @return [nil] - def self.add_neighbor_shading(model:, - length:, - hpxml_bldg:, - walls_top:) + def self.add_neighbor_shading(model, length, hpxml_bldg) + walls_top, _foundation_top = get_foundation_and_walls_top(hpxml_bldg) z_origin = 0 # shading surface always starts at grade shading_surfaces = [] hpxml_bldg.neighbor_buildings.each do |neighbor_building| height = neighbor_building.height.nil? ? walls_top : neighbor_building.height - vertices = create_wall_vertices(length: length, height: height, z_origin: z_origin, azimuth: neighbor_building.azimuth) + vertices = create_wall_vertices(length, height, z_origin, neighbor_building.azimuth) shading_surface = OpenStudio::Model::ShadingSurface.new(vertices, model) shading_surface.additionalProperties.setFeature('Azimuth', neighbor_building.azimuth) shading_surface.additionalProperties.setFeature('Distance', neighbor_building.distance) @@ -581,8 +1616,7 @@ def self.add_neighbor_shading(model:, # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param location [String] the location of interest (HPXML::LocationXXX) # @return [Double] TODO - def self.calculate_zone_volume(hpxml_bldg:, - location:) + def self.calculate_zone_volume(hpxml_bldg, location) if [HPXML::LocationBasementUnconditioned, HPXML::LocationCrawlspaceUnvented, HPXML::LocationCrawlspaceVented, @@ -612,7 +1646,7 @@ def self.calculate_zone_volume(hpxml_bldg:, # # @param location [String] the general HPXML location # @return [Hash] TODO - def self.get_temperature_scheduled_space_values(location:) + def self.get_temperature_scheduled_space_values(location) if location == HPXML::LocationOtherHeatedSpace # Average of indoor/outdoor temperatures with minimum of heating setpoint return { temp_min: 68, @@ -776,7 +1810,7 @@ def self.get_space_temperature_schedule(model, location, spaces) sch.setName(location) sch.additionalProperties.setFeature('ObjectType', location) - space_values = get_temperature_scheduled_space_values(location: location) + space_values = get_temperature_scheduled_space_values(location) htg_weekday_setpoints, htg_weekend_setpoints = HVAC.get_default_heating_setpoint(HPXML::HVACControlTypeManual, @eri_version) if htg_weekday_setpoints.split(', ').uniq.size == 1 && htg_weekend_setpoints.split(', ').uniq.size == 1 && htg_weekday_setpoints.split(', ').uniq == htg_weekend_setpoints.split(', ').uniq @@ -958,8 +1992,7 @@ def self.get_surface_length(surface:) # @param length [Double] length of the subsurface (m) # @param width [Double] width of the subsurface (m) # @return [Double] minimum needed buffer distance (m) - def self.calculate_subsurface_parent_buffer(length:, - width:) + def self.calculate_subsurface_parent_buffer(length, width) min_surface_area = 0.005 # m^2 return 0.5 * (((length + width)**2 + 4.0 * min_surface_area)**0.5 - length - width) end @@ -974,8 +2007,23 @@ def self.calculate_subsurface_parent_buffer(length:, # @return [OpenStudio::Model::Space] the OpenStudio::Model::Space object corresponding to HPXML::LocationXXX def self.create_or_get_space(model, spaces, location, hpxml_bldg) if spaces[location].nil? - create_space_and_zone(model: model, spaces: spaces, location: location, zone_multiplier: hpxml_bldg.building_construction.number_of_units) + create_space_and_zone(model, spaces, location, hpxml_bldg.building_construction.number_of_units) end return spaces[location] end + + # Store the HPXML Building object unit number for use in reporting measure. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param unit_number [Integer] index number corresponding to an HPXML Building object + # @return [nil] + def self.apply_building_unit(model, unit_num) + return if unit_num.nil? + + unit = OpenStudio::Model::BuildingUnit.new(model) + unit.additionalProperties.setFeature('unit_num', unit_num) + model.getSpaces.each do |s| + s.setBuildingUnit(unit) + end + end end diff --git a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb index 43f7f91119..af3db0a867 100644 --- a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb +++ b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb @@ -62,7 +62,8 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat # Clothes washer energy if not clothes_washer.nil? - cw_annual_kwh, cw_frac_sens, cw_frac_lat, cw_gpd = calc_clothes_washer_energy_gpd(eri_version, nbeds, clothes_washer, clothes_washer.additional_properties.space.nil?, n_occ) + cw_space = Geometry.get_space_from_location(clothes_washer.location, spaces) + cw_annual_kwh, cw_frac_sens, cw_frac_lat, cw_gpd = calc_clothes_washer_energy_gpd(eri_version, nbeds, clothes_washer, cw_space.nil?, n_occ) # Create schedule cw_power_schedule = nil @@ -86,14 +87,14 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat runner.registerWarning("Both '#{cw_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !clothes_washer.monthly_multipliers.nil? end - cw_space = clothes_washer.additional_properties.space cw_space = conditioned_space if cw_space.nil? # appliance is outdoors, so we need to assign the equipment to an arbitrary space add_electric_equipment(model, cw_object_name, cw_space, cw_design_level_w, cw_frac_sens, cw_frac_lat, cw_power_schedule) end # Clothes dryer energy if not clothes_dryer.nil? - cd_annual_kwh, cd_annual_therm, cd_frac_sens, cd_frac_lat = calc_clothes_dryer_energy(eri_version, nbeds, clothes_dryer, clothes_washer, clothes_dryer.additional_properties.space.nil?, n_occ) + cd_space = Geometry.get_space_from_location(clothes_dryer.location, spaces) + cd_annual_kwh, cd_annual_therm, cd_frac_sens, cd_frac_lat = calc_clothes_dryer_energy(eri_version, nbeds, clothes_dryer, clothes_washer, cd_space.nil?, n_occ) # Create schedule cd_schedule = nil @@ -119,7 +120,6 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat runner.registerWarning("Both '#{cd_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !clothes_dryer.monthly_multipliers.nil? end - cd_space = clothes_dryer.additional_properties.space cd_space = conditioned_space if cd_space.nil? # appliance is outdoors, so we need to assign the equipment to an arbitrary space add_electric_equipment(model, cd_obj_name, cd_space, cd_design_level_e, cd_frac_sens, cd_frac_lat, cd_schedule) add_other_equipment(model, cd_obj_name, cd_space, cd_design_level_f, cd_frac_sens, cd_frac_lat, cd_schedule, clothes_dryer.fuel_type) @@ -127,7 +127,8 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat # Dishwasher energy if not dishwasher.nil? - dw_annual_kwh, dw_frac_sens, dw_frac_lat, dw_gpd = calc_dishwasher_energy_gpd(eri_version, nbeds, dishwasher, dishwasher.additional_properties.space.nil?, n_occ) + dw_space = Geometry.get_space_from_location(dishwasher.location, spaces) + dw_annual_kwh, dw_frac_sens, dw_frac_lat, dw_gpd = calc_dishwasher_energy_gpd(eri_version, nbeds, dishwasher, dw_space.nil?, n_occ) # Create schedule dw_power_schedule = nil @@ -151,14 +152,14 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat runner.registerWarning("Both '#{dw_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !dishwasher.monthly_multipliers.nil? end - dw_space = dishwasher.additional_properties.space dw_space = conditioned_space if dw_space.nil? # appliance is outdoors, so we need to assign the equipment to an arbitrary space add_electric_equipment(model, dw_obj_name, dw_space, dw_design_level_w, dw_frac_sens, dw_frac_lat, dw_power_schedule) end # Refrigerator(s) energy hpxml_bldg.refrigerators.each do |refrigerator| - rf_annual_kwh, rf_frac_sens, rf_frac_lat = calc_refrigerator_or_freezer_energy(refrigerator, refrigerator.additional_properties.loc_space.nil?) + rf_space, rf_schedule = Geometry.get_space_or_schedule_from_location(refrigerator.location, model, spaces) + rf_annual_kwh, rf_frac_sens, rf_frac_lat = calc_fridge_or_freezer_energy(refrigerator, rf_space.nil?) # Create schedule fridge_schedule = nil @@ -174,7 +175,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat # if both weekday_fractions/weekend_fractions/monthly_multipliers and constant_coefficients/temperature_coefficients provided, ignore the former if !refrigerator.constant_coefficients.nil? && !refrigerator.temperature_coefficients.nil? fridge_design_level = UnitConversions.convert(rf_annual_kwh / 8760.0, 'kW', 'W') - fridge_schedule = refrigerator_or_freezer_coefficients_schedule(model, fridge_col_name, fridge_obj_name, refrigerator, fridge_unavailable_periods) + fridge_schedule = fridge_or_freezer_coefficients_schedule(model, fridge_col_name, fridge_obj_name, refrigerator, rf_space, rf_schedule, fridge_unavailable_periods) elsif !refrigerator.weekday_fractions.nil? && !refrigerator.weekend_fractions.nil? && !refrigerator.monthly_multipliers.nil? fridge_weekday_sch = refrigerator.weekday_fractions fridge_weekend_sch = refrigerator.weekend_fractions @@ -192,7 +193,6 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat runner.registerWarning("Both '#{fridge_col_name}' schedule file and temperature coefficients provided; the latter will be ignored.") if !refrigerator.temperature_coefficients.nil? end - rf_space = refrigerator.additional_properties.loc_space rf_space = conditioned_space if rf_space.nil? # appliance is outdoors, so we need to assign the equipment to an arbitrary space add_electric_equipment(model, fridge_obj_name, rf_space, fridge_design_level, rf_frac_sens, rf_frac_lat, fridge_schedule) @@ -200,7 +200,8 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat # Freezer(s) energy hpxml_bldg.freezers.each do |freezer| - fz_annual_kwh, fz_frac_sens, fz_frac_lat = calc_refrigerator_or_freezer_energy(freezer, freezer.additional_properties.loc_space.nil?) + fz_space, fz_schedule = Geometry.get_space_or_schedule_from_location(freezer.location, model, spaces) + fz_annual_kwh, fz_frac_sens, fz_frac_lat = calc_fridge_or_freezer_energy(freezer, fz_space.nil?) # Create schedule freezer_schedule = nil @@ -216,7 +217,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat # if both weekday_fractions/weekend_fractions/monthly_multipliers and constant_coefficients/temperature_coefficients provided, ignore the former if !freezer.constant_coefficients.nil? && !freezer.temperature_coefficients.nil? freezer_design_level = UnitConversions.convert(fz_annual_kwh / 8760.0, 'kW', 'W') - freezer_schedule = refrigerator_or_freezer_coefficients_schedule(model, freezer_col_name, freezer_obj_name, freezer, freezer_unavailable_periods) + freezer_schedule = fridge_or_freezer_coefficients_schedule(model, freezer_col_name, freezer_obj_name, freezer, fz_space, fz_schedule, freezer_unavailable_periods) elsif !freezer.weekday_fractions.nil? && !freezer.weekend_fractions.nil? && !freezer.monthly_multipliers.nil? freezer_weekday_sch = freezer.weekday_fractions freezer_weekend_sch = freezer.weekend_fractions @@ -234,7 +235,6 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat runner.registerWarning("Both '#{freezer_col_name}' schedule file and temperature coefficients provided; the latter will be ignored.") if !freezer.temperature_coefficients.nil? end - fz_space = freezer.additional_properties.loc_space fz_space = conditioned_space if fz_space.nil? # appliance is outdoors, so we need to assign the equipment to an arbitrary space add_electric_equipment(model, freezer_obj_name, fz_space, freezer_design_level, fz_frac_sens, fz_frac_lat, freezer_schedule) @@ -242,7 +242,8 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat # Cooking Range energy if not cooking_range.nil? - cook_annual_kwh, cook_annual_therm, cook_frac_sens, cook_frac_lat = calc_range_oven_energy(nbeds_eq, cooking_range, oven, cooking_range.additional_properties.space.nil?) + cook_space = Geometry.get_space_from_location(cooking_range.location, spaces) + cook_annual_kwh, cook_annual_therm, cook_frac_sens, cook_frac_lat = calc_range_oven_energy(nbeds_eq, cooking_range, oven, cook_space.nil?) # Create schedule cook_schedule = nil @@ -268,7 +269,6 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat runner.registerWarning("Both '#{cook_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !cooking_range.monthly_multipliers.nil? end - cook_space = cooking_range.additional_properties.space cook_space = conditioned_space if cook_space.nil? # appliance is outdoors, so we need to assign the equipment to an arbitrary space add_electric_equipment(model, cook_obj_name, cook_space, cook_design_level_e, cook_frac_sens, cook_frac_lat, cook_schedule) add_other_equipment(model, cook_obj_name, cook_space, cook_design_level_f, cook_frac_sens, cook_frac_lat, cook_schedule, cooking_range.fuel_type) @@ -848,13 +848,13 @@ def self.calc_clothes_washer_mef_from_imef(imef) # TODO # - # @param refrigerator_or_freezer [TODO] TODO + # @param fridge_or_freezer [TODO] TODO # @param is_outside [TODO] TODO # @return [TODO] TODO - def self.calc_refrigerator_or_freezer_energy(refrigerator_or_freezer, is_outside = false) + def self.calc_fridge_or_freezer_energy(fridge_or_freezer, is_outside = false) # Get values - annual_kwh = refrigerator_or_freezer.rated_annual_kwh - annual_kwh *= refrigerator_or_freezer.usage_multiplier + annual_kwh = fridge_or_freezer.rated_annual_kwh + annual_kwh *= fridge_or_freezer.usage_multiplier if not is_outside frac_sens = 1.0 frac_lat = 0.0 @@ -876,10 +876,12 @@ def self.calc_refrigerator_or_freezer_energy(refrigerator_or_freezer, is_outside # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param col_name [TODO] TODO # @param obj_name [String] Name for the OpenStudio object - # @param refrigerator_or_freezer [TODO] TODO + # @param fridge_or_freezer [TODO] TODO + # @param loc_space [TODO] TODO + # @param loc_schedule [TODO] TODO # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @return [TODO] TODO - def self.refrigerator_or_freezer_coefficients_schedule(model, col_name, obj_name, refrigerator_or_freezer, unavailable_periods) + def self.fridge_or_freezer_coefficients_schedule(model, col_name, obj_name, fridge_or_freezer, loc_space, loc_schedule, unavailable_periods) # Create availability sensor if not unavailable_periods.empty? avail_sch = ScheduleConstant.new(model, col_name, 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: unavailable_periods) @@ -892,21 +894,21 @@ def self.refrigerator_or_freezer_coefficients_schedule(model, col_name, obj_name schedule = OpenStudio::Model::ScheduleConstant.new(model) schedule.setName(obj_name + ' schedule') - if not refrigerator_or_freezer.additional_properties.loc_space.nil? + if not loc_space.nil? temperature_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mean Air Temperature') temperature_sensor.setName(obj_name + ' tin s') - temperature_sensor.setKeyName(refrigerator_or_freezer.additional_properties.loc_space.thermalZone.get.name.to_s) - elsif not refrigerator_or_freezer.additional_properties.loc_schedule.nil? + temperature_sensor.setKeyName(loc_space.thermalZone.get.name.to_s) + elsif not loc_schedule.nil? temperature_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') temperature_sensor.setName(obj_name + ' tin s') - temperature_sensor.setKeyName(refrigerator_or_freezer.additional_properties.loc_schedule.name.to_s) + temperature_sensor.setKeyName(loc_schedule.name.to_s) end schedule_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(schedule, *EPlus::EMSActuatorScheduleConstantValue) schedule_actuator.setName("#{schedule.name} act") - constant_coefficients = refrigerator_or_freezer.constant_coefficients.split(',').map { |i| i.to_f } - temperature_coefficients = refrigerator_or_freezer.temperature_coefficients.split(',').map { |i| i.to_f } + constant_coefficients = fridge_or_freezer.constant_coefficients.split(',').map { |i| i.to_f } + temperature_coefficients = fridge_or_freezer.temperature_coefficients.split(',').map { |i| i.to_f } schedule_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) schedule_program.setName("#{schedule.name} program") diff --git a/HPXMLtoOpenStudio/resources/hpxml_defaults.rb b/HPXMLtoOpenStudio/resources/hpxml_defaults.rb index 23dad1eb5f..2738b27971 100644 --- a/HPXMLtoOpenStudio/resources/hpxml_defaults.rb +++ b/HPXMLtoOpenStudio/resources/hpxml_defaults.rb @@ -22,19 +22,19 @@ module HPXMLDefaults # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param hpxml [HPXML] HPXML object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param weather [WeatherFile] Weather object containing EPW information # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param convert_shared_systems [Boolean] Whether to convert shared systems to equivalent in-unit systems per ANSI 301 # @param design_load_details_output_file_path [String] Detailed HVAC sizing output file path # @param output_format [String] Detailed HVAC sizing output file format ('csv', 'json', or 'msgpack') # @return [nil] - def self.apply(runner, hpxml, hpxml_bldg, eri_version, weather, schedules_file: nil, convert_shared_systems: true, + def self.apply(runner, hpxml, hpxml_bldg, weather, schedules_file: nil, convert_shared_systems: true, design_load_details_output_file_path: nil, output_format: 'csv') cfa = hpxml_bldg.building_construction.conditioned_floor_area nbeds = hpxml_bldg.building_construction.number_of_bedrooms ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors ncfl_ag = hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade + eri_version = hpxml.header.eri_calculation_version if hpxml.buildings.size > 1 # This is helpful if we need to make unique HPXML IDs across dwelling units diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index 475c2d9b62..c3e09607f2 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -8,6 +8,250 @@ module HVAC AirSourceCoolRatedIWB = 67.0 # degF, Rated indoor wetbulb for air-source systems, cooling CrankcaseHeaterTemp = 50.0 # degF + # TODO + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # + # @return [Array] Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil), TODO + def self.apply_hvac_systems(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + return if hpxml_bldg.hvac_controls.size == 0 + + # Init + @remaining_heat_load_frac = 1.0 + @remaining_cool_load_frac = 1.0 + @hp_backup_system_object = nil + @hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, hpxml_header.unavailable_periods) + + # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling + # season begin/end month/day, respectively. + hvac_control = hpxml_bldg.hvac_controls[0] + htg_start_month = hvac_control.seasons_heating_begin_month + htg_start_day = hvac_control.seasons_heating_begin_day + htg_end_month = hvac_control.seasons_heating_end_month + htg_end_day = hvac_control.seasons_heating_end_day + clg_start_month = hvac_control.seasons_cooling_begin_month + clg_start_day = hvac_control.seasons_cooling_begin_day + clg_end_month = hvac_control.seasons_cooling_end_month + clg_end_day = hvac_control.seasons_cooling_end_day + if (htg_start_month != 1) || (htg_start_day != 1) || (htg_end_month != 12) || (htg_end_day != 31) || (clg_start_month != 1) || (clg_start_day != 1) || (clg_end_month != 12) || (clg_end_day != 31) + runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.') + end + @heating_days = Calendar.get_daily_season(hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) + @cooling_days = Calendar.get_daily_season(hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) + + airloop_map = {} + + apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header) + apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + + return airloop_map, @hvac_unavailable_periods + end + + # Adds any HPXML Cooling Systems to the OpenStudio model. + # TODO for adding more description (e.g., around sequential load fractions) + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] + def self.apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + + get_hpxml_hvac_systems(hpxml_bldg).each do |hvac_system| + next if hvac_system[:cooling].nil? + next unless hvac_system[:cooling].is_a? HPXML::CoolingSystem + + cooling_system = hvac_system[:cooling] + heating_system = hvac_system[:heating] + + check_distribution_system(cooling_system.distribution_system, cooling_system.cooling_system_type) + + # Calculate cooling sequential load fractions + sequential_cool_load_fracs = calc_sequential_load_fractions(cooling_system.fraction_cool_load_served.to_f, @remaining_cool_load_frac, @cooling_days) + @remaining_cool_load_frac -= cooling_system.fraction_cool_load_served.to_f + + # Calculate heating sequential load fractions + if not heating_system.nil? + sequential_heat_load_fracs = calc_sequential_load_fractions(heating_system.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) + @remaining_heat_load_frac -= heating_system.fraction_heat_load_served + elsif cooling_system.has_integrated_heating + sequential_heat_load_fracs = calc_sequential_load_fractions(cooling_system.integrated_heating_system_fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) + @remaining_heat_load_frac -= cooling_system.integrated_heating_system_fraction_heat_load_served + else + sequential_heat_load_fracs = [0] + end + + sys_id = cooling_system.id + if [HPXML::HVACTypeCentralAirConditioner, + HPXML::HVACTypeRoomAirConditioner, + HPXML::HVACTypeMiniSplitAirConditioner, + HPXML::HVACTypePTAC].include? cooling_system.cooling_system_type + + airloop_map[sys_id] = apply_air_source_hvac_systems(model, runner, cooling_system, heating_system, sequential_cool_load_fracs, sequential_heat_load_fracs, + weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, conditioned_zone, + @hvac_unavailable_periods, schedules_file, hpxml_bldg, hpxml_header) + + elsif [HPXML::HVACTypeEvaporativeCooler].include? cooling_system.cooling_system_type + + airloop_map[sys_id] = apply_evaporative_cooler(model, cooling_system, sequential_cool_load_fracs, conditioned_zone, @hvac_unavailable_periods, + hpxml_bldg.building_construction.number_of_units) + end + end + end + + # Adds any HPXML Heating Systems to the OpenStudio model. + # TODO for adding more description (e.g., around sequential load fractions) + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] + def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + + get_hpxml_hvac_systems(hpxml_bldg).each do |hvac_system| + next if hvac_system[:heating].nil? + next unless hvac_system[:heating].is_a? HPXML::HeatingSystem + + cooling_system = hvac_system[:cooling] + heating_system = hvac_system[:heating] + + check_distribution_system(heating_system.distribution_system, heating_system.heating_system_type) + + if (heating_system.heating_system_type == HPXML::HVACTypeFurnace) && (not cooling_system.nil?) + next # Already processed combined AC+furnace + end + + # Calculate heating sequential load fractions + if heating_system.is_heat_pump_backup_system + # Heating system will be last in the EquipmentList and should meet entirety of + # remaining load during the heating season. + sequential_heat_load_fracs = @heating_days.map(&:to_f) + if not heating_system.fraction_heat_load_served.nil? + fail 'Heat pump backup system cannot have a fraction heat load served specified.' + end + else + sequential_heat_load_fracs = calc_sequential_load_fractions(heating_system.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) + @remaining_heat_load_frac -= heating_system.fraction_heat_load_served + end + + sys_id = heating_system.id + if [HPXML::HVACTypeFurnace].include? heating_system.heating_system_type + + airloop_map[sys_id] = apply_air_source_hvac_systems(model, runner, nil, heating_system, [0], sequential_heat_load_fracs, + weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, + conditioned_zone, @hvac_unavailable_periods, schedules_file, hpxml_bldg, + hpxml_header) + + elsif [HPXML::HVACTypeBoiler].include? heating_system.heating_system_type + + airloop_map[sys_id] = apply_boiler(model, runner, heating_system, sequential_heat_load_fracs, conditioned_zone, + @hvac_unavailable_periods) + + elsif [HPXML::HVACTypeElectricResistance].include? heating_system.heating_system_type + + apply_electric_baseboard(model, heating_system, + sequential_heat_load_fracs, conditioned_zone, @hvac_unavailable_periods) + + elsif [HPXML::HVACTypeStove, + HPXML::HVACTypeSpaceHeater, + HPXML::HVACTypeWallFurnace, + HPXML::HVACTypeFloorFurnace, + HPXML::HVACTypeFireplace].include? heating_system.heating_system_type + + apply_unit_heater(model, heating_system, + sequential_heat_load_fracs, conditioned_zone, @hvac_unavailable_periods) + end + + next unless heating_system.is_heat_pump_backup_system + + # Store OS object for later use + @hp_backup_system_object = model.getZoneHVACEquipmentLists.find { |el| el.thermalZone == conditioned_zone }.equipment[-1] + end + end + + # Adds any HPXML Heat Pumps to the OpenStudio model. + # TODO for adding more description (e.g., around sequential load fractions) + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [nil] + def self.apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + + get_hpxml_hvac_systems(hpxml_bldg).each do |hvac_system| + next if hvac_system[:cooling].nil? + next unless hvac_system[:cooling].is_a? HPXML::HeatPump + + heat_pump = hvac_system[:cooling] + + check_distribution_system(heat_pump.distribution_system, heat_pump.heat_pump_type) + + # Calculate heating sequential load fractions + sequential_heat_load_fracs = calc_sequential_load_fractions(heat_pump.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) + @remaining_heat_load_frac -= heat_pump.fraction_heat_load_served + + # Calculate cooling sequential load fractions + sequential_cool_load_fracs = calc_sequential_load_fractions(heat_pump.fraction_cool_load_served, @remaining_cool_load_frac, @cooling_days) + @remaining_cool_load_frac -= heat_pump.fraction_cool_load_served + + sys_id = heat_pump.id + if [HPXML::HVACTypeHeatPumpWaterLoopToAir].include? heat_pump.heat_pump_type + + airloop_map[sys_id] = apply_water_loop_to_air_heat_pump(model, heat_pump, + sequential_heat_load_fracs, sequential_cool_load_fracs, + conditioned_zone, @hvac_unavailable_periods) + elsif [HPXML::HVACTypeHeatPumpAirToAir, + HPXML::HVACTypeHeatPumpMiniSplit, + HPXML::HVACTypeHeatPumpPTHP, + HPXML::HVACTypeHeatPumpRoom].include? heat_pump.heat_pump_type + airloop_map[sys_id] = apply_air_source_hvac_systems(model, runner, heat_pump, heat_pump, sequential_cool_load_fracs, sequential_heat_load_fracs, + weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, + conditioned_zone, @hvac_unavailable_periods, schedules_file, hpxml_bldg, + hpxml_header) + elsif [HPXML::HVACTypeHeatPumpGroundToAir].include? heat_pump.heat_pump_type + + airloop_map[sys_id] = apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, + sequential_heat_load_fracs, sequential_cool_load_fracs, + conditioned_zone, hpxml_bldg.site.ground_conductivity, hpxml_bldg.site.ground_diffusivity, + @hvac_unavailable_periods, hpxml_bldg.building_construction.number_of_units) + + end + + next if heat_pump.backup_system.nil? + + equipment_list = model.getZoneHVACEquipmentLists.find { |el| el.thermalZone == conditioned_zone } + + # Set priority to be last (i.e., after the heat pump that it is backup for) + equipment_list.setHeatingPriority(@hp_backup_system_object, 99) + equipment_list.setCoolingPriority(@hp_backup_system_object, 99) + end + end + # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -334,8 +578,7 @@ def self.apply_evaporative_cooler(model, cooling_system, sequential_cool_load_fr # @param hvac_unavailable_periods [TODO] TODO # @param unit_multiplier [Integer] Number of similar dwelling units # @return [TODO] TODO - def self.apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, - sequential_heat_load_fracs, sequential_cool_load_fracs, + def self.apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, sequential_heat_load_fracs, sequential_cool_load_fracs, control_zone, ground_conductivity, ground_diffusivity, hvac_unavailable_periods, unit_multiplier) @@ -538,8 +781,7 @@ def self.apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, # @param control_zone [OpenStudio::Model::ThermalZone] Conditioned space thermal zone # @param hvac_unavailable_periods [TODO] TODO # @return [TODO] TODO - def self.apply_water_loop_to_air_heat_pump(model, heat_pump, - sequential_heat_load_fracs, sequential_cool_load_fracs, + def self.apply_water_loop_to_air_heat_pump(model, heat_pump, sequential_heat_load_fracs, sequential_cool_load_fracs, control_zone, hvac_unavailable_periods) if heat_pump.fraction_cool_load_served > 0 # WLHPs connected to chillers or cooling towers should have already been converted to @@ -786,9 +1028,7 @@ def self.apply_boiler(model, runner, heating_system, sequential_heat_load_fracs, # @param control_zone [OpenStudio::Model::ThermalZone] Conditioned space thermal zone # @param hvac_unavailable_periods [TODO] TODO # @return [nil] - def self.apply_electric_baseboard(model, heating_system, - sequential_heat_load_fracs, control_zone, hvac_unavailable_periods) - + def self.apply_electric_baseboard(model, heating_system, sequential_heat_load_fracs, control_zone, hvac_unavailable_periods) obj_name = Constants::ObjectTypeElectricBaseboard # Baseboard @@ -811,9 +1051,7 @@ def self.apply_electric_baseboard(model, heating_system, # @param control_zone [OpenStudio::Model::ThermalZone] Conditioned space thermal zone # @param hvac_unavailable_periods [TODO] TODO # @return [TODO] TODO - def self.apply_unit_heater(model, heating_system, - sequential_heat_load_fracs, control_zone, hvac_unavailable_periods) - + def self.apply_unit_heater(model, heating_system, sequential_heat_load_fracs, control_zone, hvac_unavailable_periods) obj_name = Constants::ObjectTypeUnitHeater # Heating Coil @@ -848,6 +1086,57 @@ def self.apply_unit_heater(model, heating_system, set_sequential_load_fractions(model, control_zone, unitary_system, sequential_heat_load_fracs, nil, hvac_unavailable_periods, heating_system) end + # Adds an ideal air system as needed to meet the load under certain circumstances: + # 1. the sum of fractions load served is less than 1 and greater than 0 (e.g., room ACs serving a portion of the home's load), + # in which case we need the ideal system to help fully condition the thermal zone to prevent incorrect heat transfers, or + # 2. ASHRAE 140 tests where we need heating/cooling loads. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header) + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + + if hpxml_header.apply_ashrae140_assumptions && (hpxml_bldg.total_fraction_heat_load_served + hpxml_bldg.total_fraction_heat_load_served == 0.0) + cooling_load_frac = 1.0 + heating_load_frac = 1.0 + if hpxml_header.apply_ashrae140_assumptions + if weather.header.StateProvinceRegion.downcase == 'co' + cooling_load_frac = 0.0 + elsif weather.header.StateProvinceRegion.downcase == 'nv' + heating_load_frac = 0.0 + else + fail 'Unexpected weather file for ASHRAE 140 run.' + end + end + apply_ideal_air_loads(model, [cooling_load_frac], [heating_load_frac], + conditioned_zone, @hvac_unavailable_periods) + return + end + + if (hpxml_bldg.total_fraction_heat_load_served < 1.0) && (hpxml_bldg.total_fraction_heat_load_served > 0.0) + sequential_heat_load_fracs = calc_sequential_load_fractions(@remaining_heat_load_frac - hpxml_bldg.total_fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) + @remaining_heat_load_frac -= (1.0 - hpxml_bldg.total_fraction_heat_load_served) + else + sequential_heat_load_fracs = [0.0] + end + + if (hpxml_bldg.total_fraction_cool_load_served < 1.0) && (hpxml_bldg.total_fraction_cool_load_served > 0.0) + sequential_cool_load_fracs = calc_sequential_load_fractions(@remaining_cool_load_frac - hpxml_bldg.total_fraction_cool_load_served, @remaining_cool_load_frac, @cooling_days) + @remaining_cool_load_frac -= (1.0 - hpxml_bldg.total_fraction_cool_load_served) + else + sequential_cool_load_fracs = [0.0] + end + + if (sequential_heat_load_fracs.sum > 0.0) || (sequential_cool_load_fracs.sum > 0.0) + apply_ideal_air_loads(model, sequential_cool_load_fracs, sequential_heat_load_fracs, + conditioned_zone, @hvac_unavailable_periods) + end + end + # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -887,16 +1176,21 @@ def self.apply_ideal_air_loads(model, sequential_cool_load_fracs, set_sequential_load_fractions(model, control_zone, ideal_air, sequential_heat_load_fracs, sequential_cool_load_fracs, hvac_unavailable_periods) end - # TODO + # Adds any HPXML Dehumidifiers to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param dehumidifiers [TODO] TODO - # @param conditioned_space [TODO] TODO - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.apply_dehumidifiers(runner, model, dehumidifiers, conditioned_space, unavailable_periods, unit_multiplier) + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [nil] + def self.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml_header) + dehumidifiers = hpxml_bldg.dehumidifiers + return if dehumidifiers.size == 0 + + conditioned_space = spaces[HPXML::LocationConditionedSpace] + unit_multiplier = hpxml_bldg.building_construction.number_of_units + dehumidifier_id = dehumidifiers[0].id # Syncs with the ReportSimulationOutput measure, which only looks at first dehumidifier ID if dehumidifiers.map { |d| d.rh_setpoint }.uniq.size > 1 @@ -953,7 +1247,7 @@ def self.apply_dehumidifiers(runner, model, dehumidifiers, conditioned_space, un control_zone.setZoneControlHumidistat(humidistat) # Availability Schedule - dehum_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:Dehumidifier].name, unavailable_periods) + dehum_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:Dehumidifier].name, hpxml_header.unavailable_periods) avail_sch = ScheduleConstant.new(model, obj_name + ' schedule', 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: dehum_unavailable_periods) avail_sch = avail_sch.schedule @@ -974,18 +1268,21 @@ def self.apply_dehumidifiers(runner, model, dehumidifiers, conditioned_space, un end end - # TODO + # Adds an HPXML Ceiling Fan to the OpenStudio model. # - # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param weather [WeatherFile] Weather object containing EPW information - # @param ceiling_fan [TODO] TODO - # @param conditioned_space [TODO] TODO + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @return [TODO] TODO - def self.apply_ceiling_fans(model, runner, weather, ceiling_fan, conditioned_space, schedules_file, - unavailable_periods) + # @return [nil] + def self.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml_header, schedules_file) + return if hpxml_bldg.ceiling_fans.size == 0 + + ceiling_fan = hpxml_bldg.ceiling_fans[0] + obj_name = Constants::ObjectTypeCeilingFan hrs_per_day = 10.5 # From ANSI 301-2019 cfm_per_w = ceiling_fan.efficiency @@ -1002,12 +1299,12 @@ def self.apply_ceiling_fans(model, runner, weather, ceiling_fan, conditioned_spa ceiling_fan_sch = nil ceiling_fan_col_name = SchedulesFile::Columns[:CeilingFan].name if not schedules_file.nil? - annual_kwh *= HVAC.get_default_ceiling_fan_months(weather).map(&:to_f).sum(0.0) / 12.0 + annual_kwh *= get_default_ceiling_fan_months(weather).map(&:to_f).sum(0.0) / 12.0 ceiling_fan_design_level = schedules_file.calc_design_level_from_annual_kwh(col_name: ceiling_fan_col_name, annual_kwh: annual_kwh) ceiling_fan_sch = schedules_file.create_schedule_file(model, col_name: ceiling_fan_col_name) end if ceiling_fan_sch.nil? - ceiling_fan_unavailable_periods = Schedule.get_unavailable_periods(runner, ceiling_fan_col_name, unavailable_periods) + ceiling_fan_unavailable_periods = Schedule.get_unavailable_periods(runner, ceiling_fan_col_name, hpxml_header.unavailable_periods) annual_kwh *= ceiling_fan.monthly_multipliers.split(',').map(&:to_f).sum(0.0) / 12.0 weekday_sch = ceiling_fan.weekday_fractions weekend_sch = ceiling_fan.weekend_fractions @@ -1025,7 +1322,7 @@ def self.apply_ceiling_fans(model, runner, weather, ceiling_fan, conditioned_spa equip_def.setName(obj_name) equip = OpenStudio::Model::ElectricEquipment.new(equip_def) equip.setName(equip_def.name.to_s) - equip.setSpace(conditioned_space) + equip.setSpace(spaces[HPXML::LocationConditionedSpace]) equip_def.setDesignLevel(ceiling_fan_design_level) equip_def.setFractionRadiant(0.558) equip_def.setFractionLatent(0) @@ -1034,20 +1331,23 @@ def self.apply_ceiling_fans(model, runner, weather, ceiling_fan, conditioned_spa equip.setSchedule(ceiling_fan_sch) end - # TODO + # Adds an HPXML HVAC Control to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param weather [WeatherFile] Weather object containing EPW information - # @param hvac_control [TODO] TODO - # @param conditioned_zone [TODO] TODO - # @param has_ceiling_fan [TODO] TODO - # @param heating_days [TODO] TODO - # @param cooling_days [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @return [TODO] TODO - def self.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone, has_ceiling_fan, heating_days, cooling_days, hpxml_header, schedules_file) + # @return [nil] + def self.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + return if hpxml_bldg.hvac_controls.size == 0 + + hvac_control = hpxml_bldg.hvac_controls[0] + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + has_ceiling_fan = (hpxml_bldg.ceiling_fans.size > 0) + heating_sch = nil cooling_sch = nil year = hpxml_header.sim_calendar_year @@ -1074,7 +1374,7 @@ def self.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone, # only deal with deadband issue if both schedules are simple if heating_sch.nil? && cooling_sch.nil? - htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints = create_setpoint_schedules(runner, heating_days, cooling_days, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, year) + htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints = create_setpoint_schedules(runner, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, year) end if heating_sch.nil? @@ -1099,15 +1399,13 @@ def self.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone, # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param heating_days [TODO] TODO - # @param cooling_days [TODO] TODO # @param htg_weekday_setpoints [TODO] TODO # @param htg_weekend_setpoints [TODO] TODO # @param clg_weekday_setpoints [TODO] TODO # @param clg_weekend_setpoints [TODO] TODO # @param year [Integer] the calendar year # @return [TODO] TODO - def self.create_setpoint_schedules(runner, heating_days, cooling_days, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, year) + def self.create_setpoint_schedules(runner, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, year) # Create setpoint schedules # This method ensures that we don't construct a setpoint schedule where the cooling setpoint # is less than the heating setpoint, which would result in an E+ error. @@ -1118,17 +1416,17 @@ def self.create_setpoint_schedules(runner, heating_days, cooling_days, htg_weekd warning = false for i in 0..(Calendar.num_days_in_year(year) - 1) - if (heating_days[i] == cooling_days[i]) # both (or neither) heating/cooling seasons + if (@heating_days[i] == @cooling_days[i]) # both (or neither) heating/cooling seasons htg_wkdy = htg_weekday_setpoints[i].zip(clg_weekday_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : h } htg_wked = htg_weekend_setpoints[i].zip(clg_weekend_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : h } clg_wkdy = htg_weekday_setpoints[i].zip(clg_weekday_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : c } clg_wked = htg_weekend_setpoints[i].zip(clg_weekend_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : c } - elsif heating_days[i] == 1 # heating only seasons; cooling has minimum of heating + elsif @heating_days[i] == 1 # heating only seasons; cooling has minimum of heating htg_wkdy = htg_weekday_setpoints[i] htg_wked = htg_weekend_setpoints[i] clg_wkdy = htg_weekday_setpoints[i].zip(clg_weekday_setpoints[i]).map { |h, c| c < h ? h : c } clg_wked = htg_weekend_setpoints[i].zip(clg_weekend_setpoints[i]).map { |h, c| c < h ? h : c } - elsif cooling_days[i] == 1 # cooling only seasons; heating has maximum of cooling + elsif @cooling_days[i] == 1 # cooling only seasons; heating has maximum of cooling htg_wkdy = clg_weekday_setpoints[i].zip(htg_weekday_setpoints[i]).map { |c, h| c < h ? c : h } htg_wked = clg_weekend_setpoints[i].zip(htg_weekend_setpoints[i]).map { |c, h| c < h ? c : h } clg_wkdy = clg_weekday_setpoints[i] diff --git a/HPXMLtoOpenStudio/resources/hvac_sizing.rb b/HPXMLtoOpenStudio/resources/hvac_sizing.rb index 8087c0f51b..a120dd9d94 100644 --- a/HPXMLtoOpenStudio/resources/hvac_sizing.rb +++ b/HPXMLtoOpenStudio/resources/hvac_sizing.rb @@ -1745,7 +1745,7 @@ def self.get_duct_regain_factor(duct, hpxml_bldg) elsif [HPXML::LocationOtherHousingUnit, HPXML::LocationOtherHeatedSpace, HPXML::LocationOtherMultifamilyBufferSpace, HPXML::LocationOtherNonFreezingSpace, HPXML::LocationExteriorWall, HPXML::LocationUnderSlab, HPXML::LocationManufacturedHomeBelly].include? duct.duct_location - space_values = Geometry.get_temperature_scheduled_space_values(location: duct.duct_location) + space_values = Geometry.get_temperature_scheduled_space_values(duct.duct_location) f_regain = space_values[:f_regain] elsif [HPXML::LocationBasementUnconditioned, HPXML::LocationCrawlspaceVented, HPXML::LocationCrawlspaceUnvented].include? duct.duct_location @@ -3802,7 +3802,7 @@ def self.get_space_ua_values(mj, location, weather, hpxml_bldg) else # Unvented space ach = Airflow.get_default_unvented_space_ach() end - volume = Geometry.calculate_zone_volume(hpxml_bldg: hpxml_bldg, location: location) + volume = Geometry.calculate_zone_volume(hpxml_bldg, location) infiltration_cfm = ach / UnitConversions.convert(1.0, 'hr', 'min') * volume space_UAs[HPXML::LocationOutside] += infiltration_cfm * mj.outside_air_density * Gas.Air.cp * UnitConversions.convert(1.0, 'hr', 'min') @@ -3890,7 +3890,7 @@ def self.calculate_space_design_temp(mj, location, weather, hpxml_bldg, setpoint # @param ground_temp [Double] The approximate ground temperature during the heating or cooling season (F) # @return [Double] The location's design temperature (F) def self.calculate_scheduled_space_design_temps(location, setpoint_temp, outdoor_design_temp, ground_temp) - space_values = Geometry.get_temperature_scheduled_space_values(location: location) + space_values = Geometry.get_temperature_scheduled_space_values(location) design_temp = setpoint_temp * space_values[:indoor_weight] + outdoor_design_temp * space_values[:outdoor_weight] + ground_temp * space_values[:ground_weight] if not space_values[:temp_min].nil? design_temp = [design_temp, space_values[:temp_min]].max diff --git a/HPXMLtoOpenStudio/resources/internal_gains.rb b/HPXMLtoOpenStudio/resources/internal_gains.rb index f1ae2e43a5..6167a584dc 100644 --- a/HPXMLtoOpenStudio/resources/internal_gains.rb +++ b/HPXMLtoOpenStudio/resources/internal_gains.rb @@ -2,33 +2,21 @@ # TODO module InternalGains - # TODO - # - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param num_occ [Double] Number of occupants in the dwelling unit - # @param space [OpenStudio::Model::Space] an OpenStudio::Model::Space object - # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param apply_ashrae140_assumptions [Boolean] TODO - # @return [nil] - def self.apply(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods, apply_ashrae140_assumptions) - apply_building_occupancy(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods) - apply_general_water_use(model, runner, hpxml_bldg, space, schedules_file, unavailable_periods, apply_ashrae140_assumptions) - end - # Create an OpenStudio People object using number of occupants and people/activity schedules. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param num_occ [Double] Number of occupants in the dwelling unit - # @param space [OpenStudio::Model::Space] an OpenStudio::Model::Space object + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @return [nil] - def self.apply_building_occupancy(model, runner, hpxml_bldg, num_occ, space, schedules_file, unavailable_periods) + def self.apply_building_occupants(model, runner, hpxml_bldg, hpxml_header, spaces, schedules_file) + if hpxml_bldg.building_occupancy.number_of_residents.nil? # Asset calculation + num_occ = Geometry.get_occupancy_default_num(nbeds: hpxml_bldg.building_construction.number_of_bedrooms) + else # Operational calculation + num_occ = hpxml_bldg.building_occupancy.number_of_residents + end return if num_occ <= 0 occ_gain, _hrs_per_day, sens_frac, _lat_frac = get_occupancy_default_values() @@ -45,7 +33,7 @@ def self.apply_building_occupancy(model, runner, hpxml_bldg, num_occ, space, sch people_sch = schedules_file.create_schedule_file(model, col_name: people_col_name) end if people_sch.nil? - people_unavailable_periods = Schedule.get_unavailable_periods(runner, people_col_name, unavailable_periods) + people_unavailable_periods = Schedule.get_unavailable_periods(runner, people_col_name, hpxml_header.unavailable_periods) weekday_sch = hpxml_bldg.building_occupancy.weekday_fractions.split(',').map(&:to_f) weekday_sch = weekday_sch.map { |v| v / weekday_sch.max }.join(',') weekend_sch = hpxml_bldg.building_occupancy.weekend_fractions.split(',').map(&:to_f) @@ -68,7 +56,7 @@ def self.apply_building_occupancy(model, runner, hpxml_bldg, num_occ, space, sch occ_def = OpenStudio::Model::PeopleDefinition.new(model) occ = OpenStudio::Model::People.new(occ_def) occ.setName(Constants::ObjectTypeOccupants) - occ.setSpace(space) + occ.setSpace(spaces[HPXML::LocationConditionedSpace]) occ_def.setName(Constants::ObjectTypeOccupants) occ_def.setNumberofPeople(num_occ) occ_def.setFractionRadiant(occ_rad) @@ -94,19 +82,20 @@ def self.get_occupancy_default_values() return heat_gain, hrs_per_day, sens_frac, lat_frac end - # Set calendar year on the OpenStudio YearDescription object. + # Adds general water use internal gains (floor mopping, shower evaporation, water films + # on showers, tubs & sinks surfaces, plant watering, etc.) to the OpenStudio Model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param apply_ashrae140_assumptions [Boolean] TODO + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_general_water_use(model, runner, hpxml_bldg, space, schedules_file, unavailable_periods, apply_ashrae140_assumptions) + def self.apply_general_water_use(model, runner, hpxml_bldg, hpxml_header, spaces, schedules_file) general_water_use_usage_multiplier = hpxml_bldg.building_occupancy.general_water_use_usage_multiplier nbeds_eq = hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms - if not apply_ashrae140_assumptions - # General water use internal gains - # Floor mopping, shower evaporation, water films on showers, tubs & sinks surfaces, plant watering, etc. + if not hpxml_header.apply_ashrae140_assumptions water_sens_btu, water_lat_btu = get_water_gains_sens_lat(nbeds_eq, general_water_use_usage_multiplier) # Create schedule @@ -119,7 +108,7 @@ def self.apply_general_water_use(model, runner, hpxml_bldg, space, schedules_fil water_schedule = schedules_file.create_schedule_file(model, col_name: water_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if water_schedule.nil? - water_unavailable_periods = Schedule.get_unavailable_periods(runner, water_col_name, unavailable_periods) + water_unavailable_periods = Schedule.get_unavailable_periods(runner, water_col_name, hpxml_header.unavailable_periods) water_weekday_sch = hpxml_bldg.building_occupancy.general_water_use_weekday_fractions water_weekend_sch = hpxml_bldg.building_occupancy.general_water_use_weekend_fractions water_monthly_sch = hpxml_bldg.building_occupancy.general_water_use_monthly_multipliers @@ -132,8 +121,8 @@ def self.apply_general_water_use(model, runner, hpxml_bldg, space, schedules_fil runner.registerWarning("Both '#{water_col_name}' schedule file and weekend fractions provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_weekend_fractions.nil? runner.registerWarning("Both '#{water_col_name}' schedule file and monthly multipliers provided; the latter will be ignored.") if !hpxml_bldg.building_occupancy.general_water_use_monthly_multipliers.nil? end - HotWaterAndAppliances.add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseSensible, space, water_design_level_sens, 1.0, 0.0, water_schedule, nil) - HotWaterAndAppliances.add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseLatent, space, water_design_level_lat, 0.0, 1.0, water_schedule, nil) + HotWaterAndAppliances.add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseSensible, spaces[HPXML::LocationConditionedSpace], water_design_level_sens, 1.0, 0.0, water_schedule, nil) + HotWaterAndAppliances.add_other_equipment(model, Constants::ObjectTypeGeneralWaterUseLatent, spaces[HPXML::LocationConditionedSpace], water_design_level_lat, 0.0, 1.0, water_schedule, nil) end end diff --git a/HPXMLtoOpenStudio/resources/lighting.rb b/HPXMLtoOpenStudio/resources/lighting.rb index d78447afb3..ae04cec720 100644 --- a/HPXMLtoOpenStudio/resources/lighting.rb +++ b/HPXMLtoOpenStudio/resources/lighting.rb @@ -2,21 +2,22 @@ # TODO module Lighting - # TODO + # Adds any HPXML Lighting Groups and Lighting to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects - # @param lighting_groups [TODO] TODO - # @param lighting [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.apply(runner, model, spaces, lighting_groups, lighting, eri_version, schedules_file, cfa, - unavailable_periods, unit_multiplier) + # @return [nil] + def self.apply(runner, model, spaces, hpxml_bldg, hpxml_header, schedules_file) + lighting_groups = hpxml_bldg.lighting_groups + lighting = hpxml_bldg.lighting + unit_multiplier = hpxml_bldg.building_construction.number_of_units + cfa = hpxml_bldg.building_construction.conditioned_floor_area + eri_version = hpxml_header.eri_calculation_version + ltg_locns = [HPXML::LocationInterior, HPXML::LocationExterior, HPXML::LocationGarage] ltg_types = [HPXML::LightingTypeCFL, HPXML::LightingTypeLFL, HPXML::LightingTypeLED] @@ -83,7 +84,7 @@ def self.apply(runner, model, spaces, lighting_groups, lighting, eri_version, sc interior_sch = schedules_file.create_schedule_file(model, col_name: interior_col_name) end if interior_sch.nil? - interior_unavailable_periods = Schedule.get_unavailable_periods(runner, interior_col_name, unavailable_periods) + interior_unavailable_periods = Schedule.get_unavailable_periods(runner, interior_col_name, hpxml_header.unavailable_periods) interior_weekday_sch = lighting.interior_weekday_fractions interior_weekend_sch = lighting.interior_weekend_fractions interior_monthly_sch = lighting.interior_monthly_multipliers @@ -122,7 +123,7 @@ def self.apply(runner, model, spaces, lighting_groups, lighting, eri_version, sc garage_sch = schedules_file.create_schedule_file(model, col_name: garage_col_name) end if garage_sch.nil? - garage_unavailable_periods = Schedule.get_unavailable_periods(runner, garage_col_name, unavailable_periods) + garage_unavailable_periods = Schedule.get_unavailable_periods(runner, garage_col_name, hpxml_header.unavailable_periods) garage_sch = MonthWeekdayWeekendSchedule.new(model, garage_obj_name + ' schedule', lighting.garage_weekday_fractions, lighting.garage_weekend_fractions, lighting.garage_monthly_multipliers, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: garage_unavailable_periods) design_level = garage_sch.calc_design_level_from_daily_kwh(grg_kwh / 365.0) garage_sch = garage_sch.schedule @@ -158,7 +159,7 @@ def self.apply(runner, model, spaces, lighting_groups, lighting, eri_version, sc exterior_sch = schedules_file.create_schedule_file(model, col_name: exterior_col_name) end if exterior_sch.nil? - exterior_unavailable_periods = Schedule.get_unavailable_periods(runner, exterior_col_name, unavailable_periods) + exterior_unavailable_periods = Schedule.get_unavailable_periods(runner, exterior_col_name, hpxml_header.unavailable_periods) exterior_sch = MonthWeekdayWeekendSchedule.new(model, exterior_obj_name + ' schedule', lighting.exterior_weekday_fractions, lighting.exterior_weekend_fractions, lighting.exterior_monthly_multipliers, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: exterior_unavailable_periods) design_level = exterior_sch.calc_design_level_from_daily_kwh(ext_kwh / 365.0) exterior_sch = exterior_sch.schedule @@ -191,7 +192,7 @@ def self.apply(runner, model, spaces, lighting_groups, lighting, eri_version, sc exterior_holiday_sch = schedules_file.create_schedule_file(model, col_name: exterior_holiday_col_name) end if exterior_holiday_sch.nil? - exterior_holiday_unavailable_periods = Schedule.get_unavailable_periods(runner, exterior_holiday_col_name, unavailable_periods) + exterior_holiday_unavailable_periods = Schedule.get_unavailable_periods(runner, exterior_holiday_col_name, hpxml_header.unavailable_periods) exterior_holiday_sch = MonthWeekdayWeekendSchedule.new(model, exterior_holiday_obj_name + ' schedule', lighting.holiday_weekday_fractions, lighting.holiday_weekend_fractions, lighting.exterior_monthly_multipliers, EPlus::ScheduleTypeLimitsFraction, true, lighting.holiday_period_begin_month, lighting.holiday_period_begin_day, lighting.holiday_period_end_month, lighting.holiday_period_end_day, unavailable_periods: exterior_holiday_unavailable_periods) design_level = exterior_holiday_sch.calc_design_level_from_daily_kwh(exterior_holiday_kwh_per_day) exterior_holiday_sch = exterior_holiday_sch.schedule diff --git a/HPXMLtoOpenStudio/resources/location.rb b/HPXMLtoOpenStudio/resources/location.rb index 2edfc450bf..111f45e190 100644 --- a/HPXMLtoOpenStudio/resources/location.rb +++ b/HPXMLtoOpenStudio/resources/location.rb @@ -7,16 +7,27 @@ module Location # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param epw_path [String] Path to the EPW weather file # @return [nil] - def self.apply(model, weather, hpxml_header, hpxml_bldg) + def self.apply(model, weather, hpxml_bldg, hpxml_header, epw_path) + apply_weather_file(model, epw_path) apply_year(model, hpxml_header, weather) apply_site(model, hpxml_bldg) apply_dst(model, hpxml_bldg) apply_ground_temps(model, weather, hpxml_bldg) end + # Sets the OpenStudio WeatherFile object. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param epw_path [String] Path to the EPW weather file + # @return [nil] + def self.apply_weather_file(model, epw_path) + OpenStudio::Model::WeatherFile.setWeatherFile(model, OpenStudio::EpwFile.new(epw_path)) + end + # Set latitude, longitude, time zone, and elevation on the OpenStudio Site object. # # @param model [OpenStudio::Model::Model] OpenStudio Model object diff --git a/HPXMLtoOpenStudio/resources/misc_loads.rb b/HPXMLtoOpenStudio/resources/misc_loads.rb index 017a49a30b..bd7d2466e0 100644 --- a/HPXMLtoOpenStudio/resources/misc_loads.rb +++ b/HPXMLtoOpenStudio/resources/misc_loads.rb @@ -2,18 +2,48 @@ # TODO module MiscLoads - # TODO + # Adds any HPXML Plug Loads to the OpenStudio model. # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @return [nil] + def self.apply_plug_loads(runner, model, spaces, hpxml_bldg, hpxml_header, schedules_file) + hpxml_bldg.plug_loads.each do |plug_load| + if plug_load.plug_load_type == HPXML::PlugLoadTypeOther + obj_name = Constants::ObjectTypeMiscPlugLoads + elsif plug_load.plug_load_type == HPXML::PlugLoadTypeTelevision + obj_name = Constants::ObjectTypeMiscTelevision + elsif plug_load.plug_load_type == HPXML::PlugLoadTypeElectricVehicleCharging + obj_name = Constants::ObjectTypeMiscElectricVehicleCharging + elsif plug_load.plug_load_type == HPXML::PlugLoadTypeWellPump + obj_name = Constants::ObjectTypeMiscWellPump + end + if obj_name.nil? + runner.registerWarning("Unexpected plug load type '#{plug_load.plug_load_type}'. The plug load will not be modeled.") + next + end + + apply_plug_load(runner, model, plug_load, obj_name, spaces, schedules_file, + hpxml_header.unavailable_periods, hpxml_header.apply_ashrae140_assumptions) + end + end + + # Adds the HPXML Plug Load to the OpenStudio model. + # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param plug_load [TODO] TODO # @param obj_name [String] Name for the OpenStudio object - # @param conditioned_space [TODO] TODO - # @param apply_ashrae140_assumptions [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @return [TODO] TODO - def self.apply_plug(model, runner, plug_load, obj_name, conditioned_space, apply_ashrae140_assumptions, schedules_file, unavailable_periods) + # @param apply_ashrae140_assumptions [TODO] TODO + # @return [nil] + def self.apply_plug_load(runner, model, plug_load, obj_name, spaces, schedules_file, unavailable_periods, apply_ashrae140_assumptions) kwh = 0 if not plug_load.nil? kwh = plug_load.kwh_per_year * plug_load.usage_multiplier @@ -62,7 +92,7 @@ def self.apply_plug(model, runner, plug_load, obj_name, conditioned_space, apply mel = OpenStudio::Model::ElectricEquipment.new(mel_def) mel.setName(obj_name) mel.setEndUseSubcategory(obj_name) - mel.setSpace(conditioned_space) + mel.setSpace(spaces[HPXML::LocationConditionedSpace]) mel_def.setName(obj_name) mel_def.setDesignLevel(space_design_level) mel_def.setFractionRadiant(rad_frac) @@ -71,17 +101,45 @@ def self.apply_plug(model, runner, plug_load, obj_name, conditioned_space, apply mel.setSchedule(sch) end - # TODO + # Adds any HPXML Fuel Loads to the OpenStudio model. # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @return [nil] + def self.apply_fuel_loads(runner, model, spaces, hpxml_bldg, hpxml_header, schedules_file) + hpxml_bldg.fuel_loads.each do |fuel_load| + if fuel_load.fuel_load_type == HPXML::FuelLoadTypeGrill + obj_name = Constants::ObjectTypeMiscGrill + elsif fuel_load.fuel_load_type == HPXML::FuelLoadTypeLighting + obj_name = Constants::ObjectTypeMiscLighting + elsif fuel_load.fuel_load_type == HPXML::FuelLoadTypeFireplace + obj_name = Constants::ObjectTypeMiscFireplace + end + if obj_name.nil? + runner.registerWarning("Unexpected fuel load type '#{fuel_load.fuel_load_type}'. The fuel load will not be modeled.") + next + end + + apply_fuel_load(runner, model, fuel_load, obj_name, spaces, schedules_file, + hpxml_header.unavailable_periods) + end + end + + # Adds the HPXML Fuel Load to the OpenStudio model. + # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param fuel_load [TODO] TODO # @param obj_name [String] Name for the OpenStudio object - # @param conditioned_space [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @return [TODO] TODO - def self.apply_fuel(model, runner, fuel_load, obj_name, conditioned_space, schedules_file, unavailable_periods) + # @return [nil] + def self.apply_fuel_load(runner, model, fuel_load, obj_name, spaces, schedules_file, unavailable_periods) therm = 0 if not fuel_load.nil? therm = fuel_load.therm_per_year * fuel_load.usage_multiplier @@ -122,7 +180,7 @@ def self.apply_fuel(model, runner, fuel_load, obj_name, conditioned_space, sched mfl.setName(obj_name) mfl.setEndUseSubcategory(obj_name) mfl.setFuelType(EPlus.fuel_type(fuel_load.fuel_type)) - mfl.setSpace(conditioned_space) + mfl.setSpace(spaces[HPXML::LocationConditionedSpace]) mfl_def.setName(obj_name) mfl_def.setDesignLevel(space_design_level) mfl_def.setFractionRadiant(0.6 * sens_frac) @@ -131,16 +189,38 @@ def self.apply_fuel(model, runner, fuel_load, obj_name, conditioned_space, sched mfl.setSchedule(sch) end - # TODO + # Adds any HPXML Pools and Permanent Spas to the OpenStudio model. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @return [nil] + def self.apply_pools_and_permanent_spas(runner, model, spaces, hpxml_bldg, hpxml_header, schedules_file) + (hpxml_bldg.pools + hpxml_bldg.permanent_spas).each do |pool_or_spa| + next if pool_or_spa.type == HPXML::TypeNone + + apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, spaces, + schedules_file, hpxml_header.unavailable_periods) + next if pool_or_spa.pump_type == HPXML::TypeNone + + apply_pool_or_permanent_spa_pump(runner, model, pool_or_spa, spaces, + schedules_file, hpxml_header.unavailable_periods) + end + end + + # Adds the HPXML Pool or Permanent Spa Heater to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param pool_or_spa [TODO] TODO - # @param conditioned_space [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @return [TODO] TODO - def self.apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, conditioned_space, schedules_file, unavailable_periods) + # @return [nil] + def self.apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, spaces, schedules_file, unavailable_periods) return if pool_or_spa.heater_type == HPXML::TypeNone heater_kwh = 0 @@ -187,7 +267,7 @@ def self.apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, conditio mel = OpenStudio::Model::ElectricEquipment.new(mel_def) mel.setName(obj_name) mel.setEndUseSubcategory(obj_name) - mel.setSpace(conditioned_space) # no heat gain, so assign the equipment to an arbitrary space + mel.setSpace(spaces[HPXML::LocationConditionedSpace]) # no heat gain, so assign the equipment to an arbitrary space mel_def.setName(obj_name) mel_def.setDesignLevel(space_design_level) mel_def.setFractionRadiant(0) @@ -210,7 +290,7 @@ def self.apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, conditio mfl.setName(obj_name) mfl.setEndUseSubcategory(obj_name) mfl.setFuelType(EPlus.fuel_type(HPXML::FuelTypeNaturalGas)) - mfl.setSpace(conditioned_space) # no heat gain, so assign the equipment to an arbitrary space + mfl.setSpace(spaces[HPXML::LocationConditionedSpace]) # no heat gain, so assign the equipment to an arbitrary space mfl_def.setName(obj_name) mfl_def.setDesignLevel(space_design_level) mfl_def.setFractionRadiant(0) @@ -220,16 +300,16 @@ def self.apply_pool_or_permanent_spa_heater(runner, model, pool_or_spa, conditio end end - # TODO + # Adds the HPXML Pool or Permanent Spa Pump to the OpenStudio model. # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param pool_or_spa [TODO] TODO - # @param conditioned_space [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @return [TODO] TODO - def self.apply_pool_or_permanent_spa_pump(runner, model, pool_or_spa, conditioned_space, schedules_file, unavailable_periods) + # @return [nil] + def self.apply_pool_or_permanent_spa_pump(runner, model, pool_or_spa, spaces, schedules_file, unavailable_periods) pump_kwh = 0 if not pool_or_spa.pump_kwh_per_year.nil? pump_kwh = pool_or_spa.pump_kwh_per_year * pool_or_spa.pump_usage_multiplier @@ -270,7 +350,7 @@ def self.apply_pool_or_permanent_spa_pump(runner, model, pool_or_spa, conditione mel = OpenStudio::Model::ElectricEquipment.new(mel_def) mel.setName(obj_name) mel.setEndUseSubcategory(obj_name) - mel.setSpace(conditioned_space) # no heat gain, so assign the equipment to an arbitrary space + mel.setSpace(spaces[HPXML::LocationConditionedSpace]) # no heat gain, so assign the equipment to an arbitrary space mel_def.setName(obj_name) mel_def.setDesignLevel(space_design_level) mel_def.setFractionRadiant(0) diff --git a/HPXMLtoOpenStudio/resources/output.rb b/HPXMLtoOpenStudio/resources/output.rb index 2281f77ad1..5c89e1d55f 100644 --- a/HPXMLtoOpenStudio/resources/output.rb +++ b/HPXMLtoOpenStudio/resources/output.rb @@ -158,6 +158,826 @@ module WT # TODO module Outputs + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param add_component_loads [Boolean] Whether to calculate component loads (since it incurs a runtime speed penalty) + # @return [nil] + def self.apply_ems_programs(model, hpxml_osm_map, hpxml_header, add_component_loads) + season_day_nums = Outputs.apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header) + loads_data = Outputs.apply_total_loads_ems_program(model, hpxml_osm_map, hpxml_header) + if add_component_loads + Outputs.apply_component_loads_ems_program(model, hpxml_osm_map, loads_data, season_day_nums) + end + Outputs.apply_total_airflows_ems_program(model, hpxml_osm_map) + end + + # We do our own unmet hours calculation via EMS so that we can incorporate, + # e.g., heating/cooling seasons into the logic. The calculation layers on top + # of the built-in EnergyPlus unmet hours output. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [Hash] TODO + def self.apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header) + # Create sensors and gather data + htg_sensors, clg_sensors = {}, {} + zone_air_temp_sensors, htg_spt_sensors, clg_spt_sensors = {}, {}, {} + total_heat_load_serveds, total_cool_load_serveds = {}, {} + season_day_nums = {} + onoff_deadbands = hpxml_header.hvac_onoff_thermostat_deadband.to_f + hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit| + conditioned_zone = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace } + conditioned_zone_name = conditioned_zone.name.to_s + + # EMS sensors + htg_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Heating Setpoint Not Met Time') + htg_sensors[unit].setName("#{conditioned_zone_name} htg unmet s") + htg_sensors[unit].setKeyName(conditioned_zone_name) + + clg_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Cooling Setpoint Not Met Time') + clg_sensors[unit].setName("#{conditioned_zone_name} clg unmet s") + clg_sensors[unit].setKeyName(conditioned_zone_name) + + total_heat_load_serveds[unit] = hpxml_bldg.total_fraction_heat_load_served + total_cool_load_serveds[unit] = hpxml_bldg.total_fraction_cool_load_served + + hvac_control = hpxml_bldg.hvac_controls[0] + next if hvac_control.nil? + + if (onoff_deadbands > 0) + zone_air_temp_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Temperature') + zone_air_temp_sensors[unit].setName("#{conditioned_zone_name} space temp") + zone_air_temp_sensors[unit].setKeyName(conditioned_zone_name) + + htg_sch = conditioned_zone.thermostatSetpointDualSetpoint.get.heatingSetpointTemperatureSchedule.get + htg_spt_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + htg_spt_sensors[unit].setName("#{htg_sch.name} sch value") + htg_spt_sensors[unit].setKeyName(htg_sch.name.to_s) + + clg_sch = conditioned_zone.thermostatSetpointDualSetpoint.get.coolingSetpointTemperatureSchedule.get + clg_spt_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + clg_spt_sensors[unit].setName("#{clg_sch.name} sch value") + clg_spt_sensors[unit].setKeyName(clg_sch.name.to_s) + end + + sim_year = hpxml_header.sim_calendar_year + season_day_nums[unit] = { + htg_start: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_heating_begin_month, hvac_control.seasons_heating_begin_day), + htg_end: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_heating_end_month, hvac_control.seasons_heating_end_day), + clg_start: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_cooling_begin_month, hvac_control.seasons_cooling_begin_day), + clg_end: Calendar.get_day_num_from_month_day(sim_year, hvac_control.seasons_cooling_end_month, hvac_control.seasons_cooling_end_day) + } + end + + hvac_availability_sensor = model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeHVACAvailabilitySensor } + + # EMS program + clg_hrs = 'clg_unmet_hours' + htg_hrs = 'htg_unmet_hours' + unit_clg_hrs = 'unit_clg_unmet_hours' + unit_htg_hrs = 'unit_htg_unmet_hours' + program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + program.setName('unmet hours program') + program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeUnmetHoursProgram) + program.addLine("Set #{htg_hrs} = 0") + program.addLine("Set #{clg_hrs} = 0") + for unit in 0..hpxml_osm_map.size - 1 + if total_heat_load_serveds[unit] > 0 + program.addLine("Set #{unit_htg_hrs} = 0") + if season_day_nums[unit][:htg_end] >= season_day_nums[unit][:htg_start] + line = "If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) && (DayOfYear <= #{season_day_nums[unit][:htg_end]}))" + else + line = "If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) || (DayOfYear <= #{season_day_nums[unit][:htg_end]}))" + end + line += " && (#{hvac_availability_sensor.name} == 1)" if not hvac_availability_sensor.nil? + program.addLine(line) + if zone_air_temp_sensors.keys.include? unit # on off deadband + program.addLine(" If #{zone_air_temp_sensors[unit].name} < (#{htg_spt_sensors[unit].name} - #{UnitConversions.convert(onoff_deadbands, 'deltaF', 'deltaC')})") + program.addLine(" Set #{unit_htg_hrs} = #{unit_htg_hrs} + #{htg_sensors[unit].name}") + program.addLine(' EndIf') + else + program.addLine(" Set #{unit_htg_hrs} = #{unit_htg_hrs} + #{htg_sensors[unit].name}") + end + program.addLine(" If #{unit_htg_hrs} > #{htg_hrs}") # Use max hourly value across all units + program.addLine(" Set #{htg_hrs} = #{unit_htg_hrs}") + program.addLine(' EndIf') + program.addLine('EndIf') + end + next unless total_cool_load_serveds[unit] > 0 + + program.addLine("Set #{unit_clg_hrs} = 0") + if season_day_nums[unit][:clg_end] >= season_day_nums[unit][:clg_start] + line = "If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) && (DayOfYear <= #{season_day_nums[unit][:clg_end]}))" + else + line = "If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) || (DayOfYear <= #{season_day_nums[unit][:clg_end]}))" + end + line += " && (#{hvac_availability_sensor.name} == 1)" if not hvac_availability_sensor.nil? + program.addLine(line) + if zone_air_temp_sensors.keys.include? unit # on off deadband + program.addLine(" If #{zone_air_temp_sensors[unit].name} > (#{clg_spt_sensors[unit].name} + #{UnitConversions.convert(onoff_deadbands, 'deltaF', 'deltaC')})") + program.addLine(" Set #{unit_clg_hrs} = #{unit_clg_hrs} + #{clg_sensors[unit].name}") + program.addLine(' EndIf') + else + program.addLine(" Set #{unit_clg_hrs} = #{unit_clg_hrs} + #{clg_sensors[unit].name}") + end + program.addLine(" If #{unit_clg_hrs} > #{clg_hrs}") # Use max hourly value across all units + program.addLine(" Set #{clg_hrs} = #{unit_clg_hrs}") + program.addLine(' EndIf') + program.addLine('EndIf') + end + + # EMS calling manager + program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + program_calling_manager.setName("#{program.name} calling manager") + program_calling_manager.setCallingPoint('EndOfZoneTimestepBeforeZoneReporting') + program_calling_manager.addProgram(program) + + return season_day_nums + end + + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @return [TODO] TODO + def self.apply_total_loads_ems_program(model, hpxml_osm_map, hpxml_header) + # Create sensors and gather data + htg_cond_load_sensors, clg_cond_load_sensors = {}, {} + htg_duct_load_sensors, clg_duct_load_sensors = {}, {} + total_heat_load_serveds, total_cool_load_serveds = {}, {} + dehumidifier_global_vars, dehumidifier_sensors = {}, {} + + hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit| + # Retrieve objects + conditioned_zone_name = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace }.name.to_s + duct_zone_names = unit_model.getThermalZones.select { |z| z.isPlenum }.map { |z| z.name.to_s } + dehumidifier = unit_model.getZoneHVACDehumidifierDXs + dehumidifier_name = dehumidifier[0].name.to_s unless dehumidifier.empty? + + # Fraction heat/cool load served + if hpxml_header.apply_ashrae140_assumptions + total_heat_load_serveds[unit] = 1.0 + total_cool_load_serveds[unit] = 1.0 + else + total_heat_load_serveds[unit] = hpxml_bldg.total_fraction_heat_load_served + total_cool_load_serveds[unit] = hpxml_bldg.total_fraction_cool_load_served + end + + # Energy transferred in conditioned zone, used for determining heating (winter) vs cooling (summer) + htg_cond_load_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Heating:EnergyTransfer:Zone:#{conditioned_zone_name.upcase}") + htg_cond_load_sensors[unit].setName('htg_load_cond') + clg_cond_load_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Cooling:EnergyTransfer:Zone:#{conditioned_zone_name.upcase}") + clg_cond_load_sensors[unit].setName('clg_load_cond') + + # Energy transferred in duct zone(s) + htg_duct_load_sensors[unit] = [] + clg_duct_load_sensors[unit] = [] + duct_zone_names.each do |duct_zone_name| + htg_duct_load_sensors[unit] << OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Heating:EnergyTransfer:Zone:#{duct_zone_name.upcase}") + htg_duct_load_sensors[unit][-1].setName('htg_load_duct') + clg_duct_load_sensors[unit] << OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Cooling:EnergyTransfer:Zone:#{duct_zone_name.upcase}") + clg_duct_load_sensors[unit][-1].setName('clg_load_duct') + end + + next if dehumidifier_name.nil? + + # Need to adjust E+ EnergyTransfer meters for dehumidifier internal gains. + # We also offset the dehumidifier load by one timestep so that it aligns with the EnergyTransfer meters. + + # Global Variable + dehumidifier_global_vars[unit] = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, "prev_#{dehumidifier_name}") + + # Initialization Program + timestep_offset_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + timestep_offset_program.setName("#{dehumidifier_name} timestep offset init program") + timestep_offset_program.addLine("Set #{dehumidifier_global_vars[unit].name} = 0") + + # calling managers + manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + manager.setName("#{timestep_offset_program.name} calling manager") + manager.setCallingPoint('BeginNewEnvironment') + manager.addProgram(timestep_offset_program) + manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + manager.setName("#{timestep_offset_program.name} calling manager2") + manager.setCallingPoint('AfterNewEnvironmentWarmUpIsComplete') + manager.addProgram(timestep_offset_program) + + dehumidifier_sensors[unit] = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Dehumidifier Sensible Heating Energy') + dehumidifier_sensors[unit].setName('ig_dehumidifier') + dehumidifier_sensors[unit].setKeyName(dehumidifier_name) + end + + # EMS program + program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + program.setName('total loads program') + program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeTotalLoadsProgram) + program.addLine('Set loads_htg_tot = 0') + program.addLine('Set loads_clg_tot = 0') + for unit in 0..hpxml_osm_map.size - 1 + program.addLine("If #{htg_cond_load_sensors[unit].name} > 0") + program.addLine(" Set loads_htg_tot = loads_htg_tot + (#{htg_cond_load_sensors[unit].name} - #{clg_cond_load_sensors[unit].name}) * #{total_heat_load_serveds[unit]}") + for i in 0..htg_duct_load_sensors[unit].size - 1 + program.addLine(" Set loads_htg_tot = loads_htg_tot + (#{htg_duct_load_sensors[unit][i].name} - #{clg_duct_load_sensors[unit][i].name}) * #{total_heat_load_serveds[unit]}") + end + if not dehumidifier_global_vars[unit].nil? + program.addLine(" Set loads_htg_tot = loads_htg_tot - #{dehumidifier_global_vars[unit].name}") + end + program.addLine('EndIf') + end + program.addLine('Set loads_htg_tot = (@Max loads_htg_tot 0)') + for unit in 0..hpxml_osm_map.size - 1 + program.addLine("If #{clg_cond_load_sensors[unit].name} > 0") + program.addLine(" Set loads_clg_tot = loads_clg_tot + (#{clg_cond_load_sensors[unit].name} - #{htg_cond_load_sensors[unit].name}) * #{total_cool_load_serveds[unit]}") + for i in 0..clg_duct_load_sensors[unit].size - 1 + program.addLine(" Set loads_clg_tot = loads_clg_tot + (#{clg_duct_load_sensors[unit][i].name} - #{htg_duct_load_sensors[unit][i].name}) * #{total_cool_load_serveds[unit]}") + end + if not dehumidifier_global_vars[unit].nil? + program.addLine(" Set loads_clg_tot = loads_clg_tot + #{dehumidifier_global_vars[unit].name}") + end + program.addLine('EndIf') + end + program.addLine('Set loads_clg_tot = (@Max loads_clg_tot 0)') + for unit in 0..hpxml_osm_map.size - 1 + if not dehumidifier_global_vars[unit].nil? + # Store dehumidifier internal gain, will be used in EMS program next timestep + program.addLine("Set #{dehumidifier_global_vars[unit].name} = #{dehumidifier_sensors[unit].name}") + end + end + + # EMS calling manager + program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + program_calling_manager.setName("#{program.name} calling manager") + program_calling_manager.setCallingPoint('EndOfZoneTimestepAfterZoneReporting') + program_calling_manager.addProgram(program) + + return htg_cond_load_sensors, clg_cond_load_sensors, total_heat_load_serveds, total_cool_load_serveds, dehumidifier_sensors + end + + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @param loads_data [TODO] TODO + # @param season_day_nums [TODO] TODO + # @return [nil] + def self.apply_component_loads_ems_program(model, hpxml_osm_map, loads_data, season_day_nums) + htg_cond_load_sensors, clg_cond_load_sensors, total_heat_load_serveds, total_cool_load_serveds, dehumidifier_sensors = loads_data + + # Output diagnostics needed for some output variables used below + output_diagnostics = model.getOutputDiagnostics + output_diagnostics.addKey('DisplayAdvancedReportVariables') + + area_tolerance = UnitConversions.convert(1.0, 'ft^2', 'm^2') + + nonsurf_names = ['intgains', 'lighting', 'infil', 'mechvent', 'natvent', 'whf', 'ducts'] + surf_names = ['walls', 'rim_joists', 'foundation_walls', 'floors', 'slabs', 'ceilings', + 'roofs', 'windows_conduction', 'windows_solar', 'doors', 'skylights_conduction', + 'skylights_solar', 'internal_mass'] + + # EMS program + program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + program.setName('component loads program') + program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeComponentLoadsProgram) + + # Initialize + [:htg, :clg].each do |mode| + surf_names.each do |surf_name| + program.addLine("Set loads_#{mode}_#{surf_name} = 0") + end + nonsurf_names.each do |nonsurf_name| + program.addLine("Set loads_#{mode}_#{nonsurf_name} = 0") + end + end + + hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit| + conditioned_zone = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace } + + # Prevent certain objects (e.g., OtherEquipment) from being counted towards both, e.g., ducts and internal gains + objects_already_processed = [] + + # EMS Sensors: Surfaces, SubSurfaces, InternalMass + surfaces_sensors = {} + surf_names.each do |surf_name| + surfaces_sensors[surf_name.to_sym] = [] + end + + unit_model.getSurfaces.sort.each do |s| + next unless s.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + surface_type = s.additionalProperties.getFeatureAsString('SurfaceType') + if not surface_type.is_initialized + fail "Could not identify surface type for surface: '#{s.name}'." + end + + surface_type = surface_type.get + + s.subSurfaces.each do |ss| + # Conduction (windows, skylights, doors) + key = { 'Window' => :windows_conduction, + 'Door' => :doors, + 'Skylight' => :skylights_conduction }[surface_type] + fail "Unexpected subsurface for component loads: '#{ss.name}'." if key.nil? + + if (surface_type == 'Window') || (surface_type == 'Skylight') + vars = { 'Surface Inside Face Convection Heat Gain Energy' => 'ss_conv', + 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 'ss_ig', + 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 'ss_surf' } + else + vars = { 'Surface Inside Face Solar Radiation Heat Gain Energy' => 'ss_sol', + 'Surface Inside Face Lights Radiation Heat Gain Energy' => 'ss_lgt', + 'Surface Inside Face Convection Heat Gain Energy' => 'ss_conv', + 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 'ss_ig', + 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 'ss_surf' } + end + + vars.each do |var, name| + surfaces_sensors[key] << [] + sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + sensor.setName(name) + sensor.setKeyName(ss.name.to_s) + surfaces_sensors[key][-1] << sensor + end + + # Solar (windows, skylights) + next unless (surface_type == 'Window') || (surface_type == 'Skylight') + + key = { 'Window' => :windows_solar, + 'Skylight' => :skylights_solar }[surface_type] + vars = { 'Surface Window Transmitted Solar Radiation Energy' => 'ss_trans_in', + 'Surface Window Shortwave from Zone Back Out Window Heat Transfer Rate' => 'ss_back_out', + 'Surface Window Total Glazing Layers Absorbed Shortwave Radiation Rate' => 'ss_sw_abs', + 'Surface Window Total Glazing Layers Absorbed Solar Radiation Energy' => 'ss_sol_abs', + 'Surface Inside Face Initial Transmitted Diffuse Transmitted Out Window Solar Radiation Rate' => 'ss_trans_out' } + + surfaces_sensors[key] << [] + vars.each do |var, name| + sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + sensor.setName(name) + sensor.setKeyName(ss.name.to_s) + surfaces_sensors[key][-1] << sensor + end + end + + next if s.netArea < area_tolerance # Skip parent surfaces (of subsurfaces) that have near zero net area + + key = { 'FoundationWall' => :foundation_walls, + 'RimJoist' => :rim_joists, + 'Wall' => :walls, + 'Slab' => :slabs, + 'Floor' => :floors, + 'Ceiling' => :ceilings, + 'Roof' => :roofs, + 'Skylight' => :skylights_conduction, # Skylight curb/shaft + 'InferredCeiling' => :internal_mass, + 'InferredFloor' => :internal_mass }[surface_type] + fail "Unexpected surface for component loads: '#{s.name}'." if key.nil? + + surfaces_sensors[key] << [] + { 'Surface Inside Face Convection Heat Gain Energy' => 's_conv', + 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 's_ig', + 'Surface Inside Face Solar Radiation Heat Gain Energy' => 's_sol', + 'Surface Inside Face Lights Radiation Heat Gain Energy' => 's_lgt', + 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 's_surf' }.each do |var, name| + sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + sensor.setName(name) + sensor.setKeyName(s.name.to_s) + surfaces_sensors[key][-1] << sensor + end + end + + unit_model.getInternalMasss.sort.each do |m| + next unless m.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + surfaces_sensors[:internal_mass] << [] + { 'Surface Inside Face Convection Heat Gain Energy' => 'im_conv', + 'Surface Inside Face Internal Gains Radiation Heat Gain Energy' => 'im_ig', + 'Surface Inside Face Solar Radiation Heat Gain Energy' => 'im_sol', + 'Surface Inside Face Lights Radiation Heat Gain Energy' => 'im_lgt', + 'Surface Inside Face Net Surface Thermal Radiation Heat Gain Energy' => 'im_surf' }.each do |var, name| + sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + sensor.setName(name) + sensor.setKeyName(m.name.to_s) + surfaces_sensors[:internal_mass][-1] << sensor + end + end + + # EMS Sensors: Infiltration, Natural Ventilation, Whole House Fan + infil_sensors, natvent_sensors, whf_sensors = [], [], [] + unit_model.getSpaceInfiltrationDesignFlowRates.sort.each do |i| + next unless i.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + object_type = i.additionalProperties.getFeatureAsString('ObjectType').get + + { 'Infiltration Sensible Heat Gain Energy' => 'airflow_gain', + 'Infiltration Sensible Heat Loss Energy' => 'airflow_loss' }.each do |var, name| + airflow_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + airflow_sensor.setName(name) + airflow_sensor.setKeyName(i.name.to_s) + if object_type == Constants::ObjectTypeInfiltration + infil_sensors << airflow_sensor + elsif object_type == Constants::ObjectTypeNaturalVentilation + natvent_sensors << airflow_sensor + elsif object_type == Constants::ObjectTypeWholeHouseFan + whf_sensors << airflow_sensor + end + end + end + + # EMS Sensors: Mechanical Ventilation + mechvents_sensors = [] + unit_model.getElectricEquipments.sort.each do |o| + next unless o.endUseSubcategory == Constants::ObjectTypeMechanicalVentilation + + objects_already_processed << o + { 'Electric Equipment Convective Heating Energy' => 'mv_conv', + 'Electric Equipment Radiant Heating Energy' => 'mv_rad' }.each do |var, name| + mechvent_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + mechvent_sensor.setName(name) + mechvent_sensor.setKeyName(o.name.to_s) + mechvents_sensors << mechvent_sensor + end + end + unit_model.getOtherEquipments.sort.each do |o| + next unless o.endUseSubcategory == Constants::ObjectTypeMechanicalVentilationHouseFan + + objects_already_processed << o + { 'Other Equipment Convective Heating Energy' => 'mv_conv', + 'Other Equipment Radiant Heating Energy' => 'mv_rad' }.each do |var, name| + mechvent_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + mechvent_sensor.setName(name) + mechvent_sensor.setKeyName(o.name.to_s) + mechvents_sensors << mechvent_sensor + end + end + + # EMS Sensors: Ducts + ducts_sensors = [] + ducts_mix_gain_sensor = nil + ducts_mix_loss_sensor = nil + conditioned_zone.zoneMixing.each do |zone_mix| + object_type = zone_mix.additionalProperties.getFeatureAsString('ObjectType').to_s + next unless object_type == Constants::ObjectTypeDuctLoad + + ducts_mix_gain_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mixing Sensible Heat Gain Energy') + ducts_mix_gain_sensor.setName('duct_mix_gain') + ducts_mix_gain_sensor.setKeyName(conditioned_zone.name.to_s) + + ducts_mix_loss_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mixing Sensible Heat Loss Energy') + ducts_mix_loss_sensor.setName('duct_mix_loss') + ducts_mix_loss_sensor.setKeyName(conditioned_zone.name.to_s) + end + unit_model.getOtherEquipments.sort.each do |o| + next if objects_already_processed.include? o + next unless o.endUseSubcategory == Constants::ObjectTypeDuctLoad + + objects_already_processed << o + { 'Other Equipment Convective Heating Energy' => 'ducts_conv', + 'Other Equipment Radiant Heating Energy' => 'ducts_rad' }.each do |var, name| + ducts_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + ducts_sensor.setName(name) + ducts_sensor.setKeyName(o.name.to_s) + ducts_sensors << ducts_sensor + end + end + + # EMS Sensors: Lighting + lightings_sensors = [] + unit_model.getLightss.sort.each do |e| + next unless e.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + { 'Lights Convective Heating Energy' => 'ig_lgt_conv', + 'Lights Radiant Heating Energy' => 'ig_lgt_rad', + 'Lights Visible Radiation Heating Energy' => 'ig_lgt_vis' }.each do |var, name| + intgains_lights_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + intgains_lights_sensor.setName(name) + intgains_lights_sensor.setKeyName(e.name.to_s) + lightings_sensors << intgains_lights_sensor + end + end + + # EMS Sensors: Internal Gains + intgains_sensors = [] + unit_model.getElectricEquipments.sort.each do |o| + next if objects_already_processed.include? o + next unless o.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + { 'Electric Equipment Convective Heating Energy' => 'ig_ee_conv', + 'Electric Equipment Radiant Heating Energy' => 'ig_ee_rad' }.each do |var, name| + intgains_elec_equip_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + intgains_elec_equip_sensor.setName(name) + intgains_elec_equip_sensor.setKeyName(o.name.to_s) + intgains_sensors << intgains_elec_equip_sensor + end + end + + unit_model.getOtherEquipments.sort.each do |o| + next if objects_already_processed.include? o + next unless o.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + { 'Other Equipment Convective Heating Energy' => 'ig_oe_conv', + 'Other Equipment Radiant Heating Energy' => 'ig_oe_rad' }.each do |var, name| + intgains_other_equip_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + intgains_other_equip_sensor.setName(name) + intgains_other_equip_sensor.setKeyName(o.name.to_s) + intgains_sensors << intgains_other_equip_sensor + end + end + + unit_model.getPeoples.sort.each do |e| + next unless e.space.get.thermalZone.get.name.to_s == conditioned_zone.name.to_s + + { 'People Convective Heating Energy' => 'ig_ppl_conv', + 'People Radiant Heating Energy' => 'ig_ppl_rad' }.each do |var, name| + intgains_people = OpenStudio::Model::EnergyManagementSystemSensor.new(model, var) + intgains_people.setName(name) + intgains_people.setKeyName(e.name.to_s) + intgains_sensors << intgains_people + end + end + + if not dehumidifier_sensors[unit].nil? + intgains_sensors << dehumidifier_sensors[unit] + end + + intgains_dhw_sensors = {} + + (unit_model.getWaterHeaterMixeds + unit_model.getWaterHeaterStratifieds).sort.each do |wh| + next unless wh.ambientTemperatureThermalZone.is_initialized + next unless wh.ambientTemperatureThermalZone.get.name.to_s == conditioned_zone.name.to_s + + dhw_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Heat Loss Energy') + dhw_sensor.setName('dhw_loss') + dhw_sensor.setKeyName(wh.name.to_s) + + if wh.is_a? OpenStudio::Model::WaterHeaterMixed + oncycle_loss = wh.onCycleLossFractiontoThermalZone + offcycle_loss = wh.offCycleLossFractiontoThermalZone + else + oncycle_loss = wh.skinLossFractiontoZone + offcycle_loss = wh.offCycleFlueLossFractiontoZone + end + + dhw_rtf_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Runtime Fraction') + dhw_rtf_sensor.setName('dhw_rtf') + dhw_rtf_sensor.setKeyName(wh.name.to_s) + + intgains_dhw_sensors[dhw_sensor] = [offcycle_loss, oncycle_loss, dhw_rtf_sensor] + end + + # EMS program: Surfaces + surfaces_sensors.each do |k, surface_sensors| + program.addLine("Set hr_#{k} = 0") + surface_sensors.each do |sensors| + s = "Set hr_#{k} = hr_#{k}" + sensors.each do |sensor| + # remove ss_net if switch + if sensor.name.to_s.start_with?('ss_net', 'ss_sol_abs', 'ss_trans_in') + s += " - #{sensor.name}" + elsif sensor.name.to_s.start_with?('ss_sw_abs', 'ss_trans_out', 'ss_back_out') + s += " + #{sensor.name} * ZoneTimestep * 3600" + else + s += " + #{sensor.name}" + end + end + program.addLine(s) if sensors.size > 0 + end + end + + # EMS program: Internal Gains, Lighting, Infiltration, Natural Ventilation, Mechanical Ventilation, Ducts + { 'intgains' => intgains_sensors, + 'lighting' => lightings_sensors, + 'infil' => infil_sensors, + 'natvent' => natvent_sensors, + 'whf' => whf_sensors, + 'mechvent' => mechvents_sensors, + 'ducts' => ducts_sensors }.each do |loadtype, sensors| + program.addLine("Set hr_#{loadtype} = 0") + next if sensors.empty? + + s = "Set hr_#{loadtype} = hr_#{loadtype}" + sensors.each do |sensor| + if ['intgains', 'lighting', 'mechvent', 'ducts'].include? loadtype + s += " - #{sensor.name}" + elsif sensor.name.to_s.include? 'gain' + s += " - #{sensor.name}" + elsif sensor.name.to_s.include? 'loss' + s += " + #{sensor.name}" + end + end + program.addLine(s) + end + intgains_dhw_sensors.each do |sensor, vals| + off_loss, on_loss, rtf_sensor = vals + program.addLine("Set hr_intgains = hr_intgains + #{sensor.name} * (#{off_loss}*(1-#{rtf_sensor.name}) + #{on_loss}*#{rtf_sensor.name})") # Water heater tank losses to zone + end + if (not ducts_mix_loss_sensor.nil?) && (not ducts_mix_gain_sensor.nil?) + program.addLine("Set hr_ducts = hr_ducts + (#{ducts_mix_loss_sensor.name} - #{ducts_mix_gain_sensor.name})") + end + + # EMS Sensors: Indoor temperature, setpoints + tin_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mean Air Temperature') + tin_sensor.setName('tin s') + tin_sensor.setKeyName(conditioned_zone.name.to_s) + thermostat = nil + if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized + thermostat = conditioned_zone.thermostatSetpointDualSetpoint.get + + htg_sp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + htg_sp_sensor.setName('htg sp s') + htg_sp_sensor.setKeyName(thermostat.heatingSetpointTemperatureSchedule.get.name.to_s) + + clg_sp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + clg_sp_sensor.setName('clg sp s') + clg_sp_sensor.setKeyName(thermostat.coolingSetpointTemperatureSchedule.get.name.to_s) + end + + # EMS program: Heating vs Cooling logic + program.addLine('Set htg_mode = 0') + program.addLine('Set clg_mode = 0') + program.addLine("If (#{htg_cond_load_sensors[unit].name} > 0)") # Assign hour to heating if heating load + program.addLine(" Set htg_mode = #{total_heat_load_serveds[unit]}") + program.addLine("ElseIf (#{clg_cond_load_sensors[unit].name} > 0)") # Assign hour to cooling if cooling load + program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") + program.addLine('Else') + program.addLine(' Set htg_season = 0') + program.addLine(' Set clg_season = 0') + if not season_day_nums[unit].nil? + # Determine whether we're in the heating and/or cooling season + if season_day_nums[unit][:clg_end] >= season_day_nums[unit][:clg_start] + program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) && (DayOfYear <= #{season_day_nums[unit][:clg_end]}))") + else + program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:clg_start]}) || (DayOfYear <= #{season_day_nums[unit][:clg_end]}))") + end + program.addLine(' Set clg_season = 1') + program.addLine(' EndIf') + if season_day_nums[unit][:htg_end] >= season_day_nums[unit][:htg_start] + program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) && (DayOfYear <= #{season_day_nums[unit][:htg_end]}))") + else + program.addLine(" If ((DayOfYear >= #{season_day_nums[unit][:htg_start]}) || (DayOfYear <= #{season_day_nums[unit][:htg_end]}))") + end + program.addLine(' Set htg_season = 1') + program.addLine(' EndIf') + end + program.addLine(" If ((#{natvent_sensors[0].name} <> 0) || (#{natvent_sensors[1].name} <> 0)) && (clg_season == 1)") # Assign hour to cooling if natural ventilation is operating + program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") + program.addLine(" ElseIf ((#{whf_sensors[0].name} <> 0) || (#{whf_sensors[1].name} <> 0)) && (clg_season == 1)") # Assign hour to cooling if whole house fan is operating + program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") + if not thermostat.nil? + program.addLine(' Else') # Indoor temperature floating between setpoints; determine assignment by comparing to average of heating/cooling setpoints + program.addLine(" Set Tmid_setpoint = (#{htg_sp_sensor.name} + #{clg_sp_sensor.name}) / 2") + program.addLine(" If (#{tin_sensor.name} > Tmid_setpoint) && (clg_season == 1)") + program.addLine(" Set clg_mode = #{total_cool_load_serveds[unit]}") + program.addLine(" ElseIf (#{tin_sensor.name} < Tmid_setpoint) && (htg_season == 1)") + program.addLine(" Set htg_mode = #{total_heat_load_serveds[unit]}") + program.addLine(' EndIf') + end + program.addLine(' EndIf') + program.addLine('EndIf') + + unit_multiplier = hpxml_bldg.building_construction.number_of_units + [:htg, :clg].each do |mode| + if mode == :htg + sign = '' + else + sign = '-' + end + surf_names.each do |surf_name| + program.addLine("Set loads_#{mode}_#{surf_name} = loads_#{mode}_#{surf_name} + (#{sign}hr_#{surf_name} * #{mode}_mode * #{unit_multiplier})") + end + nonsurf_names.each do |nonsurf_name| + program.addLine("Set loads_#{mode}_#{nonsurf_name} = loads_#{mode}_#{nonsurf_name} + (#{sign}hr_#{nonsurf_name} * #{mode}_mode * #{unit_multiplier})") + end + end + end + + # EMS calling manager + program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + program_calling_manager.setName("#{program.name} calling manager") + program_calling_manager.setCallingPoint('EndOfZoneTimestepAfterZoneReporting') + program_calling_manager.addProgram(program) + end + + # Creates airflow outputs (for infiltration, ventilation, etc.) that sum across all individual dwelling + # units for output reporting. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @return [nil] + def self.apply_total_airflows_ems_program(model, hpxml_osm_map) + # Retrieve objects + infil_vars = [] + mechvent_vars = [] + natvent_vars = [] + whf_vars = [] + unit_multipliers = [] + hpxml_osm_map.each do |hpxml_bldg, unit_model| + infil_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeInfiltration } + mechvent_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeMechanicalVentilation } + natvent_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeNaturalVentilation } + whf_vars << unit_model.getEnergyManagementSystemGlobalVariables.find { |v| v.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeWholeHouseFan } + unit_multipliers << hpxml_bldg.building_construction.number_of_units + end + + # EMS program + program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + program.setName('total airflows program') + program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeTotalAirflowsProgram) + program.addLine('Set total_infil_flow_rate = 0') + program.addLine('Set total_mechvent_flow_rate = 0') + program.addLine('Set total_natvent_flow_rate = 0') + program.addLine('Set total_whf_flow_rate = 0') + infil_vars.each_with_index do |infil_var, i| + program.addLine("Set total_infil_flow_rate = total_infil_flow_rate + (#{infil_var.name} * #{unit_multipliers[i]})") + end + mechvent_vars.each_with_index do |mechvent_var, i| + program.addLine("Set total_mechvent_flow_rate = total_mechvent_flow_rate + (#{mechvent_var.name} * #{unit_multipliers[i]})") + end + natvent_vars.each_with_index do |natvent_var, i| + program.addLine("Set total_natvent_flow_rate = total_natvent_flow_rate + (#{natvent_var.name} * #{unit_multipliers[i]})") + end + whf_vars.each_with_index do |whf_var, i| + program.addLine("Set total_whf_flow_rate = total_whf_flow_rate + (#{whf_var.name} * #{unit_multipliers[i]})") + end + + # EMS calling manager + program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + program_calling_manager.setName("#{program.name} calling manager") + program_calling_manager.setCallingPoint('EndOfZoneTimestepAfterZoneReporting') + program_calling_manager.addProgram(program) + end + + # Populate fields of both unique OpenStudio objects OutputJSON and OutputControlFiles based on the debug argument. + # Always request MessagePack output. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param debug [Boolean] true writes in.osm, generates additional log output, and creates all E+ output files + # @return [nil] + def self.apply_output_files(model, debug) + oj = model.getOutputJSON + oj.setOptionType('TimeSeriesAndTabular') + oj.setOutputJSON(debug) + oj.setOutputMessagePack(true) # Used by ReportSimulationOutput reporting measure + + ocf = model.getOutputControlFiles + ocf.setOutputAUDIT(debug) + ocf.setOutputCSV(debug) + ocf.setOutputBND(debug) + ocf.setOutputEIO(debug) + ocf.setOutputESO(debug) + ocf.setOutputMDD(debug) + ocf.setOutputMTD(debug) + ocf.setOutputMTR(debug) + ocf.setOutputRDD(debug) + ocf.setOutputSHD(debug) + ocf.setOutputCSV(debug) + ocf.setOutputSQLite(debug) + ocf.setOutputPerfLog(debug) + end + + # Store some data for use in reporting measure. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml [HPXML] HPXML object + # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit + # @param hpxml_path [String] Path to the HPXML file + # @param building_id [String] HPXML Building ID + # @param hpxml_defaults_path [TODO] TODO + # @return [nil] + def self.apply_additional_properties(model, hpxml, hpxml_osm_map, hpxml_path, building_id, hpxml_defaults_path) + additionalProperties = model.getBuilding.additionalProperties + additionalProperties.setFeature('hpxml_path', hpxml_path) + additionalProperties.setFeature('hpxml_defaults_path', hpxml_defaults_path) + additionalProperties.setFeature('building_id', building_id.to_s) + additionalProperties.setFeature('emissions_scenario_names', hpxml.header.emissions_scenarios.map { |s| s.name }.to_s) + additionalProperties.setFeature('emissions_scenario_types', hpxml.header.emissions_scenarios.map { |s| s.emissions_type }.to_s) + heated_zones, cooled_zones = [], [] + hpxml_osm_map.each do |hpxml_bldg, unit_model| + conditioned_zone_name = unit_model.getThermalZones.find { |z| z.additionalProperties.getFeatureAsString('ObjectType').to_s == HPXML::LocationConditionedSpace }.name.to_s + + heated_zones << conditioned_zone_name if hpxml_bldg.total_fraction_heat_load_served > 0 + cooled_zones << conditioned_zone_name if hpxml_bldg.total_fraction_cool_load_served > 0 + end + additionalProperties.setFeature('heated_zones', heated_zones.to_s) + additionalProperties.setFeature('cooled_zones', cooled_zones.to_s) + additionalProperties.setFeature('is_southern_hemisphere', hpxml_osm_map.keys[0].latitude < 0) + end + + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @return [nil] + def self.apply_ems_debug_output(model) + oems = model.getOutputEnergyManagementSystem + oems.setActuatorAvailabilityDictionaryReporting('Verbose') + oems.setInternalVariableAvailabilityDictionaryReporting('Verbose') + oems.setEMSRuntimeLanguageDebugOutputLevel('Verbose') + end + # TODO # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit diff --git a/HPXMLtoOpenStudio/resources/pv.rb b/HPXMLtoOpenStudio/resources/pv.rb index 0fbf1942af..dc7a632564 100644 --- a/HPXMLtoOpenStudio/resources/pv.rb +++ b/HPXMLtoOpenStudio/resources/pv.rb @@ -2,16 +2,37 @@ # Collection of methods for adding photovoltaic-related OpenStudio objects. module PV + # Adds any HPXML Photovoltaics to the OpenStudio model. + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [nil] + def self.apply_pv_systems(model, hpxml_bldg) + # Error-checking + hpxml_bldg.pv_systems.each do |pv_system| + next if pv_system.inverter.inverter_efficiency == hpxml_bldg.pv_systems[0].inverter.inverter_efficiency + + fail 'Expected all InverterEfficiency values to be equal.' + end + + hpxml_bldg.pv_systems.each do |pv_system| + apply_pv_system(model, hpxml_bldg, pv_system) + end + end + + # Adds the HPXML Photovoltaic to the OpenStudio model. + # # Apply a photovoltaic system to the model using OpenStudio ElectricLoadCenterDistribution, ElectricLoadCenterInverterPVWatts, and GeneratorPVWatts objects. # The system may be shared, in which case max power is apportioned to the dwelling unit by total number of bedrooms served. # In case an ElectricLoadCenterDistribution object does not already exist, a new ElectricLoadCenterInverterPVWatts object is set on a new ElectricLoadCenterDistribution object. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param nbeds [Integer] Number of bedrooms in the dwelling unit + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param pv_system [HPXML::PVSystem] Object that defines a single solar electric photovoltaic (PV) system - # @param unit_multiplier [Integer] Number of similar dwelling units # @return [nil] - def self.apply(model, nbeds, pv_system, unit_multiplier) + def self.apply_pv_system(model, hpxml_bldg, pv_system) + nbeds = hpxml_bldg.building_construction.number_of_bedrooms + unit_multiplier = hpxml_bldg.building_construction.number_of_units obj_name = pv_system.id # Apply unit multiplier From 0741681d8d3d82875716057f76f4ed2e7c73f7b8 Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 16 Sep 2024 16:58:59 -0600 Subject: [PATCH 10/16] Final pass for now (hot water and airflow). --- BuildResidentialHPXML/measure.xml | 46 +- HPXMLtoOpenStudio/measure.rb | 239 ++-------- HPXMLtoOpenStudio/measure.xml | 36 +- HPXMLtoOpenStudio/resources/airflow.rb | 120 ++++- HPXMLtoOpenStudio/resources/battery.rb | 4 +- HPXMLtoOpenStudio/resources/constructions.rb | 433 +++++++----------- HPXMLtoOpenStudio/resources/generator.rb | 4 +- HPXMLtoOpenStudio/resources/geometry.rb | 54 +-- .../resources/hotwater_appliances.rb | 122 +---- HPXMLtoOpenStudio/resources/hpxml_defaults.rb | 7 + HPXMLtoOpenStudio/resources/hvac.rb | 31 +- HPXMLtoOpenStudio/resources/internal_gains.rb | 9 +- HPXMLtoOpenStudio/resources/lighting.rb | 2 +- HPXMLtoOpenStudio/resources/misc_loads.rb | 2 +- HPXMLtoOpenStudio/resources/pv.rb | 4 +- HPXMLtoOpenStudio/resources/waterheater.rb | 264 ++++++++--- HPXMLtoOpenStudio/resources/xmlhelper.rb | 2 +- HPXMLtoOpenStudio/tests/test_defaults.rb | 2 +- 18 files changed, 633 insertions(+), 748 deletions(-) diff --git a/BuildResidentialHPXML/measure.xml b/BuildResidentialHPXML/measure.xml index 0a922154ed..6f315bfcc1 100644 --- a/BuildResidentialHPXML/measure.xml +++ b/BuildResidentialHPXML/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_hpxml a13a8983-2b01-4930-8af2-42030b6e4233 - de78116f-221f-4cab-9488-203b60d0821f - 2024-09-16T19:31:14Z + 84219786-d19f-45a5-b735-1366ee7c2d99 + 2024-09-16T22:42:52Z 2C38F48B BuildResidentialHPXML HPXML Builder @@ -7455,6 +7455,48 @@ resource C62D3E76 + + extra_files/base-mf.xml + xml + test + 06C3D0DD + + + extra_files/base-mf2.xml + xml + test + 04582640 + + + extra_files/base-sfa.xml + xml + test + 16ED9F15 + + + extra_files/base-sfa2.xml + xml + test + 1B60C132 + + + extra_files/base-sfa3.xml + xml + test + DD9CD517 + + + extra_files/base-sfd.xml + xml + test + 79463062 + + + extra_files/base-sfd2.xml + xml + test + 9FC7FBA9 + test_build_residential_hpxml.rb rb diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 84ca3f6837..158c4e9727 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -155,19 +155,6 @@ def run(model, runner, user_arguments) end return false unless hpxml.errors.empty? - # Hidden feature: Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions - if hpxml.header.eri_calculation_version.nil? - hpxml.header.eri_calculation_version = 'latest' - end - if hpxml.header.eri_calculation_version == 'latest' - hpxml.header.eri_calculation_version = Constants::ERIVersions[-1] - end - - # Hidden feature: Whether to override certain assumptions to better match the ASHRAE 140 specification - if hpxml.header.apply_ashrae140_assumptions.nil? - hpxml.header.apply_ashrae140_assumptions = false - end - # Process weather once upfront epw_path = Location.get_epw_path(hpxml.buildings[0], args[:hpxml_path]) weather = WeatherFile.new(epw_path: epw_path, runner: runner, hpxml: hpxml) @@ -286,12 +273,8 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched model.setStrictnessLevel('None'.to_StrictnessLevel) # Init - apply_init(hpxml_bldg, hpxml.header) - - # Simulation Controls + init(hpxml_bldg, hpxml.header) SimControls.apply(model, hpxml.header) - - # Location Location.apply(model, weather, hpxml_bldg, hpxml.header, epw_path) # Geometry/Enclosure @@ -300,7 +283,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched Geometry.apply_walls(runner, model, spaces, hpxml_bldg, hpxml.header) Geometry.apply_rim_joists(runner, model, spaces, hpxml_bldg) Geometry.apply_floors(runner, model, spaces, hpxml_bldg, hpxml.header) - Geometry.apply_foundation_walls_slabs(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + Geometry.apply_foundation_walls_slabs(runner, model, spaces, hpxml_bldg) Geometry.apply_windows(model, spaces, hpxml_bldg, hpxml.header) Geometry.apply_doors(model, spaces, hpxml_bldg) Geometry.apply_skylights(model, spaces, hpxml_bldg, hpxml.header) @@ -311,12 +294,12 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched Geometry.apply_building_unit(model, unit_num) # Systems - hvac_data = HVAC.apply_hvac_systems(model, runner, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + hvac_data = HVAC.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) HVAC.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml.header) HVAC.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml.header, schedules_file) - # Hot Water - add_hot_water_and_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + # Hot Water & Appliances + Waterheater.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) # Lighting Lighting.apply(runner, model, spaces, hpxml_bldg, hpxml.header, schedules_file) @@ -327,14 +310,14 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched MiscLoads.apply_pools_and_permanent_spas(runner, model, spaces, hpxml_bldg, hpxml.header, schedules_file) # Internal Gains - InternalGains.apply_building_occupants(model, runner, hpxml_bldg, hpxml.header, spaces, schedules_file) - InternalGains.apply_general_water_use(model, runner, hpxml_bldg, hpxml.header, spaces, schedules_file) + InternalGains.apply_building_occupants(runner, model, hpxml_bldg, hpxml.header, spaces, schedules_file) + InternalGains.apply_general_water_use(runner, model, hpxml_bldg, hpxml.header, spaces, schedules_file) # Other - add_airflow(runner, model, weather, spaces, hpxml_bldg, hpxml.header, hvac_data, schedules_file) - PV.apply_pv_systems(model, hpxml_bldg) - Generator.apply_generators(model, hpxml_bldg) - Battery.apply_batteries(runner, model, spaces, hpxml_bldg, schedules_file) + Airflow.apply(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, hvac_data) + PV.apply(model, hpxml_bldg) + Generator.apply(model, hpxml_bldg) + Battery.apply(runner, model, spaces, hpxml_bldg, schedules_file) end # TODO @@ -342,16 +325,27 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @return [nil] - def apply_init(hpxml_bldg, hpxml_header) - # Apply unit multipliers to HVAC systems and water heaters - HVAC.apply_unit_multiplier(hpxml_bldg, hpxml_header) - # Ensure that no capacities/airflows are zero in order to prevent potential E+ errors. - HVAC.ensure_nonzero_sizing_values(hpxml_bldg) + def init(hpxml_bldg, hpxml_header) + # Store the fraction of windows operable before we collapse surfaces + hpxml_bldg.additional_properties.initial_frac_windows_operable = hpxml_bldg.fraction_of_windows_operable() + # Make adjustments for modeling purposes - @frac_windows_operable = hpxml_bldg.fraction_of_windows_operable() hpxml_bldg.collapse_enclosure_surfaces() # Speeds up simulation hpxml_bldg.delete_adiabatic_subsurfaces() # EnergyPlus doesn't allow this + # Hidden feature: Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions + if hpxml_header.eri_calculation_version.nil? + hpxml_header.eri_calculation_version = 'latest' + end + if hpxml_header.eri_calculation_version == 'latest' + hpxml_header.eri_calculation_version = Constants::ERIVersions[-1] + end + + # Hidden feature: Whether to override certain assumptions to better match the ASHRAE 140 specification + if hpxml_header.apply_ashrae140_assumptions.nil? + hpxml_header.apply_ashrae140_assumptions = false + end + if not hpxml_bldg.building_occupancy.number_of_residents.nil? # If zero occupants, ensure end uses of interest are zeroed out if (hpxml_bldg.building_occupancy.number_of_residents == 0) && (not hpxml_header.apply_ashrae140_assumptions) @@ -366,183 +360,6 @@ def apply_init(hpxml_bldg, hpxml_header) end end end - - # First assign OpenStudio Space object for appliances based on HPXML Location. - # Then adds any of the following to the OpenStudio model: - # - HPXML Clothes Washers - # - HPXML Clothes Dryers - # - HPXML Dishwashers - # - HPXML Refrigerators - # - HPXML Freezers - # - HPXML Cooking Ranges / Ovens - # - HPXML Hot Water Distribution - # - HPXML Solar Thermal System - # - HPXML Water Heating Systems - # - HPXML Water Fixtures - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @return [nil] - def add_hot_water_and_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) - # Distribution - if hpxml_bldg.water_heating_systems.size > 0 - hot_water_distribution = hpxml_bldg.hot_water_distributions[0] - end - - # Solar thermal system - solar_thermal_system = nil - if hpxml_bldg.solar_thermal_systems.size > 0 - solar_thermal_system = hpxml_bldg.solar_thermal_systems[0] - end - - # Water Heater - unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:WaterHeater].name, hpxml_header.unavailable_periods) - unit_multiplier = hpxml_bldg.building_construction.number_of_units - has_uncond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementUnconditioned) - has_cond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementConditioned) - nbeds = hpxml_bldg.building_construction.number_of_bedrooms - cfa = hpxml_bldg.building_construction.conditioned_floor_area - ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors - eri_version = hpxml_header.eri_calculation_version - plantloop_map = {} - hpxml_bldg.water_heating_systems.each do |water_heating_system| - loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) - - ec_adj = HotWaterAndAppliances.get_dist_energy_consumption_adjustment(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl, water_heating_system, hot_water_distribution) - - sys_id = water_heating_system.id - if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeStorage - plantloop_map[sys_id] = Waterheater.apply_tank(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) - elsif water_heating_system.water_heater_type == HPXML::WaterHeaterTypeTankless - plantloop_map[sys_id] = Waterheater.apply_tankless(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) - elsif water_heating_system.water_heater_type == HPXML::WaterHeaterTypeHeatPump - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - plantloop_map[sys_id] = Waterheater.apply_heatpump(model, runner, loc_space, loc_schedule, hpxml_bldg.elevation, water_heating_system, ec_adj, solar_thermal_system, conditioned_zone, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) - elsif [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? water_heating_system.water_heater_type - plantloop_map[sys_id] = Waterheater.apply_combi(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) - else - fail "Unhandled water heater (#{water_heating_system.water_heater_type})." - end - end - - # Hot water fixtures and appliances - HotWaterAndAppliances.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_water_distribution, - solar_thermal_system, eri_version, schedules_file, plantloop_map, - hpxml_header.unavailable_periods, hpxml_bldg.building_construction.number_of_units) - - if (not solar_thermal_system.nil?) && (not solar_thermal_system.collector_area.nil?) # Detailed solar water heater - loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) - Waterheater.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_system, plantloop_map, unit_multiplier) - end - - # Add combi-system EMS program with water use equipment information - Waterheater.apply_combi_system_EMS(model, hpxml_bldg.water_heating_systems, plantloop_map) - end - - # Adds HPXML Air Infiltration and HPXML HVAC Distribution to the OpenStudio model. - # TODO for adding more description (e.g., around checks and warnings) - # - # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects - # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param hvac_data [Array] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects, HVAC unavailable period objects - # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @return [nil] - def add_airflow(runner, model, weather, spaces, hpxml_bldg, hpxml_header, hvac_data, schedules_file) - cfa = hpxml_bldg.building_construction.conditioned_floor_area - airloop_map, hvac_unavailable_periods = hvac_data - - # Ducts - duct_systems = {} - hpxml_bldg.hvac_distributions.each do |hvac_distribution| - next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir - - air_ducts = Airflow.create_ducts(model, hvac_distribution, spaces) - next if air_ducts.empty? - - # Connect AirLoopHVACs to ducts - added_ducts = false - hvac_distribution.hvac_systems.each do |hvac_system| - next if airloop_map[hvac_system.id].nil? - - object = airloop_map[hvac_system.id] - if duct_systems[air_ducts].nil? - duct_systems[air_ducts] = object - added_ducts = true - elsif duct_systems[air_ducts] != object - # Multiple air loops associated with this duct system, treat - # as separate duct systems. - air_ducts2 = Airflow.create_ducts(model, hvac_distribution, spaces) - duct_systems[air_ducts2] = object - added_ducts = true - end - end - if not added_ducts - fail 'Unexpected error adding ducts to model.' - end - end - - # Duct leakage to outside warnings? - # Need to check here instead of in schematron in case duct locations are defaulted - hpxml_bldg.hvac_distributions.each do |hvac_distribution| - next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir - next if hvac_distribution.duct_leakage_measurements.empty? - - units = hvac_distribution.duct_leakage_measurements[0].duct_leakage_units - lto_measurements = hvac_distribution.duct_leakage_measurements.select { |dlm| dlm.duct_leakage_total_or_to_outside == HPXML::DuctLeakageToOutside } - sum_lto = lto_measurements.map { |dlm| dlm.duct_leakage_value }.sum(0.0) - - if hvac_distribution.ducts.select { |d| !HPXML::conditioned_locations_this_unit.include?(d.duct_location) }.size == 0 - # If ducts completely in conditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 5%) - issue_warning = false - if units == HPXML::UnitsCFM25 - issue_warning = true if sum_lto > 0.04 * cfa - elsif units == HPXML::UnitsCFM50 - issue_warning = true if sum_lto > 0.06 * cfa - elsif units == HPXML::UnitsPercent - issue_warning = true if sum_lto > 0.05 - end - next unless issue_warning - - runner.registerWarning('Ducts are entirely within conditioned space but there is moderate leakage to the outside. Leakage to the outside is typically zero or near-zero in these situations, consider revising leakage values. Leakage will be modeled as heat lost to the ambient environment.') - else - # If ducts in unconditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 40%) - issue_warning = false - if units == HPXML::UnitsCFM25 - issue_warning = true if sum_lto >= 0.32 * cfa - elsif units == HPXML::UnitsCFM50 - issue_warning = true if sum_lto >= 0.48 * cfa - elsif units == HPXML::UnitsPercent - issue_warning = true if sum_lto >= 0.4 - end - next unless issue_warning - - runner.registerWarning('Very high sum of supply + return duct leakage to the outside; double-check inputs.') - end - end - - # Create HVAC availability sensor - hvac_availability_sensor = nil - if not hvac_unavailable_periods.empty? - avail_sch = ScheduleConstant.new(model, SchedulesFile::Columns[:HVAC].name, 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: hvac_unavailable_periods) - - hvac_availability_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') - hvac_availability_sensor.setName('hvac availability s') - hvac_availability_sensor.setKeyName(avail_sch.schedule.name.to_s) - hvac_availability_sensor.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeHVACAvailabilitySensor) - end - - Airflow.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_systems, - airloop_map, @frac_windows_operable, schedules_file, hvac_availability_sensor) - end end # register the measure to be used by the application diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 475ae78767..742f95fe14 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 735921d7-e5e7-46fa-969c-87cafd760d8a - 2024-09-16T20:47:51Z + a294ff2f-7ad6-4abf-aecf-f93f4927d01d + 2024-09-16T22:58:15Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,19 +183,19 @@ measure.rb rb script - 3B5C6261 + 008103D2 airflow.rb rb resource - 5929C59D + 27C404EF battery.rb rb resource - 1F96B42D + 01C783CF calendar.rb @@ -213,7 +213,7 @@ constructions.rb rb resource - DE13291A + C9CD03A7 data/Xing_okstate_0664D_13659_Table_A-3.csv @@ -333,19 +333,19 @@ generator.rb rb resource - 6952CD2A + B5787B92 geometry.rb rb resource - B1B13531 + 30759588 hotwater_appliances.rb rb resource - 14725DD9 + 87025692 hpxml.rb @@ -357,7 +357,7 @@ hpxml_defaults.rb rb resource - 18DD0B67 + 4F6E5BAE hpxml_schema/HPXML.xsd @@ -387,7 +387,7 @@ hvac.rb rb resource - 7BB9BD6F + BF48A30C hvac_sizing.rb @@ -399,13 +399,13 @@ internal_gains.rb rb resource - FD08F8A8 + 234EE1ED lighting.rb rb resource - 3851C46B + 22236F72 location.rb @@ -441,7 +441,7 @@ misc_loads.rb rb resource - 9A5914F8 + EF6685E4 model.rb @@ -465,7 +465,7 @@ pv.rb rb resource - FF6B391A + C87470E8 schedule_files/battery.csv @@ -627,7 +627,7 @@ waterheater.rb rb resource - 20E8AE0A + 274CCF96 weather.rb @@ -639,7 +639,7 @@ xmlhelper.rb rb resource - 7CBAECF6 + D1BB113E xmlvalidator.rb @@ -663,7 +663,7 @@ test_defaults.rb rb test - 92B11778 + E943ED22 test_enclosure.rb diff --git a/HPXMLtoOpenStudio/resources/airflow.rb b/HPXMLtoOpenStudio/resources/airflow.rb index 5dda4d43ac..9d899fee32 100644 --- a/HPXMLtoOpenStudio/resources/airflow.rb +++ b/HPXMLtoOpenStudio/resources/airflow.rb @@ -8,23 +8,19 @@ module Airflow AssumedInsideTemp = 73.5 # (F) Gravity = 32.174 # acceleration of gravity (ft/s2) - # TODO + # Adds HPXML Air Infiltration and HPXML HVAC Distribution to the OpenStudio model. + # TODO for adding more description (e.g., around checks and warnings) # - # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param duct_systems [TODO] TODO - # @param airloop_map [TODO] TODO - # @param frac_windows_operable [TODO] TODO + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param hvac_availability_sensor [TODO] TODO + # @param hvac_data [Array] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects, HVAC unavailable period objects # @return [TODO] TODO - def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_systems, - airloop_map, frac_windows_operable, schedules_file, hvac_availability_sensor) - + def self.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, hvac_data) # Global variables @runner = runner @@ -37,9 +33,10 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_sy @apply_ashrae140_assumptions = hpxml_header.apply_ashrae140_assumptions @cooking_range_in_cond_space = hpxml_bldg.cooking_ranges.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml_bldg.cooking_ranges[0].location) @clothes_dryer_in_cond_space = hpxml_bldg.clothes_dryers.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml_bldg.clothes_dryers[0].location) - @hvac_availability_sensor = hvac_availability_sensor cfa = hpxml_bldg.building_construction.conditioned_floor_area unavailable_periods = hpxml_header.unavailable_periods + airloop_map, hvac_unavailable_periods = hvac_data + frac_windows_operable = hpxml_bldg.additional_properties.initial_frac_windows_operable # Global sensors @@ -66,6 +63,17 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_sy @adiabatic_const = nil + # Create HVAC availability sensor + @hvac_availability_sensor = nil + if not hvac_unavailable_periods.empty? + avail_sch = ScheduleConstant.new(model, SchedulesFile::Columns[:HVAC].name, 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: hvac_unavailable_periods) + + @hvac_availability_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value') + @hvac_availability_sensor.setName('hvac availability s') + @hvac_availability_sensor.setKeyName(avail_sch.schedule.name.to_s) + @hvac_availability_sensor.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeHVACAvailabilitySensor) + end + # Ventilation fans vent_fans_mech = [] vent_fans_kitchen = [] @@ -107,6 +115,8 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, duct_sy # Apply ducts duct_lk_imbals = [] + duct_systems = create_duct_systems(model, spaces, hpxml_bldg, airloop_map) + check_duct_leakage(runner, hpxml_bldg) duct_systems.each do |ducts, object| apply_ducts(model, ducts, object, vent_fans_mech, hpxml_bldg.building_construction.number_of_units, duct_lk_imbals) end @@ -873,7 +883,7 @@ def self.create_other_equipment_object_and_actuator(model:, name:, space:, frac_ # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param vent_fans_mech [TODO] TODO - # @param airloop_map [TODO] TODO + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies # @return [TODO] TODO def self.initialize_cfis(model, vent_fans_mech, airloop_map, unavailable_periods) @@ -970,6 +980,53 @@ def self.initialize_fan_objects(model, osm_object) end end + # TODO + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @return [nil] + def self.check_duct_leakage(runner, hpxml_bldg) + # Duct leakage to outside warnings? + # Need to check here instead of in schematron in case duct locations are defaulted + cfa = hpxml_bldg.building_construction.conditioned_floor_area + hpxml_bldg.hvac_distributions.each do |hvac_distribution| + next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir + next if hvac_distribution.duct_leakage_measurements.empty? + + units = hvac_distribution.duct_leakage_measurements[0].duct_leakage_units + lto_measurements = hvac_distribution.duct_leakage_measurements.select { |dlm| dlm.duct_leakage_total_or_to_outside == HPXML::DuctLeakageToOutside } + sum_lto = lto_measurements.map { |dlm| dlm.duct_leakage_value }.sum(0.0) + + if hvac_distribution.ducts.select { |d| !HPXML::conditioned_locations_this_unit.include?(d.duct_location) }.size == 0 + # If ducts completely in conditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 5%) + issue_warning = false + if units == HPXML::UnitsCFM25 + issue_warning = true if sum_lto > 0.04 * cfa + elsif units == HPXML::UnitsCFM50 + issue_warning = true if sum_lto > 0.06 * cfa + elsif units == HPXML::UnitsPercent + issue_warning = true if sum_lto > 0.05 + end + next unless issue_warning + + runner.registerWarning('Ducts are entirely within conditioned space but there is moderate leakage to the outside. Leakage to the outside is typically zero or near-zero in these situations, consider revising leakage values. Leakage will be modeled as heat lost to the ambient environment.') + else + # If ducts in unconditioned space, issue warning if duct leakage to outside above a certain threshold (e.g., 40%) + issue_warning = false + if units == HPXML::UnitsCFM25 + issue_warning = true if sum_lto >= 0.32 * cfa + elsif units == HPXML::UnitsCFM50 + issue_warning = true if sum_lto >= 0.48 * cfa + elsif units == HPXML::UnitsPercent + issue_warning = true if sum_lto >= 0.4 + end + next unless issue_warning + + runner.registerWarning('Very high sum of supply + return duct leakage to the outside; double-check inputs.') + end + end + end + # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -2729,6 +2786,45 @@ def self.get_mech_vent_qfan_cfm(q_tot, q_inf, is_balanced, frac_imbal, a_ext, bl return [q_fan, 0.0].max end + # TODO + # + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @return [TODO] TODO + def self.create_duct_systems(model, spaces, hpxml_bldg, airloop_map) + duct_systems = {} + hpxml_bldg.hvac_distributions.each do |hvac_distribution| + next unless hvac_distribution.distribution_system_type == HPXML::HVACDistributionTypeAir + + air_ducts = create_ducts(model, hvac_distribution, spaces) + next if air_ducts.empty? + + # Connect AirLoopHVACs to ducts + added_ducts = false + hvac_distribution.hvac_systems.each do |hvac_system| + next if airloop_map[hvac_system.id].nil? + + object = airloop_map[hvac_system.id] + if duct_systems[air_ducts].nil? + duct_systems[air_ducts] = object + added_ducts = true + elsif duct_systems[air_ducts] != object + # Multiple air loops associated with this duct system, treat + # as separate duct systems. + air_ducts2 = create_ducts(model, hvac_distribution, spaces) + duct_systems[air_ducts2] = object + added_ducts = true + end + end + if not added_ducts + fail 'Unexpected error adding ducts to model.' + end + end + return duct_systems + end + # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object diff --git a/HPXMLtoOpenStudio/resources/battery.rb b/HPXMLtoOpenStudio/resources/battery.rb index 9181e9d2b1..ac807a24b0 100644 --- a/HPXMLtoOpenStudio/resources/battery.rb +++ b/HPXMLtoOpenStudio/resources/battery.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Collection of methods for adding battery-related OpenStudio objects. +# Collection of methods related to batteries. module Battery # Adds any HPXML Batteries to the OpenStudio model. # @@ -10,7 +10,7 @@ module Battery # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_batteries(runner, model, spaces, hpxml_bldg, schedules_file) + def self.apply(runner, model, spaces, hpxml_bldg, schedules_file) hpxml_bldg.batteries.each do |battery| apply_battery(runner, model, spaces, hpxml_bldg, battery, schedules_file) end diff --git a/HPXMLtoOpenStudio/resources/constructions.rb b/HPXMLtoOpenStudio/resources/constructions.rb index 46a1d5ba3f..e9fdd0d057 100644 --- a/HPXMLtoOpenStudio/resources/constructions.rb +++ b/HPXMLtoOpenStudio/resources/constructions.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to surface constructions. module Constructions # Container class for walls, floors/ceilings, roofs, etc. @@ -25,24 +25,10 @@ module Constructions # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_wood_stud_wall(model, - surfaces, - constr_name, - cavity_r, - install_grade, - cavity_depth_in, - cavity_filled, - framing_factor, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_wood_stud_wall(model, surfaces, constr_name, cavity_r, install_grade, cavity_depth_in, + cavity_filled, framing_factor, mat_int_finish, osb_thick_in, rigid_r, + mat_ext_finish, has_radiant_barrier, inside_film, outside_film, + radiant_barrier_grade, solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -132,26 +118,11 @@ def self.apply_wood_stud_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_double_stud_wall(model, - surfaces, - constr_name, - cavity_r, - install_grade, - stud_depth_in, - gap_depth_in, - framing_factor, - framing_spacing, - is_staggered, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_double_stud_wall(model, surfaces, constr_name, cavity_r, install_grade, stud_depth_in, + gap_depth_in, framing_factor, framing_spacing, is_staggered, + mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, + has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, + solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -252,26 +223,10 @@ def self.apply_double_stud_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_cmu_wall(model, - surfaces, - constr_name, - thick_in, - conductivity, - density, - framing_factor, - furring_r, - furring_cavity_depth, - furring_spacing, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_cmu_wall(model, surfaces, constr_name, thick_in, conductivity, density, framing_factor, + furring_r, furring_cavity_depth, furring_spacing, mat_int_finish, osb_thick_in, + rigid_r, mat_ext_finish, has_radiant_barrier, inside_film, outside_film, + radiant_barrier_grade, solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -365,23 +320,10 @@ def self.apply_cmu_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_icf_wall(model, - surfaces, - constr_name, - icf_r, - ins_thick_in, - concrete_thick_in, - framing_factor, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_icf_wall(model, surfaces, constr_name, icf_r, ins_thick_in, concrete_thick_in, + framing_factor, mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, + has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, + solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -459,23 +401,10 @@ def self.apply_icf_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_sip_wall(model, - surfaces, - constr_name, - sip_r, - sip_thick_in, - framing_factor, - sheathing_thick_in, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_sip_wall(model, surfaces, constr_name, sip_r, sip_thick_in, framing_factor, + sheathing_thick_in, mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, + has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, + solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -563,25 +492,10 @@ def self.apply_sip_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_steel_stud_wall(model, - surfaces, - constr_name, - cavity_r, - install_grade, - cavity_depth, - cavity_filled, - framing_factor, - correction_factor, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_steel_stud_wall(model, surfaces, constr_name, cavity_r, install_grade, cavity_depth, + cavity_filled, framing_factor, correction_factor, mat_int_finish, + osb_thick_in, rigid_r, mat_ext_finish, has_radiant_barrier, inside_film, + outside_film, radiant_barrier_grade, solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -669,23 +583,10 @@ def self.apply_steel_stud_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_generic_layered_wall(model, - surfaces, - constr_name, - thick_ins, - conds, - denss, - specheats, - mat_int_finish, - osb_thick_in, - rigid_r, - mat_ext_finish, - has_radiant_barrier, - inside_film, - outside_film, - radiant_barrier_grade, - solar_absorptance = nil, - emittance = nil) + def self.apply_generic_layered_wall(model, surfaces, constr_name, thick_ins, conds, denss, specheats, + mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, + has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, + solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -776,10 +677,8 @@ def self.apply_generic_layered_wall(model, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_rim_joist(model, surfaces, constr_name, - cavity_r, install_grade, framing_factor, - mat_int_finish, osb_thick_in, - rigid_r, mat_ext_finish, inside_film, + def self.apply_rim_joist(model, surfaces, constr_name, cavity_r, install_grade, framing_factor, + mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, inside_film, outside_film, solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -856,13 +755,10 @@ def self.apply_rim_joist(model, surfaces, constr_name, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_open_cavity_roof(model, surfaces, constr_name, - cavity_r, install_grade, cavity_ins_thick_in, - framing_factor, framing_thick_in, - osb_thick_in, rigid_r, - mat_roofing, has_radiant_barrier, - inside_film, outside_film, radiant_barrier_grade, - solar_absorptance = nil, emittance = nil) + def self.apply_open_cavity_roof(model, surfaces, constr_name, cavity_r, install_grade, + cavity_ins_thick_in, framing_factor, framing_thick_in, osb_thick_in, + rigid_r, mat_roofing, has_radiant_barrier, inside_film, outside_film, + radiant_barrier_grade, solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -950,8 +846,7 @@ def self.apply_open_cavity_roof(model, surfaces, constr_name, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_closed_cavity_roof(model, surfaces, constr_name, - cavity_r, install_grade, cavity_depth, + def self.apply_closed_cavity_roof(model, surfaces, constr_name, cavity_r, install_grade, cavity_depth, filled_cavity, framing_factor, mat_int_finish, osb_thick_in, rigid_r, mat_roofing, has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, @@ -1038,11 +933,10 @@ def self.apply_closed_cavity_roof(model, surfaces, constr_name, # @param outside_film [TODO] TODO # @param radiant_barrier_grade [TODO] TODO # @return [TODO] TODO - def self.apply_wood_frame_floor_ceiling(model, surfaces, constr_name, is_ceiling, - cavity_r, install_grade, - framing_factor, joist_height_in, - plywood_thick_in, rigid_r, mat_int_finish_or_covering, - has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade) + def self.apply_wood_frame_floor_ceiling(model, surfaces, constr_name, is_ceiling, cavity_r, install_grade, + framing_factor, joist_height_in, plywood_thick_in, + rigid_r, mat_int_finish_or_covering, has_radiant_barrier, + inside_film, outside_film, radiant_barrier_grade) # Interior finish below, open cavity above (e.g., attic floor) # Open cavity below, floor covering above (e.g., crawlspace ceiling) @@ -1153,8 +1047,7 @@ def self.apply_wood_frame_floor_ceiling(model, surfaces, constr_name, is_ceiling # @param outside_film [TODO] TODO # @param radiant_barrier_grade [TODO] TODO # @return [TODO] TODO - def self.apply_steel_frame_floor_ceiling(model, surfaces, constr_name, is_ceiling, - cavity_r, install_grade, + def self.apply_steel_frame_floor_ceiling(model, surfaces, constr_name, is_ceiling, cavity_r, install_grade, framing_factor, correction_factor, joist_height_in, plywood_thick_in, rigid_r, mat_int_finish_or_covering, has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade) @@ -1265,11 +1158,10 @@ def self.apply_steel_frame_floor_ceiling(model, surfaces, constr_name, is_ceilin # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_sip_floor_ceiling(model, surfaces, constr_name, is_ceiling, - sip_r, sip_thick_in, framing_factor, - mat_int_finish, osb_thick_in, rigid_r, - mat_ext_finish, has_radiant_barrier, inside_film, outside_film, - radiant_barrier_grade, solar_absorptance = nil, emittance = nil) + def self.apply_sip_floor_ceiling(model, surfaces, constr_name, is_ceiling, sip_r, sip_thick_in, + framing_factor, mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, + has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, + solar_absorptance = nil, emittance = nil) return if surfaces.empty? @@ -1357,9 +1249,8 @@ def self.apply_sip_floor_ceiling(model, surfaces, constr_name, is_ceiling, # @param solar_absorptance [TODO] TODO # @param emittance [TODO] TODO # @return [TODO] TODO - def self.apply_generic_layered_floor_ceiling(model, surfaces, constr_name, is_ceiling, - thick_ins, conds, denss, specheats, - mat_int_finish, osb_thick_in, rigid_r, + def self.apply_generic_layered_floor_ceiling(model, surfaces, constr_name, is_ceiling, thick_ins, conds, + denss, specheats, mat_int_finish, osb_thick_in, rigid_r, mat_ext_finish, has_radiant_barrier, inside_film, outside_film, radiant_barrier_grade, solar_absorptance = nil, emittance = nil) @@ -1455,10 +1346,9 @@ def self.apply_generic_layered_floor_ceiling(model, surfaces, constr_name, is_ce # @param height_above_grade [TODO] TODO # @param soil_k_in [TODO] TODO # @return [TODO] TODO - def self.apply_foundation_wall(model, surfaces, constr_name, - ext_rigid_ins_offset, int_rigid_ins_offset, ext_rigid_ins_height, - int_rigid_ins_height, ext_rigid_r, int_rigid_r, mat_int_finish, - mat_wall, height_above_grade, soil_k_in) + def self.apply_foundation_wall(model, surfaces, constr_name, ext_rigid_ins_offset, int_rigid_ins_offset, + ext_rigid_ins_height, int_rigid_ins_height, ext_rigid_r, int_rigid_r, + mat_int_finish, mat_wall, height_above_grade, soil_k_in) # Create Kiva foundation foundation = apply_kiva_walled_foundation(model, ext_rigid_r, int_rigid_r, ext_rigid_ins_offset, @@ -1499,11 +1389,10 @@ def self.apply_foundation_wall(model, surfaces, constr_name, # @param soil_k_in [TODO] TODO # @param foundation [TODO] TODO # @return [TODO] TODO - def self.apply_foundation_slab(model, surface, constr_name, - under_r, under_width, gap_r, - perimeter_r, perimeter_depth, - whole_r, concrete_thick_in, exposed_perimeter, - mat_carpet, soil_k_in, foundation, ext_horiz_r, ext_horiz_width, ext_horiz_depth) + def self.apply_foundation_slab(model, surface, constr_name, under_r, under_width, gap_r, perimeter_r, + perimeter_depth, whole_r, concrete_thick_in, exposed_perimeter, + mat_carpet, soil_k_in, foundation, ext_horiz_r, ext_horiz_width, + ext_horiz_depth) return if surface.nil? @@ -2126,120 +2015,142 @@ def self.apply_kiva_settings(model, soil_k_in) settings.setSimulationTimestep('Timestep') end - # TODO + # Sets Kiva foundation initial temperatures. # - # @param foundation [TODO] TODO - # @param slab [TODO] TODO + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param conditioned_zone [TODO] TODO - # @param sim_begin_month [TODO] TODO - # @param sim_begin_day [TODO] TODO - # @param sim_year [TODO] TODO # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param foundation_walls_insulated [TODO] TODO - # @param foundation_ceiling_insulated [TODO] TODO - # @return [TODO] TODO - def self.apply_kiva_initial_temp(foundation, slab, weather, conditioned_zone, - sim_begin_month, sim_begin_day, sim_year, schedules_file, - foundation_walls_insulated, foundation_ceiling_insulated) - # Set Kiva foundation initial temperature + # @return [nil] + def self.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_header, conditioned_zone, schedules_file) + sim_begin_month = hpxml_header.sim_begin_month + sim_begin_day = hpxml_header.sim_begin_day + sim_year = hpxml_header.sim_calendar_year outdoor_temp = weather.data.MonthlyAvgDrybulbs[sim_begin_month - 1] - # Approximate indoor temperature - if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized - # Building has HVAC system - setpoint_sch = conditioned_zone.thermostatSetpointDualSetpoint.get - sim_begin_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(sim_begin_month), sim_begin_day, sim_year) - sim_begin_hour = (Calendar.get_day_num_from_month_day(sim_year, sim_begin_month, sim_begin_day) - 1) * 24 - - # Get heating/cooling setpoints for the simulation start - htg_setpoint_sch = setpoint_sch.heatingSetpointTemperatureSchedule.get - if htg_setpoint_sch.to_ScheduleRuleset.is_initialized - htg_day_sch = htg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] - heat_setpoint = UnitConversions.convert(htg_day_sch.values[0], 'C', 'F') - else - heat_setpoint = schedules_file.schedules[SchedulesFile::Columns[:HeatingSetpoint].name][sim_begin_hour] + model.getFoundationKivas.each do |foundation| + interior_adjacent_to = foundation.surfaces[0].space.get.thermalZone.get.additionalProperties.getFeatureAsString('ObjectType').to_s + + foundation_walls_insulated = false + hpxml_bldg.foundation_walls.each do |fnd_wall| + next unless fnd_wall.interior_adjacent_to == interior_adjacent_to + next unless fnd_wall.exterior_adjacent_to == HPXML::LocationGround + + if fnd_wall.insulation_assembly_r_value.to_f > 5 + foundation_walls_insulated = true + elsif fnd_wall.insulation_exterior_r_value.to_f + fnd_wall.insulation_interior_r_value.to_f > 0 + foundation_walls_insulated = true + end end - clg_setpoint_sch = setpoint_sch.coolingSetpointTemperatureSchedule.get - if clg_setpoint_sch.to_ScheduleRuleset.is_initialized - clg_day_sch = clg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] - cool_setpoint = UnitConversions.convert(clg_day_sch.values[0], 'C', 'F') - else - cool_setpoint = schedules_file.schedules[SchedulesFile::Columns[:CoolingSetpoint].name][sim_begin_hour] - end - - # Methodology adapted from https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L303-L313 - heat_balance_temp = UnitConversions.convert(10.0, 'C', 'F') - cool_balance_temp = UnitConversions.convert(15.0, 'C', 'F') - if outdoor_temp < heat_balance_temp - indoor_temp = heat_setpoint - elsif outdoor_temp > cool_balance_temp - indoor_temp = cool_setpoint - elsif cool_balance_temp == heat_balance_temp - indoor_temp = heat_balance_temp - else - weight = (cool_balance_temp - outdoor_temp) / (cool_balance_temp - heat_balance_temp) - indoor_temp = heat_setpoint * weight + cool_setpoint * (1.0 - weight) + + foundation_ceiling_insulated = false + hpxml_bldg.floors.each do |floor| + next unless floor.interior_adjacent_to == HPXML::LocationConditionedSpace + next unless floor.exterior_adjacent_to == interior_adjacent_to + + if floor.insulation_assembly_r_value > 5 + foundation_ceiling_insulated = true + end end - else - # Building does not have HVAC system - indoor_temp = outdoor_temp - end - # Determine initial temperature - # For unconditioned spaces, this overrides EnergyPlus's built-in assumption of 22C (71.6F); - # see https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L257-L259 - # For conditioned spaces, this avoids an E+ 22.2 bug; see https://github.com/NREL/EnergyPlus/issues/9692 - if HPXML::conditioned_locations.include? slab.interior_adjacent_to - initial_temp = indoor_temp - else - # Space temperature assumptions from ASHRAE 152 - Duct Efficiency Calculations.xls, Zone temperatures - ground_temp = weather.data.ShallowGroundMonthlyTemps[sim_begin_month - 1] - if slab.interior_adjacent_to == HPXML::LocationBasementUnconditioned - if foundation_ceiling_insulated - # Insulated ceiling: 75% ground, 25% outdoor, 0% indoor - ground_weight, outdoor_weight, indoor_weight = 0.75, 0.25, 0.0 - elsif foundation_walls_insulated - # Insulated walls: 50% ground, 0% outdoor, 50% indoor (case not in ASHRAE 152) - ground_weight, outdoor_weight, indoor_weight = 0.5, 0.0, 0.5 + # Approximate indoor temperature + if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized + # Building has HVAC system + setpoint_sch = conditioned_zone.thermostatSetpointDualSetpoint.get + sim_begin_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(sim_begin_month), sim_begin_day, sim_year) + sim_begin_hour = (Calendar.get_day_num_from_month_day(sim_year, sim_begin_month, sim_begin_day) - 1) * 24 + + # Get heating/cooling setpoints for the simulation start + htg_setpoint_sch = setpoint_sch.heatingSetpointTemperatureSchedule.get + if htg_setpoint_sch.to_ScheduleRuleset.is_initialized + htg_day_sch = htg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] + heat_setpoint = UnitConversions.convert(htg_day_sch.values[0], 'C', 'F') else - # Uninsulated: 50% ground, 20% outdoor, 30% indoor - ground_weight, outdoor_weight, indoor_weight = 0.5, 0.2, 0.3 + heat_setpoint = schedules_file.schedules[SchedulesFile::Columns[:HeatingSetpoint].name][sim_begin_hour] end - initial_temp = outdoor_temp * outdoor_weight + ground_temp * ground_weight + indoor_weight * indoor_temp - elsif slab.interior_adjacent_to == HPXML::LocationCrawlspaceVented - if foundation_ceiling_insulated - # Insulated ceiling: 90% outdoor, 10% indoor - outdoor_weight, indoor_weight = 0.9, 0.1 - elsif foundation_walls_insulated - # Insulated walls: 25% outdoor, 75% indoor (case not in ASHRAE 152) - outdoor_weight, indoor_weight = 0.25, 0.75 + clg_setpoint_sch = setpoint_sch.coolingSetpointTemperatureSchedule.get + if clg_setpoint_sch.to_ScheduleRuleset.is_initialized + clg_day_sch = clg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] + cool_setpoint = UnitConversions.convert(clg_day_sch.values[0], 'C', 'F') else - # Uninsulated: 50% outdoor, 50% indoor - outdoor_weight, indoor_weight = 0.5, 0.5 + cool_setpoint = schedules_file.schedules[SchedulesFile::Columns[:CoolingSetpoint].name][sim_begin_hour] end - initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp - elsif slab.interior_adjacent_to == HPXML::LocationCrawlspaceUnvented - if foundation_ceiling_insulated - # Insulated ceiling: 85% outdoor, 15% indoor - outdoor_weight, indoor_weight = 0.85, 0.15 - elsif foundation_walls_insulated - # Insulated walls: 25% outdoor, 75% indoor - outdoor_weight, indoor_weight = 0.25, 0.75 + + # Methodology adapted from https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L303-L313 + heat_balance_temp = UnitConversions.convert(10.0, 'C', 'F') + cool_balance_temp = UnitConversions.convert(15.0, 'C', 'F') + if outdoor_temp < heat_balance_temp + indoor_temp = heat_setpoint + elsif outdoor_temp > cool_balance_temp + indoor_temp = cool_setpoint + elsif cool_balance_temp == heat_balance_temp + indoor_temp = heat_balance_temp else - # Uninsulated: 40% outdoor, 60% indoor - outdoor_weight, indoor_weight = 0.4, 0.6 + weight = (cool_balance_temp - outdoor_temp) / (cool_balance_temp - heat_balance_temp) + indoor_temp = heat_setpoint * weight + cool_setpoint * (1.0 - weight) end - initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp - elsif slab.interior_adjacent_to == HPXML::LocationGarage - initial_temp = outdoor_temp + 11.0 else - fail "Unhandled space: #{slab.interior_adjacent_to}" + # Building does not have HVAC system + indoor_temp = outdoor_temp + end + + # Determine initial temperature + # For unconditioned spaces, this overrides EnergyPlus's built-in assumption of 22C (71.6F); + # see https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L257-L259 + # For conditioned spaces, this avoids an E+ 22.2 bug; see https://github.com/NREL/EnergyPlus/issues/9692 + if HPXML::conditioned_locations.include? interior_adjacent_to + initial_temp = indoor_temp + else + # Space temperature assumptions from ASHRAE 152 - Duct Efficiency Calculations.xls, Zone temperatures + ground_temp = weather.data.ShallowGroundMonthlyTemps[sim_begin_month - 1] + if interior_adjacent_to == HPXML::LocationBasementUnconditioned + if foundation_ceiling_insulated + # Insulated ceiling: 75% ground, 25% outdoor, 0% indoor + ground_weight, outdoor_weight, indoor_weight = 0.75, 0.25, 0.0 + elsif foundation_walls_insulated + # Insulated walls: 50% ground, 0% outdoor, 50% indoor (case not in ASHRAE 152) + ground_weight, outdoor_weight, indoor_weight = 0.5, 0.0, 0.5 + else + # Uninsulated: 50% ground, 20% outdoor, 30% indoor + ground_weight, outdoor_weight, indoor_weight = 0.5, 0.2, 0.3 + end + initial_temp = outdoor_temp * outdoor_weight + ground_temp * ground_weight + indoor_weight * indoor_temp + elsif interior_adjacent_to == HPXML::LocationCrawlspaceVented + if foundation_ceiling_insulated + # Insulated ceiling: 90% outdoor, 10% indoor + outdoor_weight, indoor_weight = 0.9, 0.1 + elsif foundation_walls_insulated + # Insulated walls: 25% outdoor, 75% indoor (case not in ASHRAE 152) + outdoor_weight, indoor_weight = 0.25, 0.75 + else + # Uninsulated: 50% outdoor, 50% indoor + outdoor_weight, indoor_weight = 0.5, 0.5 + end + initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp + elsif interior_adjacent_to == HPXML::LocationCrawlspaceUnvented + if foundation_ceiling_insulated + # Insulated ceiling: 85% outdoor, 15% indoor + outdoor_weight, indoor_weight = 0.85, 0.15 + elsif foundation_walls_insulated + # Insulated walls: 25% outdoor, 75% indoor + outdoor_weight, indoor_weight = 0.25, 0.75 + else + # Uninsulated: 40% outdoor, 60% indoor + outdoor_weight, indoor_weight = 0.4, 0.6 + end + initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp + elsif interior_adjacent_to == HPXML::LocationGarage + initial_temp = outdoor_temp + 11.0 + else + fail "Unhandled space: #{interior_adjacent_to}" + end end - end - foundation.setInitialIndoorAirTemperature(UnitConversions.convert(initial_temp, 'F', 'C')) + foundation.setInitialIndoorAirTemperature(UnitConversions.convert(initial_temp, 'F', 'C')) + end end # TODO diff --git a/HPXMLtoOpenStudio/resources/generator.rb b/HPXMLtoOpenStudio/resources/generator.rb index 82a7a87a83..16b1a65290 100644 --- a/HPXMLtoOpenStudio/resources/generator.rb +++ b/HPXMLtoOpenStudio/resources/generator.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -# Collection of methods for adding generator-related OpenStudio objects. +# Collection of methods related to generators. module Generator # Adds any HPXML Generators to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @return [nil] - def self.apply_generators(model, hpxml_bldg) + def self.apply(model, hpxml_bldg) hpxml_bldg.generators.each do |generator| apply_generator(model, hpxml_bldg, generator) end diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index 0db151fb25..3701b92e3a 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Collection of methods to get, add, assign, create, etc. geometry-related OpenStudio objects. +# Collection of methods related to geometry. module Geometry # Adds any HPXML Roofs to the OpenStudio model. # @@ -385,13 +385,10 @@ def self.apply_floors(runner, model, spaces, hpxml_bldg, hpxml_header) # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_foundation_walls_slabs(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + def self.apply_foundation_walls_slabs(runner, model, spaces, hpxml_bldg) default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) foundation_types = hpxml_bldg.slabs.map { |s| s.interior_adjacent_to }.uniq @@ -411,7 +408,7 @@ def self.apply_foundation_walls_slabs(runner, model, weather, spaces, hpxml_bldg if ext_fnd_walls.empty? # Slab w/o foundation walls - apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, -1 * slab.depth_below_grade.to_f, slab.exposed_perimeter, nil, schedules_file, default_azimuths) + apply_foundation_slab(model, spaces, hpxml_bldg, slab, -1 * slab.depth_below_grade.to_f, slab.exposed_perimeter, nil, default_azimuths) else # Slab w/ foundation walls ext_fnd_walls_length = ext_fnd_walls.map { |fw| fw.area / fw.height }.sum @@ -429,14 +426,14 @@ def self.apply_foundation_walls_slabs(runner, model, weather, spaces, hpxml_bldg remaining_exposed_length -= exposed_length kiva_foundation = apply_foundation_wall(runner, model, spaces, hpxml_bldg, fnd_wall, exposed_length, fnd_wall_length, default_azimuths) - apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, -1 * fnd_wall.depth_below_grade, exposed_length, kiva_foundation, schedules_file, default_azimuths) + apply_foundation_slab(model, spaces, hpxml_bldg, slab, -1 * fnd_wall.depth_below_grade, exposed_length, kiva_foundation, default_azimuths) end if remaining_exposed_length > 1 # Skip if a small length (e.g., due to rounding) # The slab's exposed perimeter exceeds the sum of attached exterior foundation wall lengths. # This may legitimately occur for a walkout basement, where a portion of the slab has no # adjacent foundation wall. - apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, 0, remaining_exposed_length, nil, schedules_file, default_azimuths) + apply_foundation_slab(model, spaces, hpxml_bldg, slab, 0, remaining_exposed_length, nil, default_azimuths) end end end @@ -592,18 +589,15 @@ def self.apply_foundation_wall(runner, model, spaces, hpxml_bldg, foundation_wal # Adds an HPXML Slab to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param slab [HPXML::Slab] HPXML Slab object # @param z_origin [Double] The z-coordinate for which the slab is relative (ft) # @param exposed_length [Double] TODO # @param kiva_foundation [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object - # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param default_azimuths [TODO] TODO # @return [nil] - def self.apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, z_origin, exposed_length, kiva_foundation, schedules_file, default_azimuths) + def self.apply_foundation_slab(model, spaces, hpxml_bldg, slab, z_origin, exposed_length, kiva_foundation, default_azimuths) exposed_fraction = exposed_length / slab.exposed_perimeter slab_tot_perim = exposed_length slab_area = slab.area * exposed_fraction @@ -662,36 +656,8 @@ def self.apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, Constructions.apply_foundation_slab(model, surface, "#{slab.id} construction", slab_under_r, slab_under_width, slab_gap_r, slab_perim_r, slab_perim_depth, slab_whole_r, slab.thickness, - exposed_length, mat_carpet, soil_k_in, kiva_foundation, ext_horiz_r, ext_horiz_width, ext_horiz_depth) - - kiva_foundation = surface.adjacentFoundation.get - - foundation_walls_insulated = false - foundation_ceiling_insulated = false - hpxml_bldg.foundation_walls.each do |fnd_wall| - next unless fnd_wall.interior_adjacent_to == slab.interior_adjacent_to - next unless fnd_wall.exterior_adjacent_to == HPXML::LocationGround - - if fnd_wall.insulation_assembly_r_value.to_f > 5 - foundation_walls_insulated = true - elsif fnd_wall.insulation_exterior_r_value.to_f + fnd_wall.insulation_interior_r_value.to_f > 0 - foundation_walls_insulated = true - end - end - hpxml_bldg.floors.each do |floor| - next unless floor.interior_adjacent_to == HPXML::LocationConditionedSpace - next unless floor.exterior_adjacent_to == slab.interior_adjacent_to - - if floor.insulation_assembly_r_value > 5 - foundation_ceiling_insulated = true - end - end - - Constructions.apply_kiva_initial_temp(kiva_foundation, slab, weather, - spaces[HPXML::LocationConditionedSpace].thermalZone.get, - hpxml_header.sim_begin_month, hpxml_header.sim_begin_day, - hpxml_header.sim_calendar_year, schedules_file, - foundation_walls_insulated, foundation_ceiling_insulated) + exposed_length, mat_carpet, soil_k_in, kiva_foundation, + ext_horiz_r, ext_horiz_width, ext_horiz_depth) return kiva_foundation end @@ -1132,8 +1098,8 @@ def self.get_occupancy_default_num(nbeds:) return Float(nbeds) # Per ANSI 301 for an asset calculation end - # Create space and zone based on contents of spaces and value of location. - # Set a "dwelling unit multiplier" equal to the number of similar units represented. + # Creates a space and zone based on contents of spaces and value of location. + # Sets a "dwelling unit multiplier" equal to the number of similar units represented. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects diff --git a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb index af3db0a867..c8e6e496c9 100644 --- a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb +++ b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb @@ -1,27 +1,19 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to hot water use and appliances. module HotWaterAndAppliances # TODO # - # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects - # @param hot_water_distribution [TODO] TODO - # @param solar_thermal_system [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param plantloop_map [TODO] TODO - # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units # @return [TODO] TODO - def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_water_distribution, - solar_thermal_system, eri_version, schedules_file, plantloop_map, - unavailable_periods, unit_multiplier) - + def self.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, plantloop_map) @runner = runner cfa = hpxml_bldg.building_construction.conditioned_floor_area ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors @@ -32,6 +24,8 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat nbeds = hpxml_bldg.building_construction.number_of_bedrooms nbeds_eq = hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms n_occ = hpxml_bldg.building_occupancy.number_of_residents + eri_version = hpxml_header.eri_calculation_version + unit_multiplier = hpxml_bldg.building_construction.number_of_units # Get appliances, etc. if not hpxml_bldg.clothes_washers.empty? @@ -74,7 +68,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat cw_power_schedule = schedules_file.create_schedule_file(model, col_name: cw_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if cw_power_schedule.nil? - cw_unavailable_periods = Schedule.get_unavailable_periods(runner, cw_col_name, unavailable_periods) + cw_unavailable_periods = Schedule.get_unavailable_periods(runner, cw_col_name, hpxml_header.unavailable_periods) cw_weekday_sch = clothes_washer.weekday_fractions cw_weekend_sch = clothes_washer.weekend_fractions cw_monthly_sch = clothes_washer.monthly_multipliers @@ -106,7 +100,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat cd_schedule = schedules_file.create_schedule_file(model, col_name: cd_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if cd_schedule.nil? - cd_unavailable_periods = Schedule.get_unavailable_periods(runner, cd_col_name, unavailable_periods) + cd_unavailable_periods = Schedule.get_unavailable_periods(runner, cd_col_name, hpxml_header.unavailable_periods) cd_weekday_sch = clothes_dryer.weekday_fractions cd_weekend_sch = clothes_dryer.weekend_fractions cd_monthly_sch = clothes_dryer.monthly_multipliers @@ -139,7 +133,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat dw_power_schedule = schedules_file.create_schedule_file(model, col_name: dw_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if dw_power_schedule.nil? - dw_unavailable_periods = Schedule.get_unavailable_periods(runner, dw_col_name, unavailable_periods) + dw_unavailable_periods = Schedule.get_unavailable_periods(runner, dw_col_name, hpxml_header.unavailable_periods) dw_weekday_sch = dishwasher.weekday_fractions dw_weekend_sch = dishwasher.weekend_fractions dw_monthly_sch = dishwasher.monthly_multipliers @@ -170,7 +164,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat fridge_schedule = schedules_file.create_schedule_file(model, col_name: fridge_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if fridge_schedule.nil? - fridge_unavailable_periods = Schedule.get_unavailable_periods(runner, fridge_col_name, unavailable_periods) + fridge_unavailable_periods = Schedule.get_unavailable_periods(runner, fridge_col_name, hpxml_header.unavailable_periods) # if both weekday_fractions/weekend_fractions/monthly_multipliers and constant_coefficients/temperature_coefficients provided, ignore the former if !refrigerator.constant_coefficients.nil? && !refrigerator.temperature_coefficients.nil? @@ -212,7 +206,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat freezer_schedule = schedules_file.create_schedule_file(model, col_name: freezer_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if freezer_schedule.nil? - freezer_unavailable_periods = Schedule.get_unavailable_periods(runner, freezer_col_name, unavailable_periods) + freezer_unavailable_periods = Schedule.get_unavailable_periods(runner, freezer_col_name, hpxml_header.unavailable_periods) # if both weekday_fractions/weekend_fractions/monthly_multipliers and constant_coefficients/temperature_coefficients provided, ignore the former if !freezer.constant_coefficients.nil? && !freezer.temperature_coefficients.nil? @@ -255,7 +249,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat cook_schedule = schedules_file.create_schedule_file(model, col_name: cook_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if cook_schedule.nil? - cook_unavailable_periods = Schedule.get_unavailable_periods(runner, cook_col_name, unavailable_periods) + cook_unavailable_periods = Schedule.get_unavailable_periods(runner, cook_col_name, hpxml_header.unavailable_periods) cook_weekday_sch = cooking_range.weekday_fractions cook_weekend_sch = cooking_range.weekend_fractions cook_monthly_sch = cooking_range.monthly_multipliers @@ -274,6 +268,9 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat add_other_equipment(model, cook_obj_name, cook_space, cook_design_level_f, cook_frac_sens, cook_frac_lat, cook_schedule, cooking_range.fuel_type) end + if hpxml_bldg.hot_water_distributions.size > 0 + hot_water_distribution = hpxml_bldg.hot_water_distributions[0] + end if not hot_water_distribution.nil? fixtures = hpxml_bldg.water_fixtures.select { |wf| [HPXML::WaterFixtureTypeShowerhead, HPXML::WaterFixtureTypeFaucet].include? wf.water_fixture_type } if fixtures.size > 0 @@ -334,7 +331,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat fixtures_schedule = schedules_file.create_schedule_file(model, col_name: fixtures_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if fixtures_schedule.nil? - fixtures_unavailable_periods = Schedule.get_unavailable_periods(runner, fixtures_col_name, unavailable_periods) + fixtures_unavailable_periods = Schedule.get_unavailable_periods(runner, fixtures_col_name, hpxml_header.unavailable_periods) fixtures_weekday_sch = hpxml_bldg.water_heating.water_fixtures_weekday_fractions fixtures_weekend_sch = hpxml_bldg.water_heating.water_fixtures_weekend_fractions fixtures_monthly_sch = hpxml_bldg.water_heating.water_fixtures_monthly_multipliers @@ -348,7 +345,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat end hpxml_bldg.water_heating_systems.each do |water_heating_system| - non_solar_fraction = 1.0 - Waterheater.get_water_heater_solar_fraction(water_heating_system, solar_thermal_system) + non_solar_fraction = 1.0 - Waterheater.get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) gpd_frac = water_heating_system.fraction_dhw_load_served # Fixtures fraction if gpd_frac > 0 @@ -386,7 +383,7 @@ def self.apply(model, runner, hpxml_header, hpxml_bldg, weather, spaces, hot_wat recirc_pump_sch = schedules_file.create_schedule_file(model, col_name: recirc_pump_col_name, schedule_type_limits_name: EPlus::ScheduleTypeLimitsFraction) end if recirc_pump_sch.nil? - recirc_pump_unavailable_periods = Schedule.get_unavailable_periods(runner, recirc_pump_col_name, unavailable_periods) + recirc_pump_unavailable_periods = Schedule.get_unavailable_periods(runner, recirc_pump_col_name, hpxml_header.unavailable_periods) recirc_pump_weekday_sch = hot_water_distribution.recirculation_pump_weekday_fractions recirc_pump_weekend_sch = hot_water_distribution.recirculation_pump_weekend_fractions recirc_pump_monthly_sch = hot_water_distribution.recirculation_pump_monthly_multipliers @@ -935,42 +932,6 @@ def self.fridge_or_freezer_coefficients_schedule(model, col_name, obj_name, frid return schedule end - # TODO - # - # @param has_uncond_bsmnt [TODO] TODO - # @param has_cond_bsmnt [TODO] TODO - # @param cfa [Double] Conditioned floor area in the dwelling unit (ft2) - # @param ncfl [Double] Total number of conditioned floors in the dwelling unit - # @param water_heating_system [TODO] TODO - # @param hot_water_distribution [TODO] TODO - # @return [TODO] TODO - def self.get_dist_energy_consumption_adjustment(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl, - water_heating_system, hot_water_distribution) - - if water_heating_system.fraction_dhw_load_served <= 0 - # No fixtures; not accounting for distribution system - return 1.0 - end - - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems - # Eq. 4.2-16 - ew_fact = get_dist_energy_waste_factor(hot_water_distribution) - o_frac = 0.25 # fraction of hot water waste from standard operating conditions - oew_fact = ew_fact * o_frac # standard operating condition portion of hot water energy waste - ocd_eff = 0.0 - sew_fact = ew_fact - oew_fact - ref_pipe_l = get_default_std_pipe_length(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl) - if hot_water_distribution.system_type == HPXML::DHWDistTypeStandard - pe_ratio = hot_water_distribution.standard_piping_length / ref_pipe_l - elsif hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc - ref_loop_l = get_default_recirc_loop_length(ref_pipe_l) - pe_ratio = hot_water_distribution.recirculation_piping_loop_length / ref_loop_l - end - e_waste = oew_fact * (1.0 - ocd_eff) + sew_fact * pe_ratio - return (e_waste + 128.0) / 160.0 - end - # TODO # # @param has_uncond_bsmnt [TODO] TODO @@ -1344,51 +1305,6 @@ def self.get_dist_waste_gpd(eri_version, nbeds, has_uncond_bsmnt, has_cond_bsmnt return mw_gpd * fixtures_usage_multiplier end - # TODO - # - # @param hot_water_distribution [TODO] TODO - # @return [TODO] TODO - def self.get_dist_energy_waste_factor(hot_water_distribution) - # ANSI/RESNET 301-2014 Addendum A-2015 - # Amendment on Domestic Hot Water (DHW) Systems - # Table 4.2.2.5.2.11(6) Hot water distribution system relative annual energy waste factors - if hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc - if (hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeNone) || - (hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeTimer) - if hot_water_distribution.pipe_r_value < 3.0 - return 500.0 - else - return 250.0 - end - elsif hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeTemperature - if hot_water_distribution.pipe_r_value < 3.0 - return 375.0 - else - return 187.5 - end - elsif hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeSensor - if hot_water_distribution.pipe_r_value < 3.0 - return 64.8 - else - return 43.2 - end - elsif hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeManual - if hot_water_distribution.pipe_r_value < 3.0 - return 43.2 - else - return 28.8 - end - end - elsif hot_water_distribution.system_type == HPXML::DHWDistTypeStandard - if hot_water_distribution.pipe_r_value < 3.0 - return 32.0 - else - return 28.8 - end - end - fail 'Unexpected hot water distribution system.' - end - # TODO # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit diff --git a/HPXMLtoOpenStudio/resources/hpxml_defaults.rb b/HPXMLtoOpenStudio/resources/hpxml_defaults.rb index 2738b27971..8620343aeb 100644 --- a/HPXMLtoOpenStudio/resources/hpxml_defaults.rb +++ b/HPXMLtoOpenStudio/resources/hpxml_defaults.rb @@ -34,7 +34,14 @@ def self.apply(runner, hpxml, hpxml_bldg, weather, schedules_file: nil, convert_ nbeds = hpxml_bldg.building_construction.number_of_bedrooms ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors ncfl_ag = hpxml_bldg.building_construction.number_of_conditioned_floors_above_grade + eri_version = hpxml.header.eri_calculation_version + if eri_version.nil? + eri_version = 'latest' + end + if eri_version == 'latest' + eri_version = Constants::ERIVersions[-1] + end if hpxml.buildings.size > 1 # This is helpful if we need to make unique HPXML IDs across dwelling units diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index c3e09607f2..5a2f2bbaec 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to HVAC systems. module HVAC AirSourceHeatRatedODB = 47.0 # degF, Rated outdoor drybulb for air-source systems, heating AirSourceHeatRatedIDB = 70.0 # degF, Rated indoor drybulb for air-source systems, heating @@ -9,23 +9,26 @@ module HVAC CrankcaseHeaterTemp = 50.0 # degF # TODO - # @param model [OpenStudio::Model::Model] OpenStudio Model object + # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # # @return [Array] Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil), TODO - def self.apply_hvac_systems(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) - return if hpxml_bldg.hvac_controls.size == 0 - + def self.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) # Init @remaining_heat_load_frac = 1.0 @remaining_cool_load_frac = 1.0 @hp_backup_system_object = nil @hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, hpxml_header.unavailable_periods) + airloop_map = {} + + if hpxml_bldg.hvac_controls.size == 0 + return airloop_map, @hvac_unavailable_periods + end # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling # season begin/end month/day, respectively. @@ -44,8 +47,8 @@ def self.apply_hvac_systems(model, runner, weather, spaces, hpxml_bldg, hpxml_he @heating_days = Calendar.get_daily_season(hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) @cooling_days = Calendar.get_daily_season(hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) - airloop_map = {} - + apply_unit_multiplier(hpxml_bldg, hpxml_header) + ensure_nonzero_sizing_values(hpxml_bldg) apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header) apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) @@ -1394,6 +1397,9 @@ def self.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_heade thermostat_setpoint.setCoolingSetpointTemperatureSchedule(cooling_sch) thermostat_setpoint.setTemperatureDifferenceBetweenCutoutAndSetpoint(UnitConversions.convert(onoff_thermostat_ddb, 'deltaF', 'deltaC')) conditioned_zone.setThermostatSetpointDualSetpoint(thermostat_setpoint) + + # Now that we have assigned the HVAC setpoint, set Kiva foundation initial temperatures. + Constructions.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_header, conditioned_zone, schedules_file) end # TODO @@ -5500,7 +5506,7 @@ def self.get_hpxml_hvac_systems(hpxml_bldg) return hvac_systems end - # TODO + # Ensure that no capacities/airflows are zero in order to prevent potential E+ errors. # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @return [TODO] TODO @@ -5548,15 +5554,14 @@ def self.ensure_nonzero_sizing_values(hpxml_bldg) end end - # TODO + # Apply unit multiplier (E+ thermal zone multiplier) to HVAC systems; E+ sends the + # multiplied thermal zone load to the HVAC system, so the HVAC system needs to be + # sized to meet the entire multiplied zone load. # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @return [TODO] TODO def self.apply_unit_multiplier(hpxml_bldg, hpxml_header) - # Apply unit multiplier (E+ thermal zone multiplier); E+ sends the - # multiplied thermal zone load to the HVAC system, so the HVAC system - # needs to be sized to meet the entire multiplied zone load. unit_multiplier = hpxml_bldg.building_construction.number_of_units hpxml_bldg.heating_systems.each do |htg_sys| htg_sys.heating_capacity *= unit_multiplier diff --git a/HPXMLtoOpenStudio/resources/internal_gains.rb b/HPXMLtoOpenStudio/resources/internal_gains.rb index 6167a584dc..7185fc6230 100644 --- a/HPXMLtoOpenStudio/resources/internal_gains.rb +++ b/HPXMLtoOpenStudio/resources/internal_gains.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to internal gains. module InternalGains # Create an OpenStudio People object using number of occupants and people/activity schedules. # - # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_building_occupants(model, runner, hpxml_bldg, hpxml_header, spaces, schedules_file) + def self.apply_building_occupants(runner, model, hpxml_bldg, hpxml_header, spaces, schedules_file) if hpxml_bldg.building_occupancy.number_of_residents.nil? # Asset calculation num_occ = Geometry.get_occupancy_default_num(nbeds: hpxml_bldg.building_construction.number_of_bedrooms) else # Operational calculation @@ -85,13 +85,14 @@ def self.get_occupancy_default_values() # Adds general water use internal gains (floor mopping, shower evaporation, water films # on showers, tubs & sinks surfaces, plant watering, etc.) to the OpenStudio Model. # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_general_water_use(model, runner, hpxml_bldg, hpxml_header, spaces, schedules_file) + def self.apply_general_water_use(runner, model, hpxml_bldg, hpxml_header, spaces, schedules_file) general_water_use_usage_multiplier = hpxml_bldg.building_occupancy.general_water_use_usage_multiplier nbeds_eq = hpxml_bldg.building_construction.additional_properties.equivalent_number_of_bedrooms diff --git a/HPXMLtoOpenStudio/resources/lighting.rb b/HPXMLtoOpenStudio/resources/lighting.rb index ae04cec720..0213526200 100644 --- a/HPXMLtoOpenStudio/resources/lighting.rb +++ b/HPXMLtoOpenStudio/resources/lighting.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to lighting. module Lighting # Adds any HPXML Lighting Groups and Lighting to the OpenStudio model. # diff --git a/HPXMLtoOpenStudio/resources/misc_loads.rb b/HPXMLtoOpenStudio/resources/misc_loads.rb index bd7d2466e0..cda083fdb8 100644 --- a/HPXMLtoOpenStudio/resources/misc_loads.rb +++ b/HPXMLtoOpenStudio/resources/misc_loads.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to miscellaneous plug/fuel loads. module MiscLoads # Adds any HPXML Plug Loads to the OpenStudio model. # diff --git a/HPXMLtoOpenStudio/resources/pv.rb b/HPXMLtoOpenStudio/resources/pv.rb index dc7a632564..f25ed070af 100644 --- a/HPXMLtoOpenStudio/resources/pv.rb +++ b/HPXMLtoOpenStudio/resources/pv.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -# Collection of methods for adding photovoltaic-related OpenStudio objects. +# Collection of methods related to Photovoltaic systems. module PV # Adds any HPXML Photovoltaics to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @return [nil] - def self.apply_pv_systems(model, hpxml_bldg) + def self.apply(model, hpxml_bldg) # Error-checking hpxml_bldg.pv_systems.each do |pv_system| next if pv_system.inverter.inverter_efficiency == hpxml_bldg.pv_systems[0].inverter.inverter_efficiency diff --git a/HPXMLtoOpenStudio/resources/waterheater.rb b/HPXMLtoOpenStudio/resources/waterheater.rb index 937706ad63..164474fb71 100644 --- a/HPXMLtoOpenStudio/resources/waterheater.rb +++ b/HPXMLtoOpenStudio/resources/waterheater.rb @@ -1,29 +1,64 @@ # frozen_string_literal: true -# TODO +# Collection of methods related to water heating systems. module Waterheater + # TODO + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @return [nil] + def self.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:WaterHeater].name, hpxml_header.unavailable_periods) + + plantloop_map = {} + hpxml_bldg.water_heating_systems.each do |dhw_system| + if dhw_system.water_heater_type == HPXML::WaterHeaterTypeStorage + apply_tank(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) + elsif dhw_system.water_heater_type == HPXML::WaterHeaterTypeTankless + apply_tankless(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) + elsif dhw_system.water_heater_type == HPXML::WaterHeaterTypeHeatPump + apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) + elsif [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? dhw_system.water_heater_type + apply_combi(model, runner, spaces, hpxml_bldg, hpxml_header, dhw_system, schedules_file, unavailable_periods, plantloop_map) + else + fail "Unhandled water heater (#{dhw_system.water_heater_type})." + end + end + + HotWaterAndAppliances.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, plantloop_map) + + apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) + + # Add combi-system EMS program with water use equipment information + apply_combi_system_EMS(model, hpxml_bldg.water_heating_systems, plantloop_map) + end + # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param loc_space [TODO] TODO - # @param loc_schedule [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param water_heating_system [TODO] TODO - # @param ec_adj [TODO] TODO - # @param solar_thermal_system [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units - # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.apply_tank(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) - solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system) + # @param plantloop_map [Hash] Map of HPXML System ID => OpenStudio PlantLoop objects + # @return [nil] + def self.apply_tank(model, runner, spaces, hpxml_bldg, hpxml_header, water_heating_system, schedules_file, unavailable_periods, plantloop_map) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) + unit_multiplier = hpxml_bldg.building_construction.number_of_units + solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier) + loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) act_vol = calc_storage_tank_actual_vol(water_heating_system.tank_volume, water_heating_system.fuel_type) - u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, nbeds) + u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) new_heater = create_new_heater(name: Constants::ObjectTypeWaterHeater, water_heating_system: water_heating_system, act_vol: act_vol, @@ -40,35 +75,34 @@ def self.apply_tank(model, runner, loc_space, loc_schedule, water_heating_system unit_multiplier: unit_multiplier) loop.addSupplyBranchForComponent(new_heater) - add_ec_adj(model, new_heater, ec_adj, loc_space, water_heating_system, unit_multiplier) + add_ec_adj(model, hpxml_bldg, new_heater, loc_space, water_heating_system, unit_multiplier) add_desuperheater(model, runner, water_heating_system, new_heater, loc_space, loc_schedule, loop, unit_multiplier) - return loop + plantloop_map[water_heating_system.id] = loop end # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param loc_space [TODO] TODO - # @param loc_schedule [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param water_heating_system [TODO] TODO - # @param ec_adj [TODO] TODO - # @param solar_thermal_system [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units - # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.apply_tankless(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) + # @param plantloop_map [Hash] Map of HPXML System ID => OpenStudio PlantLoop objects + # @return [nil] + def self.apply_tankless(model, runner, spaces, hpxml_bldg, hpxml_header, water_heating_system, schedules_file, unavailable_periods, plantloop_map) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) + unit_multiplier = hpxml_bldg.building_construction.number_of_units water_heating_system.heating_capacity = 100000000000.0 * unit_multiplier - solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system) + solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier) + loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) act_vol = 1.0 * unit_multiplier - _u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, nbeds) + _u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) new_heater = create_new_heater(name: Constants::ObjectTypeWaterHeater, water_heating_system: water_heating_system, act_vol: act_vol, @@ -85,34 +119,32 @@ def self.apply_tankless(model, runner, loc_space, loc_schedule, water_heating_sy loop.addSupplyBranchForComponent(new_heater) - add_ec_adj(model, new_heater, ec_adj, loc_space, water_heating_system, unit_multiplier) + add_ec_adj(model, hpxml_bldg, new_heater, loc_space, water_heating_system, unit_multiplier) add_desuperheater(model, runner, water_heating_system, new_heater, loc_space, loc_schedule, loop, unit_multiplier) - return loop + plantloop_map[water_heating_system.id] = loop end # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param loc_space [TODO] TODO - # @param loc_schedule [TODO] TODO - # @param elevation [Double] Elevation of the building site (ft) + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param water_heating_system [TODO] TODO - # @param ec_adj [TODO] TODO - # @param solar_thermal_system [TODO] TODO - # @param conditioned_zone [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units - # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.apply_heatpump(model, runner, loc_space, loc_schedule, elevation, water_heating_system, ec_adj, solar_thermal_system, conditioned_zone, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) + # @param plantloop_map [Hash] Map of HPXML System ID => OpenStudio PlantLoop objects + # @return [nil] + def self.apply_heatpump(model, runner, spaces, hpxml_bldg, hpxml_header, water_heating_system, schedules_file, unavailable_periods, plantloop_map) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) + unit_multiplier = hpxml_bldg.building_construction.number_of_units obj_name_hpwh = Constants::ObjectTypeWaterHeater - solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system) + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier) + loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) h_tank = 0.0188 * water_heating_system.tank_volume + 0.0935 # Linear relationship that gets GE height at 50 gal and AO Smith height at 80 gal @@ -158,10 +190,10 @@ def self.apply_heatpump(model, runner, loc_space, loc_schedule, elevation, water max_temp = 120.0 # F # Coil:WaterHeating:AirToWaterHeatPump:Wrapped - coil = setup_hpwh_dxcoil(model, runner, water_heating_system, elevation, obj_name_hpwh, airflow_rate, unit_multiplier) + coil = setup_hpwh_dxcoil(model, runner, water_heating_system, hpxml_bldg.elevation, obj_name_hpwh, airflow_rate, unit_multiplier) # WaterHeater:Stratified - tank = setup_hpwh_stratified_tank(model, water_heating_system, obj_name_hpwh, h_tank, solar_fraction, hpwh_tamb, bottom_element_setpoint_schedule, top_element_setpoint_schedule, unit_multiplier, nbeds) + tank = setup_hpwh_stratified_tank(model, water_heating_system, obj_name_hpwh, h_tank, solar_fraction, hpwh_tamb, bottom_element_setpoint_schedule, top_element_setpoint_schedule, unit_multiplier, hpxml_bldg.building_construction.number_of_bedrooms) loop.addSupplyBranchForComponent(tank) add_desuperheater(model, runner, water_heating_system, tank, loc_space, loc_schedule, loop, unit_multiplier) @@ -187,28 +219,27 @@ def self.apply_heatpump(model, runner, loc_space, loc_schedule, elevation, water program_calling_manager.addProgram(hpwh_ctrl_program) program_calling_manager.addProgram(hpwh_inlet_air_program) - add_ec_adj(model, hpwh, ec_adj, loc_space, water_heating_system, unit_multiplier) + add_ec_adj(model, hpxml_bldg, hpwh, loc_space, water_heating_system, unit_multiplier) - return loop + plantloop_map[water_heating_system.id] = loop end # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param loc_space [TODO] TODO - # @param loc_schedule [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param water_heating_system [TODO] TODO - # @param ec_adj [TODO] TODO - # @param solar_thermal_system [TODO] TODO - # @param eri_version [String] Version of the ANSI/RESNET/ICC 301 Standard to use for equations/assumptions # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies - # @param unit_multiplier [Integer] Number of similar dwelling units - # @param nbeds [Integer] Number of bedrooms in the dwelling unit - # @return [TODO] TODO - def self.apply_combi(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds) - solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system) + # @param plantloop_map [Hash] Map of HPXML System ID => OpenStudio PlantLoop objects + # @return [nil] + def self.apply_combi(model, runner, spaces, hpxml_bldg, hpxml_header, water_heating_system, schedules_file, unavailable_periods, plantloop_map) + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(water_heating_system.location, model, spaces) + unit_multiplier = hpxml_bldg.building_construction.number_of_units + solar_fraction = get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) boiler, boiler_plant_loop = get_combi_boiler_and_plant_loop(model, water_heating_system.related_hvac_idref) boiler.setName('combi boiler') @@ -224,14 +255,14 @@ def self.apply_combi(model, runner, loc_space, loc_schedule, water_heating_syste act_vol = calc_storage_tank_actual_vol(water_heating_system.tank_volume, nil) a_side = calc_tank_areas(act_vol)[1] - ua = calc_indirect_ua_with_standbyloss(act_vol, water_heating_system, a_side, solar_fraction, nbeds) + ua = calc_indirect_ua_with_standbyloss(act_vol, water_heating_system, a_side, solar_fraction, hpxml_bldg.building_construction.number_of_bedrooms) else ua = 0.0 act_vol = 1.0 end t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type) - loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier) + loop = create_new_loop(model, t_set_c, hpxml_header.eri_calculation_version, unit_multiplier) # Create water heater new_heater = create_new_heater(name: obj_name_combi, @@ -279,9 +310,91 @@ def self.apply_combi(model, runner, loc_space, loc_schedule, water_heating_syste loop.addSupplyBranchForComponent(new_heater) - add_ec_adj(model, new_heater, ec_adj, loc_space, water_heating_system, unit_multiplier, boiler) + add_ec_adj(model, hpxml_bldg, new_heater, loc_space, water_heating_system, unit_multiplier, boiler) - return loop + plantloop_map[water_heating_system.id] = loop + end + + # TODO + # + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param water_heating_system [TODO] TODO + # @return [TODO] TODO + def self.get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system) + if water_heating_system.fraction_dhw_load_served <= 0 + # No fixtures; not accounting for distribution system + return 1.0 + end + + hot_water_distribution = hpxml_bldg.hot_water_distributions[0] + + has_uncond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementUnconditioned) + has_cond_bsmnt = hpxml_bldg.has_location(HPXML::LocationBasementConditioned) + cfa = hpxml_bldg.building_construction.conditioned_floor_area + ncfl = hpxml_bldg.building_construction.number_of_conditioned_floors + + # ANSI/RESNET 301-2014 Addendum A-2015 + # Amendment on Domestic Hot Water (DHW) Systems + # Eq. 4.2-16 + ew_fact = get_dist_energy_waste_factor(hot_water_distribution) + o_frac = 0.25 # fraction of hot water waste from standard operating conditions + oew_fact = ew_fact * o_frac # standard operating condition portion of hot water energy waste + ocd_eff = 0.0 + sew_fact = ew_fact - oew_fact + ref_pipe_l = HotWaterAndAppliances.get_default_std_pipe_length(has_uncond_bsmnt, has_cond_bsmnt, cfa, ncfl) + if hot_water_distribution.system_type == HPXML::DHWDistTypeStandard + pe_ratio = hot_water_distribution.standard_piping_length / ref_pipe_l + elsif hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc + ref_loop_l = get_default_recirc_loop_length(ref_pipe_l) + pe_ratio = hot_water_distribution.recirculation_piping_loop_length / ref_loop_l + end + e_waste = oew_fact * (1.0 - ocd_eff) + sew_fact * pe_ratio + return (e_waste + 128.0) / 160.0 + end + + # TODO + # + # @param hot_water_distribution [TODO] TODO + # @return [TODO] TODO + def self.get_dist_energy_waste_factor(hot_water_distribution) + # ANSI/RESNET 301-2014 Addendum A-2015 + # Amendment on Domestic Hot Water (DHW) Systems + # Table 4.2.2.5.2.11(6) Hot water distribution system relative annual energy waste factors + if hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc + if (hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeNone) || + (hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeTimer) + if hot_water_distribution.pipe_r_value < 3.0 + return 500.0 + else + return 250.0 + end + elsif hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeTemperature + if hot_water_distribution.pipe_r_value < 3.0 + return 375.0 + else + return 187.5 + end + elsif hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeSensor + if hot_water_distribution.pipe_r_value < 3.0 + return 64.8 + else + return 43.2 + end + elsif hot_water_distribution.recirculation_control_type == HPXML::DHWRecircControlTypeManual + if hot_water_distribution.pipe_r_value < 3.0 + return 43.2 + else + return 28.8 + end + end + elsif hot_water_distribution.system_type == HPXML::DHWDistTypeStandard + if hot_water_distribution.pipe_r_value < 3.0 + return 32.0 + else + return 28.8 + end + end + fail 'Unexpected hot water distribution system.' end # TODO @@ -425,13 +538,16 @@ def self.apply_combi_system_EMS(model, water_heating_systems, plantloop_map) # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param loc_space [TODO] TODO - # @param loc_schedule [TODO] TODO - # @param solar_thermal_system [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param plantloop_map [TODO] TODO - # @param unit_multiplier [Integer] Number of similar dwelling units - # @return [TODO] TODO - def self.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_system, plantloop_map, unit_multiplier) + # @return [nil] + def self.apply_solar_thermal(model, spaces, hpxml_bldg, plantloop_map) + return if hpxml_bldg.solar_thermal_systems.size == 0 + + solar_thermal_system = hpxml_bldg.solar_thermal_systems[0] + return if solar_thermal_system.collector_area.nil? # Return if simple (not detailed) solar water heater type + if [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? solar_thermal_system.water_heating_system.water_heater_type fail "Water heating system '#{solar_thermal_system.water_heating_system.id}' connected to solar thermal system '#{solar_thermal_system.id}' cannot be a space-heating boiler." end @@ -439,7 +555,9 @@ def self.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_syste fail "Water heating system '#{solar_thermal_system.water_heating_system.id}' connected to solar thermal system '#{solar_thermal_system.id}' cannot be attached to a desuperheater." end + loc_space, loc_schedule = Geometry.get_space_or_schedule_from_location(solar_thermal_system.water_heating_system.location, model, spaces) dhw_loop = plantloop_map[solar_thermal_system.water_heating_system.id] + unit_multiplier = hpxml_bldg.building_construction.number_of_units obj_name = Constants::ObjectTypeSolarHotWater @@ -1584,13 +1702,15 @@ def self.get_default_num_bathrooms(num_beds) # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param heater [TODO] TODO # @param loc_space [TODO] TODO # @param water_heating_system [TODO] TODO # @param unit_multiplier [Integer] Number of similar dwelling units # @param combi_boiler [TODO] TODO # @return [TODO] TODO - def self.add_ec_adj(model, heater, ec_adj, loc_space, water_heating_system, unit_multiplier, combi_boiler = nil) + def self.add_ec_adj(model, hpxml_bldg, heater, loc_space, water_heating_system, unit_multiplier, combi_boiler = nil) + ec_adj = get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system) adjustment = ec_adj - 1.0 if loc_space.nil? # WH is not in a zone, set the other equipment to be in a random space @@ -2182,10 +2302,14 @@ def self.create_new_loop(model, t_set_c, eri_version, unit_multiplier) # TODO # # @param water_heating_system [TODO] TODO - # @param solar_thermal_system [TODO] TODO + # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @return [TODO] TODO - def self.get_water_heater_solar_fraction(water_heating_system, solar_thermal_system) - if (not solar_thermal_system.nil?) && (solar_thermal_system.water_heating_system.nil? || (solar_thermal_system.water_heating_system.id == water_heating_system.id)) + def self.get_water_heater_solar_fraction(water_heating_system, hpxml_bldg) + return 0.0 if hpxml_bldg.solar_thermal_systems.size == 0 + + solar_thermal_system = hpxml_bldg.solar_thermal_systems[0] + + if (solar_thermal_system.water_heating_system.nil? || (solar_thermal_system.water_heating_system.id == water_heating_system.id)) solar_fraction = solar_thermal_system.solar_fraction end return solar_fraction.to_f diff --git a/HPXMLtoOpenStudio/resources/xmlhelper.rb b/HPXMLtoOpenStudio/resources/xmlhelper.rb index 4c0b64620e..bcbf52284b 100644 --- a/HPXMLtoOpenStudio/resources/xmlhelper.rb +++ b/HPXMLtoOpenStudio/resources/xmlhelper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# TODO +# Collection of helper methods related to XML reading/writing. module XMLHelper # Adds the child element with 'element_name' and sets its value. Returns the # child element. diff --git a/HPXMLtoOpenStudio/tests/test_defaults.rb b/HPXMLtoOpenStudio/tests/test_defaults.rb index 7c1e48bc0c..66baa69ba9 100644 --- a/HPXMLtoOpenStudio/tests/test_defaults.rb +++ b/HPXMLtoOpenStudio/tests/test_defaults.rb @@ -706,7 +706,7 @@ def _get_base_building(retain_cond_bsmt: false) _test_default_infiltration_values(default_hpxml_bldg, 2000 * 8, false, 8.0 + (9.7 - 8.0) * 0.25) end - def test_infiltration_compartmentaliztion_test_adjustment + def test_infiltration_compartmentalization_test_adjustment # Test single-family detached hpxml, hpxml_bldg = _create_hpxml('base.xml') hpxml_bldg.air_infiltration_measurements[0].infiltration_type = HPXML::InfiltrationTypeUnitTotal From 07813e6a4e887d2927b969dbd3c9ac0c1d71f20a Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 16 Sep 2024 17:18:37 -0600 Subject: [PATCH 11/16] Bugfix. --- HPXMLtoOpenStudio/measure.xml | 6 +++--- HPXMLtoOpenStudio/resources/waterheater.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 742f95fe14..39a5b296da 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - a294ff2f-7ad6-4abf-aecf-f93f4927d01d - 2024-09-16T22:58:15Z + 38d2b85c-d365-4840-99e0-c31750872b0b + 2024-09-16T23:18:13Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -627,7 +627,7 @@ waterheater.rb rb resource - 274CCF96 + 51541A42 weather.rb diff --git a/HPXMLtoOpenStudio/resources/waterheater.rb b/HPXMLtoOpenStudio/resources/waterheater.rb index 164474fb71..a9c36cbd36 100644 --- a/HPXMLtoOpenStudio/resources/waterheater.rb +++ b/HPXMLtoOpenStudio/resources/waterheater.rb @@ -345,7 +345,7 @@ def self.get_dist_energy_consumption_adjustment(hpxml_bldg, water_heating_system if hot_water_distribution.system_type == HPXML::DHWDistTypeStandard pe_ratio = hot_water_distribution.standard_piping_length / ref_pipe_l elsif hot_water_distribution.system_type == HPXML::DHWDistTypeRecirc - ref_loop_l = get_default_recirc_loop_length(ref_pipe_l) + ref_loop_l = HotWaterAndAppliances.get_default_recirc_loop_length(ref_pipe_l) pe_ratio = hot_water_distribution.recirculation_piping_loop_length / ref_loop_l end e_waste = oew_fact * (1.0 - ocd_eff) + sew_fact * pe_ratio From d4e0b79987761fd64ba7d8b877cc0f2a4c1247c7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 17 Sep 2024 00:07:50 +0000 Subject: [PATCH 12/16] Latest results. --- workflow/tests/base_results/results_simulations_bills.csv | 8 ++++---- .../tests/base_results/results_simulations_energy.csv | 8 ++++---- workflow/tests/base_results/results_simulations_loads.csv | 6 +++--- workflow/tests/base_results/results_simulations_misc.csv | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/workflow/tests/base_results/results_simulations_bills.csv b/workflow/tests/base_results/results_simulations_bills.csv index a055b949c6..6bb033b2ec 100644 --- a/workflow/tests/base_results/results_simulations_bills.csv +++ b/workflow/tests/base_results/results_simulations_bills.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,1842.17,144.0,1298.15,0.0,1442.15,144.0,256.02,400.02, base-battery-scheduled-power-outage.xml,1771.64,144.0,1234.11,0.0,1378.11,144.0,249.53,393.53,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-battery-scheduled.xml,1903.62,144.0,1366.31,0.0,1510.31,144.0,249.31,393.31,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-battery.xml,1840.48,144.0,1303.17,0.0,1447.17,144.0,249.31,393.31,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,1303.15,144.0,879.44,0.0,1023.44,144.0,135.71,279.71,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,1276.91,144.0,869.71,0.0,1013.71,144.0,119.2,263.2,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,962.69,144.0,818.69,0.0,962.69,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -base-bldgtype-mf-unit-adjacent-to-multiple.xml,1276.77,144.0,911.65,0.0,1055.65,144.0,77.12,221.12,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +base-bldgtype-mf-unit-adjacent-to-multiple.xml,1268.26,144.0,904.84,0.0,1048.84,144.0,75.42,219.42,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,1436.25,144.0,871.62,0.0,1015.62,144.0,276.63,420.63,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1191.78,144.0,884.21,0.0,1028.21,144.0,19.57,163.57,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1182.34,144.0,874.77,0.0,1018.77,144.0,19.57,163.57,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,1212.56,144.0,907.58,0.0,1051.58,144.0,16.98,160.98,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-infil-compartmentalization-test.xml,1241.99,144.0,945.94,0.0,1089.94,144.0,8.05,152.05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-infil-leakiness-description.xml,1244.17,144.0,950.99,0.0,1094.99,144.0,5.18,149.18,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -306,7 +306,7 @@ base-hvac-mini-split-heat-pump-ductless-detailed-performance.xml,1566.96,144.0,1 base-hvac-mini-split-heat-pump-ductless-heating-capacity-17f.xml,1537.59,144.0,1393.59,0.0,1537.59,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-mini-split-heat-pump-ductless.xml,1537.59,144.0,1393.59,0.0,1537.59,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-multiple.xml,2524.05,144.0,1937.6,0.0,2081.6,144.0,81.1,225.1,0.0,110.65,110.65,0.0,106.7,106.7,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-hvac-none.xml,2608.55,144.0,2464.55,0.0,2608.55,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-hvac-none.xml,2608.49,144.0,2464.49,0.0,2608.49,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-electricity.xml,2022.13,144.0,1878.13,0.0,2022.13,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-natural-gas.xml,1763.47,144.0,1249.48,0.0,1393.48,144.0,225.99,369.99,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac.xml,1384.75,144.0,1240.75,0.0,1384.75,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 diff --git a/workflow/tests/base_results/results_simulations_energy.csv b/workflow/tests/base_results/results_simulations_energy.csv index 79431b1979..0605256411 100644 --- a/workflow/tests/base_results/results_simulations_energy.csv +++ b/workflow/tests/base_results/results_simulations_energy.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,60.121,60.121,35.664,35.664,24.457,0.0,0.0,0.0,0.0,0.0 base-battery-scheduled-power-outage.xml,57.742,57.742,33.905,33.905,23.837,0.0,0.0,0.0,0.0,0.0,0.0,0.591,0.0,0.0,3.406,0.484,8.401,0.0,0.0,4.2,0.0,0.311,0.0,0.0,0.0,0.0,1.882,0.0,0.0,0.292,0.335,1.386,1.401,0.0,1.94,7.686,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.587,23.837,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-battery-scheduled.xml,61.353,61.353,37.537,37.537,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.591,0.0,0.0,4.398,0.662,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.735,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-battery.xml,59.618,59.618,35.802,35.802,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.591,0.0,0.0,4.398,0.662,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,37.125,37.125,24.161,24.161,12.964,0.0,0.0,0.0,0.0,0.0,0.0,0.143,0.0,0.0,1.628,0.163,9.672,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.688,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,12.964,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,35.281,35.281,23.894,23.894,11.387,0.0,0.0,0.0,0.0,0.0,0.0,0.126,0.0,0.0,1.414,0.124,9.67,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.694,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,11.387,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,22.492,22.492,22.492,22.492,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,9.564,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.061,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multiple.xml,32.413,32.413,25.046,25.046,7.367,0.0,0.0,0.0,0.0,0.0,0.0,0.081,0.0,0.0,2.195,0.262,9.558,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.083,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.367,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-bldgtype-mf-unit-adjacent-to-multiple.xml,32.063,32.063,24.859,24.859,7.205,0.0,0.0,0.0,0.0,0.0,0.0,0.079,0.0,0.0,2.039,0.233,9.558,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.082,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.205,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,50.372,50.372,23.946,23.946,26.426,0.0,0.0,0.0,0.0,0.0,0.0,0.291,0.0,0.0,1.495,0.139,9.755,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.399,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,26.426,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,26.161,26.161,24.292,24.292,1.87,0.0,0.0,0.0,0.0,0.0,0.0,0.021,0.0,0.0,1.655,0.169,9.582,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.0,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.87,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,25.902,25.902,24.032,24.032,1.869,0.0,0.0,0.0,0.0,0.0,0.0,0.021,0.0,0.0,1.452,0.131,9.59,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.972,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.869,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,26.556,26.556,24.934,24.934,1.622,0.0,0.0,0.0,0.0,0.0,0.0,0.018,0.0,0.0,2.11,0.256,9.541,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.142,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.622,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-infil-compartmentalization-test.xml,26.757,26.757,25.988,25.988,0.769,0.0,0.0,0.0,0.0,0.0,0.0,0.008,0.0,0.0,2.973,0.423,9.526,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.191,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.769,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-infil-leakiness-description.xml,26.621,26.621,26.127,26.127,0.494,0.0,0.0,0.0,0.0,0.0,0.0,0.005,0.0,0.0,3.083,0.446,9.523,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.203,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.494,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 @@ -306,7 +306,7 @@ base-hvac-mini-split-heat-pump-ductless-detailed-performance.xml,39.093,39.093,3 base-hvac-mini-split-heat-pump-ductless-heating-capacity-17f.xml,38.286,38.286,38.286,38.286,0.0,0.0,0.0,0.0,0.0,0.0,5.974,0.057,0.0,0.0,2.098,0.006,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-mini-split-heat-pump-ductless.xml,38.286,38.286,38.286,38.286,0.0,0.0,0.0,0.0,0.0,0.0,5.974,0.057,0.0,0.0,2.098,0.006,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-multiple.xml,68.904,68.904,53.232,53.232,7.747,3.92,4.005,0.0,0.0,0.0,14.851,1.067,0.322,0.019,6.363,0.46,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.07,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.747,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.92,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.005,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-hvac-none.xml,20.451,20.451,20.451,20.451,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.543,0.0,0.0,2.646,0.0,0.238,0.0,0.0,0.0,0.0,2.991,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,4.192,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-hvac-none.xml,20.45,20.45,20.45,20.45,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.543,0.0,0.0,2.646,0.0,0.238,0.0,0.0,0.0,0.0,2.991,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,4.192,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-electricity.xml,51.598,51.598,51.598,51.598,0.0,0.0,0.0,0.0,0.0,0.0,17.271,0.0,0.0,0.0,4.176,0.0,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-natural-gas.xml,55.916,55.916,34.327,34.327,21.589,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.176,0.0,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,21.589,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac.xml,34.087,34.087,34.087,34.087,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.058,0.0,9.061,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,1.903,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 diff --git a/workflow/tests/base_results/results_simulations_loads.csv b/workflow/tests/base_results/results_simulations_loads.csv index 1de2a855db..ec17d6e88e 100644 --- a/workflow/tests/base_results/results_simulations_loads.csv +++ b/workflow/tests/base_results/results_simulations_loads.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,23.106,0.0,12.576,9.071,0.804,0.0,0.0,0.0,4.166,3.889, base-battery-scheduled-power-outage.xml,22.523,0.0,10.138,8.445,0.582,0.0,0.0,0.0,3.818,3.882,0.545,7.57,0.682,10.754,-13.574,0.0,0.0,0.0,8.353,-0.113,5.216,0.0,0.841,0.0,5.327,-8.473,-2.662,0.0,0.009,-0.253,-0.023,2.661,0.018,-0.837,10.841,0.0,0.0,0.0,-6.39,-0.109,-0.914,-4.426,-0.121,0.0,2.268,5.99,1.538 base-battery-scheduled.xml,22.503,0.0,13.745,9.071,0.615,0.0,0.0,0.0,3.819,3.882,0.545,7.57,0.682,10.76,-13.574,0.0,0.0,0.0,8.363,-0.116,5.259,0.0,0.77,0.0,5.323,-8.475,-2.662,0.0,0.029,-0.188,-0.014,2.827,0.035,-0.632,10.839,0.0,0.0,0.0,-6.138,-0.112,-0.847,-3.884,-0.117,0.0,3.113,7.106,1.845 base-battery.xml,22.503,0.0,13.745,9.071,0.615,0.0,0.0,0.0,3.819,3.882,0.545,7.57,0.682,10.76,-13.574,0.0,0.0,0.0,8.363,-0.116,5.259,0.0,0.77,0.0,5.323,-8.475,-2.662,0.0,0.029,-0.188,-0.014,2.827,0.035,-0.632,10.839,0.0,0.0,0.0,-6.138,-0.112,-0.847,-3.884,-0.117,0.0,3.113,7.106,1.845 -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,12.066,0.0,3.023,9.37,0.731,0.0,0.0,0.0,3.139,3.901,0.0,0.0,0.632,1.39,-1.81,0.0,0.0,3.183,0.0,-0.038,1.638,0.0,0.0,0.0,5.25,-3.938,-1.31,0.0,-0.688,-0.109,0.0,0.0,-0.048,-0.049,1.092,0.0,0.0,-0.692,0.0,-0.034,-0.182,-0.336,0.0,0.0,0.69,2.679,0.715 +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,10.598,0.0,2.224,9.37,0.729,0.0,0.0,0.0,2.782,3.858,0.0,0.0,0.604,1.403,-1.814,0.0,0.0,2.828,0.0,-0.045,1.646,0.0,0.0,0.0,4.543,-3.926,-1.307,0.0,-0.973,-0.132,0.0,0.0,-0.068,-0.035,1.089,0.0,0.0,-0.98,0.0,-0.042,-0.176,-0.312,0.0,0.0,0.458,2.691,0.718 base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,0.0,0.0,0.0,9.37,0.618,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multiple.xml,6.86,0.0,5.135,9.37,0.612,0.0,0.0,0.0,-0.001,3.377,0.0,0.0,1.431,3.799,-4.284,0.0,0.0,4.539,0.0,-0.236,1.163,0.0,0.772,0.0,2.781,-5.429,-1.095,0.0,0.003,-0.481,0.0,0.0,-0.407,-0.186,3.921,0.0,0.0,-2.92,0.0,-0.231,-0.211,-1.105,-0.137,0.0,0.61,5.381,0.93 +base-bldgtype-mf-unit-adjacent-to-multiple.xml,6.709,0.0,4.541,9.37,0.612,0.0,0.0,0.0,-0.0,3.369,0.0,0.0,1.342,3.805,-4.269,0.0,0.0,4.496,0.0,-0.24,1.164,0.0,0.77,0.0,2.728,-5.413,-1.091,0.0,0.003,-0.493,0.0,0.0,-0.534,-0.158,3.936,0.0,0.0,-3.431,0.0,-0.235,-0.209,-1.06,-0.138,0.0,0.553,5.396,0.934 base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,24.602,0.0,2.53,9.37,0.818,0.0,0.0,0.0,5.728,4.447,0.0,0.0,0.845,1.334,-1.983,0.0,0.0,5.801,0.0,-0.071,1.651,0.0,0.0,0.0,12.544,-4.3,-1.415,0.0,-0.789,0.045,0.0,0.0,-0.041,0.017,0.92,0.0,0.0,-0.771,0.0,-0.068,-0.116,-0.245,0.0,0.0,0.653,2.317,0.61 -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1.738,0.0,3.126,9.37,0.637,0.0,0.0,0.0,0.269,3.377,0.0,0.0,0.403,1.443,-1.627,0.0,0.0,0.298,0.0,-0.065,1.636,0.0,0.0,0.0,0.54,-3.44,-1.153,0.0,-0.796,-0.314,0.0,0.0,-0.079,-0.16,1.275,0.0,0.0,-0.803,0.0,-0.062,-0.292,-0.379,0.0,0.0,0.71,3.176,0.872 +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1.738,0.0,2.366,9.37,0.645,0.0,0.0,0.0,0.268,3.38,0.0,0.0,0.404,1.446,-1.626,0.0,0.0,0.297,0.0,-0.068,1.638,0.0,0.0,0.0,0.54,-3.443,-1.153,0.0,-1.051,-0.345,0.0,0.0,-0.097,-0.145,1.276,0.0,0.0,-1.064,0.0,-0.065,-0.286,-0.362,0.0,0.0,0.484,3.174,0.872 base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,1.508,0.0,4.938,9.37,0.594,0.0,0.0,0.0,-0.002,2.727,0.0,0.0,0.312,1.172,-1.175,0.0,0.0,-0.001,0.0,-0.099,1.341,0.0,0.0,0.0,0.473,-2.442,-0.833,0.0,-0.001,-1.062,0.0,0.0,-0.13,-0.6,1.728,0.0,0.0,0.0,0.0,-0.097,-0.738,-0.539,0.0,0.0,1.072,4.174,1.192 base-bldgtype-mf-unit-infil-compartmentalization-test.xml,0.713,0.0,8.513,9.37,0.579,0.0,0.0,0.0,-0.001,1.554,0.0,0.0,0.25,2.239,-1.533,0.0,0.0,0.007,0.0,-0.259,0.539,0.0,0.403,0.0,0.0,-2.138,-0.428,0.0,0.003,-2.108,0.0,0.0,-0.258,-2.871,6.67,0.0,0.0,0.012,0.0,-0.25,-0.731,-1.408,-0.647,0.0,0.0,8.747,1.598 base-bldgtype-mf-unit-infil-leakiness-description.xml,0.458,0.0,8.879,9.37,0.575,0.0,0.0,0.0,0.002,1.375,0.0,0.0,0.215,1.92,-1.32,0.0,0.0,0.011,0.0,-0.195,0.175,0.0,0.353,0.0,0.0,-1.792,-0.357,0.0,0.006,-2.345,0.0,0.0,-0.305,-3.282,6.882,0.0,0.0,0.016,0.0,-0.186,-0.306,-1.44,-0.714,0.0,0.0,9.101,1.668 diff --git a/workflow/tests/base_results/results_simulations_misc.csv b/workflow/tests/base_results/results_simulations_misc.csv index cbb1e09223..0eb0731ce0 100644 --- a/workflow/tests/base_results/results_simulations_misc.csv +++ b/workflow/tests/base_results/results_simulations_misc.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,0.0,0.0,1354.7,998.0,11171.5,2563.5,2094.8,3429.5,3429 base-battery-scheduled-power-outage.xml,0.0,5.0,1241.4,914.9,10291.7,2361.6,2082.8,6804.8,6804.8,23.713,21.153,1.339 base-battery-scheduled.xml,0.0,0.0,1354.7,998.0,11171.6,2563.5,2088.3,3823.9,3823.9,23.71,18.744,1.435 base-battery.xml,0.0,0.0,1354.7,998.0,11171.6,2563.5,2088.3,3823.9,3823.9,23.71,18.744,0.0 -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1502.3,1938.7,1938.7,8.367,6.536,0.0 +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1499.0,1923.6,1923.6,8.362,5.891,0.0 base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1458.7,1399.8,1458.7,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multiple.xml,0.0,2.0,1354.7,998.0,11171.5,3093.4,1523.5,2272.2,2272.2,10.242,10.719,0.0 +base-bldgtype-mf-unit-adjacent-to-multiple.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1523.5,2220.7,2220.7,10.241,10.607,0.0 base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1501.5,2244.1,2244.1,11.889,9.307,0.0 -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1510.0,1994.1,1994.1,3.969,6.537,0.0 +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1507.5,1920.9,1920.9,3.969,5.909,0.0 base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,0.0,0.0,1354.7,998.0,11171.6,3093.4,1510.4,1965.1,1965.1,4.282,4.96,0.0 base-bldgtype-mf-unit-infil-compartmentalization-test.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1610.2,2275.8,2275.8,3.654,7.722,0.0 base-bldgtype-mf-unit-infil-leakiness-description.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1636.2,2159.9,2159.9,3.199,7.672,0.0 From 6db81ef461494d4538c3287564dc012c9ba17fa4 Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 16 Sep 2024 18:56:38 -0600 Subject: [PATCH 13/16] Final pass. Reorganized measure.rb into additional methods and some misc cleanup. --- BuildResidentialHPXML/measure.xml | 6 +- BuildResidentialHPXML/resources/geometry.rb | 2 +- HPXMLtoOpenStudio/measure.rb | 257 ++++++++++-------- HPXMLtoOpenStudio/measure.xml | 14 +- HPXMLtoOpenStudio/resources/internal_gains.rb | 13 +- HPXMLtoOpenStudio/resources/model.rb | 2 +- HPXMLtoOpenStudio/resources/output.rb | 23 +- HPXMLtoOpenStudio/resources/schedules.rb | 2 +- 8 files changed, 192 insertions(+), 127 deletions(-) diff --git a/BuildResidentialHPXML/measure.xml b/BuildResidentialHPXML/measure.xml index 6f315bfcc1..a4f5998811 100644 --- a/BuildResidentialHPXML/measure.xml +++ b/BuildResidentialHPXML/measure.xml @@ -3,8 +3,8 @@ 3.1 build_residential_hpxml a13a8983-2b01-4930-8af2-42030b6e4233 - 84219786-d19f-45a5-b735-1366ee7c2d99 - 2024-09-16T22:42:52Z + 9bf8fac2-4f2e-4adf-9cda-80bc44e44283 + 2024-09-17T00:18:31Z 2C38F48B BuildResidentialHPXML HPXML Builder @@ -7453,7 +7453,7 @@ geometry.rb rb resource - C62D3E76 + F80359E3 extra_files/base-mf.xml diff --git a/BuildResidentialHPXML/resources/geometry.rb b/BuildResidentialHPXML/resources/geometry.rb index c68c7f5764..4c1aea13ac 100644 --- a/BuildResidentialHPXML/resources/geometry.rb +++ b/BuildResidentialHPXML/resources/geometry.rb @@ -2151,7 +2151,7 @@ def self.get_wall_area_for_windows(surface:, end # Gable too short? - # TODO: super crude safety factor of 1.5 + # super crude safety factor of 1.5 if is_gable_wall(surface: surface) && (min_wall_height > get_surface_height(surface: surface) / 1.5) return 0.0 end diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 158c4e9727..55de754e8f 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Require all gems up front; this is much faster than multiple resource +# Require all gems upfront; this is much faster than multiple resource # files lazy loading as needed, as it prevents multiple lookups for the # same gem. require 'pathname' @@ -108,141 +108,57 @@ def run(model, runner, user_arguments) return false end - Model.tear_down(model: model, runner: runner) - Version.check_openstudio_version() + Model.tear_down(model: model, runner: runner) args = runner.getArgumentValues(arguments(model), user_arguments) - - unless (Pathname.new args[:hpxml_path]).absolute? - args[:hpxml_path] = File.expand_path(args[:hpxml_path]) - end - unless File.exist?(args[:hpxml_path]) && args[:hpxml_path].downcase.end_with?('.xml') - fail "'#{args[:hpxml_path]}' does not exist or is not an .xml file." - end - - unless (Pathname.new args[:output_dir]).absolute? - args[:output_dir] = File.expand_path(args[:output_dir]) - end - - unless File.extname(args[:annual_output_file_name]).length > 0 - args[:annual_output_file_name] = "#{args[:annual_output_file_name]}.#{args[:output_format]}" - end - annual_output_file_path = File.join(args[:output_dir], args[:annual_output_file_name]) - - unless File.extname(args[:design_load_details_output_file_name]).length > 0 - args[:design_load_details_output_file_name] = "#{args[:design_load_details_output_file_name]}.#{args[:output_format]}" - end - design_load_details_output_file_path = File.join(args[:output_dir], args[:design_load_details_output_file_name]) + set_file_paths(args) begin - if args[:skip_validation] - schema_validator = nil - schematron_validator = nil - else - schema_path = File.join(File.dirname(__FILE__), 'resources', 'hpxml_schema', 'HPXML.xsd') - schema_validator = XMLValidator.get_xml_validator(schema_path) - schematron_path = File.join(File.dirname(__FILE__), 'resources', 'hpxml_schematron', 'EPvalidator.xml') - schematron_validator = XMLValidator.get_xml_validator(schematron_path) - end - - hpxml = HPXML.new(hpxml_path: args[:hpxml_path], schema_validator: schema_validator, schematron_validator: schematron_validator, building_id: args[:building_id]) - hpxml.errors.each do |error| - runner.registerError(error) - end - hpxml.warnings.each do |warning| - runner.registerWarning(warning) - end + hpxml = create_hpxml_object(args) return false unless hpxml.errors.empty? - # Process weather once upfront - epw_path = Location.get_epw_path(hpxml.buildings[0], args[:hpxml_path]) - weather = WeatherFile.new(epw_path: epw_path, runner: runner, hpxml: hpxml) - hpxml.buildings.each_with_index do |hpxml_bldg, i| - next if i == 0 - next if Location.get_epw_path(hpxml_bldg, args[:hpxml_path]) == epw_path - - fail 'Weather station EPW filepath has different values across dwelling units.' - end - - if hpxml.header.whole_sfa_or_mf_building_sim && (hpxml.buildings.size > 1) - if hpxml.buildings.map { |hpxml_bldg| hpxml_bldg.batteries.size }.sum > 0 - # FUTURE: Figure out how to allow this. If we allow it, update docs and hpxml_translator_test.rb too. - # Batteries use "TrackFacilityElectricDemandStoreExcessOnSite"; to support modeling of batteries in whole - # SFA/MF building simulations, we'd need to create custom meters with electricity usage *for each unit* - # and switch to "TrackMeterDemandStoreExcessOnSite". - # https://github.com/NREL/OpenStudio-HPXML/issues/1499 - fail 'Modeling batteries for whole SFA/MF buildings is not currently supported.' - end - end - - # Apply HPXML defaults upfront; process schedules & emissions - hpxml_sch_map = {} - Schedule.check_emissions_references(hpxml.header, args[:hpxml_path]) - hpxml.buildings.each_with_index do |hpxml_bldg, i| - Schedule.check_schedule_references(hpxml_bldg.header, args[:hpxml_path]) - in_schedules_csv = 'in.schedules.csv' - in_schedules_csv = "in.schedules#{i + 1}.csv" if i > 0 - schedules_file = SchedulesFile.new(runner: runner, - schedules_paths: hpxml_bldg.header.schedules_filepaths, - year: Location.get_sim_calendar_year(hpxml.header.sim_calendar_year, weather), - unavailable_periods: hpxml.header.unavailable_periods, - output_path: File.join(args[:output_dir], in_schedules_csv), - offset_db: hpxml.header.hvac_onoff_thermostat_deadband) - HPXMLDefaults.apply(runner, hpxml, hpxml_bldg, weather, schedules_file: schedules_file, - design_load_details_output_file_path: design_load_details_output_file_path, - output_format: args[:output_format]) - hpxml_sch_map[hpxml_bldg] = schedules_file - end - Schedule.validate_emissions_files(hpxml.header) + # Do these once upfront for the entire HPXML object + epw_path, weather = process_weather(runner, hpxml, args) + process_whole_sfa_mf_inputs(hpxml) + hpxml_sch_map = process_defaults_schedules_emissions_files(runner, weather, hpxml, args) # Write updated HPXML object (w/ defaults) to file for inspection - hpxml_defaults_path = File.join(args[:output_dir], 'in.xml') - XMLHelper.write_file(hpxml.to_doc, hpxml_defaults_path) + XMLHelper.write_file(hpxml.to_doc, args[:hpxml_defaults_path]) # Write annual results output file # This is helpful if the user wants to get these results right away (e.g., # they might be using the run_simulation.rb --skip-simulation argument. results_out = [] Outputs.append_sizing_results(hpxml.buildings, results_out) - Outputs.write_results_out_to_file(results_out, args[:output_format], annual_output_file_path) + Outputs.write_results_out_to_file(results_out, args[:output_format], args[:annual_output_file_path]) - # Create OpenStudio model + # Create OpenStudio unit model(s) hpxml_osm_map = {} hpxml.buildings.each_with_index do |hpxml_bldg, i| - schedules_file = hpxml_sch_map[hpxml_bldg] + # Create the model for this single unit + # If we're running a whole SFA/MF building, all the unit models will be merged later if hpxml.buildings.size > 1 - # Create the model for this single unit unit_model = OpenStudio::Model::Model.new - create_unit_model(hpxml, hpxml_bldg, runner, unit_model, epw_path, weather, schedules_file, i + 1) + create_unit_model(hpxml, hpxml_bldg, runner, unit_model, epw_path, weather, hpxml_sch_map[hpxml_bldg], i + 1) hpxml_osm_map[hpxml_bldg] = unit_model else - create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, schedules_file, i + 1) + create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, hpxml_sch_map[hpxml_bldg], i + 1) hpxml_osm_map[hpxml_bldg] = model end end # Merge unit models into final model if hpxml.buildings.size > 1 - Model.add_unit_model(model, hpxml_osm_map) + Model.merge_unit_models(model, hpxml_osm_map) end - # Output + # Create/write output Outputs.apply_ems_programs(model, hpxml_osm_map, hpxml.header, args[:add_component_loads]) Outputs.apply_output_files(model, args[:debug]) - Outputs.apply_additional_properties(model, hpxml, hpxml_osm_map, args[:hpxml_path], args[:building_id], hpxml_defaults_path) + Outputs.apply_additional_properties(model, hpxml, hpxml_osm_map, args[:hpxml_path], args[:building_id], args[:hpxml_defaults_path]) + Outputs.write_debug_files(runner, model, args[:debug], args[:output_dir], epw_path) # Outputs.apply_ems_debug_output(model) # Uncomment to debug EMS - - if args[:debug] - # Write OSM file to run dir - osm_output_path = File.join(args[:output_dir], 'in.osm') - File.write(osm_output_path, model.to_s) - runner.registerInfo("Wrote file: #{osm_output_path}") - - # Copy EPW file to run dir - epw_output_path = File.join(args[:output_dir], 'in.epw') - FileUtils.cp(epw_path, epw_output_path) - end rescue Exception => e runner.registerError("#{e.message}\n#{e.backtrace.join("\n")}") return false @@ -251,6 +167,131 @@ def run(model, runner, user_arguments) return true end + # Updates the args hash with final paths for various input/output files. + # + # @param args [Hash] Map of :argument_name => value + # @return [nil] + def set_file_paths(args) + if not (Pathname.new args[:hpxml_path]).absolute? + args[:hpxml_path] = File.expand_path(args[:hpxml_path]) + end + if not File.exist?(args[:hpxml_path]) && args[:hpxml_path].downcase.end_with?('.xml') + fail "'#{args[:hpxml_path]}' does not exist or is not an .xml file." + end + + if not (Pathname.new args[:output_dir]).absolute? + args[:output_dir] = File.expand_path(args[:output_dir]) + end + + if File.extname(args[:annual_output_file_name]).length == 0 + args[:annual_output_file_name] = "#{args[:annual_output_file_name]}.#{args[:output_format]}" + end + args[:annual_output_file_path] = File.join(args[:output_dir], args[:annual_output_file_name]) + + if File.extname(args[:design_load_details_output_file_name]).length == 0 + args[:design_load_details_output_file_name] = "#{args[:design_load_details_output_file_name]}.#{args[:output_format]}" + end + args[:design_load_details_output_file_path] = File.join(args[:output_dir], args[:design_load_details_output_file_name]) + + args[:hpxml_defaults_path] = File.join(args[:output_dir], 'in.xml') + end + + # Creates the HPXML object from the HPXML file. Performs schema/schematron validation + # as appropriate. + # + # @param args [Hash] Map of :argument_name => value + # @return [HPXML] HPXML object + def create_hpxml_object(args) + if args[:skip_validation] + schema_validator = nil + schematron_validator = nil + else + schema_path = File.join(File.dirname(__FILE__), 'resources', 'hpxml_schema', 'HPXML.xsd') + schema_validator = XMLValidator.get_xml_validator(schema_path) + schematron_path = File.join(File.dirname(__FILE__), 'resources', 'hpxml_schematron', 'EPvalidator.xml') + schematron_validator = XMLValidator.get_xml_validator(schematron_path) + end + + hpxml = HPXML.new(hpxml_path: args[:hpxml_path], schema_validator: schema_validator, schematron_validator: schematron_validator, building_id: args[:building_id]) + hpxml.errors.each do |error| + runner.registerError(error) + end + hpxml.warnings.each do |warning| + runner.registerWarning(warning) + end + return hpxml + end + + # Returns the EPW file path and the WeatherFile object. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param hpxml [HPXML] HPXML object + # @param args [Hash] Map of :argument_name => value + # @return [Array] Path to the EPW weather file, Weather object containing EPW information + def process_weather(runner, hpxml, args) + epw_path = Location.get_epw_path(hpxml.buildings[0], args[:hpxml_path]) + weather = WeatherFile.new(epw_path: epw_path, runner: runner, hpxml: hpxml) + hpxml.buildings.each_with_index do |hpxml_bldg, i| + next if i == 0 + next if Location.get_epw_path(hpxml_bldg, args[:hpxml_path]) == epw_path + + fail 'Weather station EPW filepath has different values across dwelling units.' + end + + return epw_path, weather + end + + # Performs error-checking on the inputs for whole SFA/MF building simulations. + # + # @param hpxml [HPXML] HPXML object + # @return [nil] + def process_whole_sfa_mf_inputs(hpxml) + if hpxml.header.whole_sfa_or_mf_building_sim && (hpxml.buildings.size > 1) + if hpxml.buildings.map { |hpxml_bldg| hpxml_bldg.batteries.size }.sum > 0 + # FUTURE: Figure out how to allow this. If we allow it, update docs and hpxml_translator_test.rb too. + # Batteries use "TrackFacilityElectricDemandStoreExcessOnSite"; to support modeling of batteries in whole + # SFA/MF building simulations, we'd need to create custom meters with electricity usage *for each unit* + # and switch to "TrackMeterDemandStoreExcessOnSite". + # https://github.com/NREL/OpenStudio-HPXML/issues/1499 + fail 'Modeling batteries for whole SFA/MF buildings is not currently supported.' + end + end + end + + # Processes HPXML defaults, schedules, and emissions files upfront. + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param weather [WeatherFile] Weather object containing EPW information + # @param hpxml [HPXML] HPXML object + # @param args [Hash] Map of :argument_name => value + # @return [Hash] Map of HPXML Building => SchedulesFile object + def process_defaults_schedules_emissions_files(runner, weather, hpxml, args) + hpxml_sch_map = {} + hpxml.buildings.each_with_index do |hpxml_bldg, i| + # Schedules file + Schedule.check_schedule_references(hpxml_bldg.header, args[:hpxml_path]) + in_schedules_csv = i > 0 ? "in.schedules#{i + 1}.csv" : 'in.schedules.csv' + schedules_file = SchedulesFile.new(runner: runner, + schedules_paths: hpxml_bldg.header.schedules_filepaths, + year: Location.get_sim_calendar_year(hpxml.header.sim_calendar_year, weather), + unavailable_periods: hpxml.header.unavailable_periods, + output_path: File.join(args[:output_dir], in_schedules_csv), + offset_db: hpxml.header.hvac_onoff_thermostat_deadband) + hpxml_sch_map[hpxml_bldg] = schedules_file + + # HPXML defaults + HPXMLDefaults.apply(runner, hpxml, hpxml_bldg, weather, schedules_file: schedules_file, + design_load_details_output_file_path: args[:design_load_details_output_file_path], + output_format: args[:output_format]) + end + + # Emissions files + Schedule.check_emissions_references(hpxml.header, args[:hpxml_path]) + Schedule.validate_emissions_files(hpxml.header) + + return hpxml_sch_map + end + # Creates a full OpenStudio model that represents the given HPXML individual dwelling by # adding OpenStudio objects to the empty OpenStudio model for each component of the building. # @@ -277,7 +318,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched SimControls.apply(model, hpxml.header) Location.apply(model, weather, hpxml_bldg, hpxml.header, epw_path) - # Geometry/Enclosure + # Geometry & Enclosure spaces = {} # Map of HPXML locations => OpenStudio Space objects Geometry.apply_roofs(runner, model, spaces, hpxml_bldg, hpxml.header) Geometry.apply_walls(runner, model, spaces, hpxml_bldg, hpxml.header) @@ -293,7 +334,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched Geometry.explode_surfaces(model, hpxml_bldg) Geometry.apply_building_unit(model, unit_num) - # Systems + # HVAC hvac_data = HVAC.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) HVAC.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml.header) HVAC.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml.header, schedules_file) @@ -313,14 +354,16 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched InternalGains.apply_building_occupants(runner, model, hpxml_bldg, hpxml.header, spaces, schedules_file) InternalGains.apply_general_water_use(runner, model, hpxml_bldg, hpxml.header, spaces, schedules_file) - # Other + # Airflow (e.g., ducts, infiltration, ventilation) Airflow.apply(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, hvac_data) + + # Other PV.apply(model, hpxml_bldg) Generator.apply(model, hpxml_bldg) Battery.apply(runner, model, spaces, hpxml_bldg, schedules_file) end - # TODO + # Miscellaneous logic that needs to occur upfront. # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 39a5b296da..dc9bcbf2e7 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - 38d2b85c-d365-4840-99e0-c31750872b0b - 2024-09-16T23:18:13Z + f4eedfe8-91c6-4f25-877e-a5b3ae5e1d62 + 2024-09-17T00:55:16Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,7 +183,7 @@ measure.rb rb script - 008103D2 + E1019376 airflow.rb @@ -399,7 +399,7 @@ internal_gains.rb rb resource - 234EE1ED + 0AC5849A lighting.rb @@ -447,13 +447,13 @@ model.rb rb resource - 040BA07A + C620569D output.rb rb resource - AAF299D9 + 67DCE0D1 psychrometrics.rb @@ -591,7 +591,7 @@ schedules.rb rb resource - 29AC582F + 19EC4B9B simcontrols.rb diff --git a/HPXMLtoOpenStudio/resources/internal_gains.rb b/HPXMLtoOpenStudio/resources/internal_gains.rb index 7185fc6230..f99661e177 100644 --- a/HPXMLtoOpenStudio/resources/internal_gains.rb +++ b/HPXMLtoOpenStudio/resources/internal_gains.rb @@ -68,10 +68,11 @@ def self.apply_building_occupants(runner, model, hpxml_bldg, hpxml_header, space occ.setNumberofPeopleSchedule(people_sch) end - # Table 4.2.2(3). Internal Gains for Reference Homes + # Gets the default values associated with occupant internal gains. # - # @return [Array] TODO + # @return [Array] Heat gain (Btu/person/hr), Hours per day, sensible/latent fractions def self.get_occupancy_default_values() + # ANSI/RESNET/ICC 301 - Table 4.2.2(3). Internal Gains for Reference Homes hrs_per_day = 16.5 # hrs/day sens_gains = 3716.0 # Btu/person/day lat_gains = 2884.0 # Btu/person/day @@ -127,13 +128,13 @@ def self.apply_general_water_use(runner, model, hpxml_bldg, hpxml_header, spaces end end - # TODO + # Gets the default values associated with general water use internal gains. # # @param nbeds_eq [Integer] Number of bedrooms (or equivalent bedrooms, as adjusted by the number of occupants) in the dwelling unit - # @param general_water_use_usage_multiplier [TODO] TODO - # @return [TODO] TODO + # @param general_water_use_usage_multiplier [Double] Usage multiplier on internal gains + # @return [Array] Sensible/latent internal gains (Btu/yr) def self.get_water_gains_sens_lat(nbeds_eq, general_water_use_usage_multiplier = 1.0) - # Table 4.2.2(3). Internal Gains for Reference Homes + # ANSI/RESNET/ICC 301 - Table 4.2.2(3). Internal Gains for Reference Homes sens_gains = (-1227.0 - 409.0 * nbeds_eq) * general_water_use_usage_multiplier # Btu/day lat_gains = (1245.0 + 415.0 * nbeds_eq) * general_water_use_usage_multiplier # Btu/day return sens_gains * 365.0, lat_gains * 365.0 diff --git a/HPXMLtoOpenStudio/resources/model.rb b/HPXMLtoOpenStudio/resources/model.rb index 8436dbd58e..77e89cbac8 100644 --- a/HPXMLtoOpenStudio/resources/model.rb +++ b/HPXMLtoOpenStudio/resources/model.rb @@ -46,7 +46,7 @@ def self.tear_down(model:, # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit # @return [nil] - def self.add_unit_model(model, hpxml_osm_map) + def self.merge_unit_models(model, hpxml_osm_map) # Handle unique objects first: Grab one from the first model we find the # object on (may not be the first unit). unit_model_objects = [] diff --git a/HPXMLtoOpenStudio/resources/output.rb b/HPXMLtoOpenStudio/resources/output.rb index 5c89e1d55f..8a4a6dc3e9 100644 --- a/HPXMLtoOpenStudio/resources/output.rb +++ b/HPXMLtoOpenStudio/resources/output.rb @@ -915,7 +915,7 @@ def self.apply_total_airflows_ems_program(model, hpxml_osm_map) # Always request MessagePack output. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param debug [Boolean] true writes in.osm, generates additional log output, and creates all E+ output files + # @param debug [Boolean] If true, writes in.osm, generates additional log output, and creates all E+ output files # @return [nil] def self.apply_output_files(model, debug) oj = model.getOutputJSON @@ -978,6 +978,27 @@ def self.apply_ems_debug_output(model) oems.setEMSRuntimeLanguageDebugOutputLevel('Verbose') end + # TODO + # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings + # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param debug [Boolean] If true, writes the OSM/EPW files to the output dir + # @param output_dir [String] Path of the output files directory + # @param epw_path [String] Path to the EPW weather file + # @return [nil] + def self.write_debug_files(runner, model, debug, output_dir, epw_path) + return unless debug + + # Write OSM file to run dir + osm_output_path = File.join(output_dir, 'in.osm') + File.write(osm_output_path, model.to_s) + runner.registerInfo("Wrote file: #{osm_output_path}") + + # Copy EPW file to run dir + epw_output_path = File.join(output_dir, 'in.epw') + FileUtils.cp(epw_path, epw_output_path) + end + # TODO # # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index 7789f370de..b5b742d7dc 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1117,7 +1117,7 @@ def self.check_emissions_references(hpxml_header, hpxml_path) # Check/update schedule file references. # - # @param hpxml_bldg_header [TODO] TODO + # @param hpxml_bldg_header [HPXML::BuildingHeader] HPXML Building Header object # @param hpxml_path [String] Path to the HPXML file # @return [nil] def self.check_schedule_references(hpxml_bldg_header, hpxml_path) From 7f02231840890e7d62e3f2475ef16dd58d313871 Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 16 Sep 2024 19:04:43 -0600 Subject: [PATCH 14/16] Bugfix for CI regression. --- HPXMLtoOpenStudio/measure.rb | 8 +++-- HPXMLtoOpenStudio/measure.xml | 20 ++++++------ HPXMLtoOpenStudio/resources/airflow.rb | 4 +-- HPXMLtoOpenStudio/resources/constructions.rb | 31 +++++-------------- HPXMLtoOpenStudio/resources/geometry.rb | 2 +- .../resources/hotwater_appliances.rb | 2 +- HPXMLtoOpenStudio/resources/hvac.rb | 3 -- HPXMLtoOpenStudio/resources/internal_gains.rb | 4 +-- HPXMLtoOpenStudio/resources/lighting.rb | 2 +- 9 files changed, 30 insertions(+), 46 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index 55de754e8f..f58460eff5 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -115,7 +115,7 @@ def run(model, runner, user_arguments) set_file_paths(args) begin - hpxml = create_hpxml_object(args) + hpxml = create_hpxml_object(runner, args) return false unless hpxml.errors.empty? # Do these once upfront for the entire HPXML object @@ -199,9 +199,10 @@ def set_file_paths(args) # Creates the HPXML object from the HPXML file. Performs schema/schematron validation # as appropriate. # + # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param args [Hash] Map of :argument_name => value # @return [HPXML] HPXML object - def create_hpxml_object(args) + def create_hpxml_object(runner, args) if args[:skip_validation] schema_validator = nil schematron_validator = nil @@ -339,6 +340,9 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched HVAC.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml.header) HVAC.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml.header, schedules_file) + # Kiva foundations (must come after setpoints defined) + Constructions.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml.header, spaces, schedules_file) + # Hot Water & Appliances Waterheater.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index dc9bcbf2e7..0696849a42 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - f4eedfe8-91c6-4f25-877e-a5b3ae5e1d62 - 2024-09-17T00:55:16Z + d4af0b42-b2ce-4fea-a06d-836c842278d6 + 2024-09-17T01:04:26Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,13 +183,13 @@ measure.rb rb script - E1019376 + 4465A06A airflow.rb rb resource - 27C404EF + B2A12361 battery.rb @@ -213,7 +213,7 @@ constructions.rb rb resource - C9CD03A7 + 2BADE7F5 data/Xing_okstate_0664D_13659_Table_A-3.csv @@ -339,13 +339,13 @@ geometry.rb rb resource - 30759588 + 6E08125F hotwater_appliances.rb rb resource - 87025692 + 12F8E8D4 hpxml.rb @@ -387,7 +387,7 @@ hvac.rb rb resource - BF48A30C + DAAC027F hvac_sizing.rb @@ -399,13 +399,13 @@ internal_gains.rb rb resource - 0AC5849A + 29DFFEE4 lighting.rb rb resource - 22236F72 + 327965F6 location.rb diff --git a/HPXMLtoOpenStudio/resources/airflow.rb b/HPXMLtoOpenStudio/resources/airflow.rb index 9d899fee32..835c8fb7a0 100644 --- a/HPXMLtoOpenStudio/resources/airflow.rb +++ b/HPXMLtoOpenStudio/resources/airflow.rb @@ -14,7 +14,7 @@ module Airflow # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files @@ -2789,7 +2789,7 @@ def self.get_mech_vent_qfan_cfm(q_tot, q_inf, is_balanced, frac_imbal, a_ext, bl # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects # @return [TODO] TODO diff --git a/HPXMLtoOpenStudio/resources/constructions.rb b/HPXMLtoOpenStudio/resources/constructions.rb index e9fdd0d057..b71db99f02 100644 --- a/HPXMLtoOpenStudio/resources/constructions.rb +++ b/HPXMLtoOpenStudio/resources/constructions.rb @@ -2,8 +2,6 @@ # Collection of methods related to surface constructions. module Constructions - # Container class for walls, floors/ceilings, roofs, etc. - # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object @@ -1507,7 +1505,7 @@ def self.apply_skylight(model, subsurface, constr_name, ufactor, shgc) # @param constr_name [TODO] TODO # @param mat_int_finish [TODO] TODO # @param partition_wall_area [TODO] TODO - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [TODO] TODO def self.apply_partition_walls(model, constr_name, mat_int_finish, partition_wall_area, spaces) return if partition_wall_area <= 0 @@ -1517,31 +1515,15 @@ def self.apply_partition_walls(model, constr_name, mat_int_finish, partition_wal obj_name = 'partition wall mass' imdef = create_os_int_mass_and_def(model, obj_name, spaces[HPXML::LocationConditionedSpace], partition_wall_area) - apply_wood_stud_wall(model, - [imdef], - constr_name, - 0, - 1, - 3.5, - false, - 0.16, - mat_int_finish, - 0, - 0, - mat_int_finish, - false, - Material.AirFilmVertical, - Material.AirFilmVertical, - 1, - nil, - nil) + apply_wood_stud_wall(model, [imdef], constr_name, 0, 1, 3.5, false, 0.16, mat_int_finish, 0, 0, mat_int_finish, + false, Material.AirFilmVertical, Material.AirFilmVertical, 1, nil, nil) end # TODO # # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param furniture_mass [TODO] TODO - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @return [TODO] TODO def self.apply_furniture(model, furniture_mass, spaces) if furniture_mass.type == HPXML::FurnitureMassTypeLightWeight @@ -2021,10 +2003,10 @@ def self.apply_kiva_settings(model, soil_k_in) # @param weather [WeatherFile] Weather object containing EPW information # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param conditioned_zone [TODO] TODO + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_header, conditioned_zone, schedules_file) + def self.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_header, spaces, schedules_file) sim_begin_month = hpxml_header.sim_begin_month sim_begin_day = hpxml_header.sim_begin_day sim_year = hpxml_header.sim_calendar_year @@ -2057,6 +2039,7 @@ def self.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_heade end # Approximate indoor temperature + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized # Building has HVAC system setpoint_sch = conditioned_zone.thermostatSetpointDualSetpoint.get diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index 3701b92e3a..df8eeb887c 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -1102,7 +1102,7 @@ def self.get_occupancy_default_num(nbeds:) # Sets a "dwelling unit multiplier" equal to the number of similar units represented. # # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param location [String] HPXML location # @param zone_multiplier [Integer] the number of similar zones represented # @return [OpenStudio::Model::Space, nil] updated spaces hash if location is not already a key diff --git a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb index c8e6e496c9..f361bb20bd 100644 --- a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb +++ b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb @@ -7,7 +7,7 @@ module HotWaterAndAppliances # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param weather [WeatherFile] Weather object containing EPW information - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index 5a2f2bbaec..937a99c348 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -1397,9 +1397,6 @@ def self.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_heade thermostat_setpoint.setCoolingSetpointTemperatureSchedule(cooling_sch) thermostat_setpoint.setTemperatureDifferenceBetweenCutoutAndSetpoint(UnitConversions.convert(onoff_thermostat_ddb, 'deltaF', 'deltaC')) conditioned_zone.setThermostatSetpointDualSetpoint(thermostat_setpoint) - - # Now that we have assigned the HVAC setpoint, set Kiva foundation initial temperatures. - Constructions.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_header, conditioned_zone, schedules_file) end # TODO diff --git a/HPXMLtoOpenStudio/resources/internal_gains.rb b/HPXMLtoOpenStudio/resources/internal_gains.rb index f99661e177..a6fafe4525 100644 --- a/HPXMLtoOpenStudio/resources/internal_gains.rb +++ b/HPXMLtoOpenStudio/resources/internal_gains.rb @@ -8,7 +8,7 @@ module InternalGains # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] def self.apply_building_occupants(runner, model, hpxml_bldg, hpxml_header, spaces, schedules_file) @@ -90,7 +90,7 @@ def self.get_occupancy_default_values() # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] def self.apply_general_water_use(runner, model, hpxml_bldg, hpxml_header, spaces, schedules_file) diff --git a/HPXMLtoOpenStudio/resources/lighting.rb b/HPXMLtoOpenStudio/resources/lighting.rb index 0213526200..400eb80bd8 100644 --- a/HPXMLtoOpenStudio/resources/lighting.rb +++ b/HPXMLtoOpenStudio/resources/lighting.rb @@ -6,7 +6,7 @@ module Lighting # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object - # @param spaces [Hash] keys are locations and values are OpenStudio::Model::Space objects + # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files From c837fdfcf3f5a0be8441bf82abe0fb658f722e5f Mon Sep 17 00:00:00 2001 From: Scott Horowitz Date: Mon, 16 Sep 2024 21:42:31 -0600 Subject: [PATCH 15/16] Move setpoints code back to the beginning. --- HPXMLtoOpenStudio/measure.rb | 15 +- HPXMLtoOpenStudio/measure.xml | 14 +- HPXMLtoOpenStudio/resources/airflow.rb | 7 +- HPXMLtoOpenStudio/resources/constructions.rb | 213 +++++++------ HPXMLtoOpenStudio/resources/geometry.rb | 22 +- HPXMLtoOpenStudio/resources/hvac.rb | 297 ++++++++++--------- 6 files changed, 299 insertions(+), 269 deletions(-) diff --git a/HPXMLtoOpenStudio/measure.rb b/HPXMLtoOpenStudio/measure.rb index f58460eff5..7d1b27cd02 100644 --- a/HPXMLtoOpenStudio/measure.rb +++ b/HPXMLtoOpenStudio/measure.rb @@ -319,13 +319,17 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched SimControls.apply(model, hpxml.header) Location.apply(model, weather, hpxml_bldg, hpxml.header, epw_path) - # Geometry & Enclosure + # Conditioned space & setpoints spaces = {} # Map of HPXML locations => OpenStudio Space objects + Geometry.create_or_get_space(model, spaces, HPXML::LocationConditionedSpace, hpxml_bldg) + hvac_days = HVAC.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + + # Geometry & Enclosure Geometry.apply_roofs(runner, model, spaces, hpxml_bldg, hpxml.header) Geometry.apply_walls(runner, model, spaces, hpxml_bldg, hpxml.header) Geometry.apply_rim_joists(runner, model, spaces, hpxml_bldg) Geometry.apply_floors(runner, model, spaces, hpxml_bldg, hpxml.header) - Geometry.apply_foundation_walls_slabs(runner, model, spaces, hpxml_bldg) + Geometry.apply_foundation_walls_slabs(runner, model, spaces, weather, hpxml_bldg, hpxml.header, schedules_file) Geometry.apply_windows(model, spaces, hpxml_bldg, hpxml.header) Geometry.apply_doors(model, spaces, hpxml_bldg) Geometry.apply_skylights(model, spaces, hpxml_bldg, hpxml.header) @@ -336,13 +340,10 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched Geometry.apply_building_unit(model, unit_num) # HVAC - hvac_data = HVAC.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) + airloop_map = HVAC.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, hvac_days) HVAC.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml.header) HVAC.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml.header, schedules_file) - # Kiva foundations (must come after setpoints defined) - Constructions.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml.header, spaces, schedules_file) - # Hot Water & Appliances Waterheater.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file) @@ -359,7 +360,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, weather, sched InternalGains.apply_general_water_use(runner, model, hpxml_bldg, hpxml.header, spaces, schedules_file) # Airflow (e.g., ducts, infiltration, ventilation) - Airflow.apply(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, hvac_data) + Airflow.apply(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, airloop_map) # Other PV.apply(model, hpxml_bldg) diff --git a/HPXMLtoOpenStudio/measure.xml b/HPXMLtoOpenStudio/measure.xml index 0696849a42..3356eb2fd7 100644 --- a/HPXMLtoOpenStudio/measure.xml +++ b/HPXMLtoOpenStudio/measure.xml @@ -3,8 +3,8 @@ 3.1 hpxm_lto_openstudio b1543b30-9465-45ff-ba04-1d1f85e763bc - d4af0b42-b2ce-4fea-a06d-836c842278d6 - 2024-09-17T01:04:26Z + bd97e343-382a-458f-8841-a4074e467691 + 2024-09-17T03:41:38Z D8922A73 HPXMLtoOpenStudio HPXML to OpenStudio Translator @@ -183,13 +183,13 @@ measure.rb rb script - 4465A06A + AE303996 airflow.rb rb resource - B2A12361 + FB90CDF2 battery.rb @@ -213,7 +213,7 @@ constructions.rb rb resource - 2BADE7F5 + 867600F4 data/Xing_okstate_0664D_13659_Table_A-3.csv @@ -339,7 +339,7 @@ geometry.rb rb resource - 6E08125F + 095FE2B7 hotwater_appliances.rb @@ -387,7 +387,7 @@ hvac.rb rb resource - DAAC027F + 13C7BF7E hvac_sizing.rb diff --git a/HPXMLtoOpenStudio/resources/airflow.rb b/HPXMLtoOpenStudio/resources/airflow.rb index 835c8fb7a0..68edc887a8 100644 --- a/HPXMLtoOpenStudio/resources/airflow.rb +++ b/HPXMLtoOpenStudio/resources/airflow.rb @@ -18,11 +18,10 @@ module Airflow # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @param hvac_data [Array] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects, HVAC unavailable period objects + # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects # @return [TODO] TODO - def self.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, hvac_data) + def self.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) # Global variables - @runner = runner @spaces = spaces @year = hpxml_header.sim_calendar_year @@ -35,8 +34,8 @@ def self.apply(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedul @clothes_dryer_in_cond_space = hpxml_bldg.clothes_dryers.empty? ? true : HPXML::conditioned_locations_this_unit.include?(hpxml_bldg.clothes_dryers[0].location) cfa = hpxml_bldg.building_construction.conditioned_floor_area unavailable_periods = hpxml_header.unavailable_periods - airloop_map, hvac_unavailable_periods = hvac_data frac_windows_operable = hpxml_bldg.additional_properties.initial_frac_windows_operable + hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, hpxml_header.unavailable_periods) # Global sensors diff --git a/HPXMLtoOpenStudio/resources/constructions.rb b/HPXMLtoOpenStudio/resources/constructions.rb index b71db99f02..6d02d4d2f2 100644 --- a/HPXMLtoOpenStudio/resources/constructions.rb +++ b/HPXMLtoOpenStudio/resources/constructions.rb @@ -1997,143 +1997,140 @@ def self.apply_kiva_settings(model, soil_k_in) settings.setSimulationTimestep('Timestep') end - # Sets Kiva foundation initial temperatures. + # Sets Kiva foundation initial temperature. # - # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param foundation [TODO] TODO # @param weather [WeatherFile] Weather object containing EPW information # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files + # @param interior_adjacent_to [String] Interior adjacent to location (HPXML::LocationXXX) # @return [nil] - def self.apply_kiva_initial_temperatures(model, weather, hpxml_bldg, hpxml_header, spaces, schedules_file) + def self.apply_kiva_initial_temperature(foundation, weather, hpxml_bldg, hpxml_header, spaces, schedules_file, interior_adjacent_to) sim_begin_month = hpxml_header.sim_begin_month sim_begin_day = hpxml_header.sim_begin_day sim_year = hpxml_header.sim_calendar_year outdoor_temp = weather.data.MonthlyAvgDrybulbs[sim_begin_month - 1] - model.getFoundationKivas.each do |foundation| - interior_adjacent_to = foundation.surfaces[0].space.get.thermalZone.get.additionalProperties.getFeatureAsString('ObjectType').to_s + foundation_walls_insulated = false + hpxml_bldg.foundation_walls.each do |fnd_wall| + next unless fnd_wall.interior_adjacent_to == interior_adjacent_to + next unless fnd_wall.exterior_adjacent_to == HPXML::LocationGround - foundation_walls_insulated = false - hpxml_bldg.foundation_walls.each do |fnd_wall| - next unless fnd_wall.interior_adjacent_to == interior_adjacent_to - next unless fnd_wall.exterior_adjacent_to == HPXML::LocationGround - - if fnd_wall.insulation_assembly_r_value.to_f > 5 - foundation_walls_insulated = true - elsif fnd_wall.insulation_exterior_r_value.to_f + fnd_wall.insulation_interior_r_value.to_f > 0 - foundation_walls_insulated = true - end + if fnd_wall.insulation_assembly_r_value.to_f > 5 + foundation_walls_insulated = true + elsif fnd_wall.insulation_exterior_r_value.to_f + fnd_wall.insulation_interior_r_value.to_f > 0 + foundation_walls_insulated = true end + end - foundation_ceiling_insulated = false - hpxml_bldg.floors.each do |floor| - next unless floor.interior_adjacent_to == HPXML::LocationConditionedSpace - next unless floor.exterior_adjacent_to == interior_adjacent_to + foundation_ceiling_insulated = false + hpxml_bldg.floors.each do |floor| + next unless floor.interior_adjacent_to == HPXML::LocationConditionedSpace + next unless floor.exterior_adjacent_to == interior_adjacent_to - if floor.insulation_assembly_r_value > 5 - foundation_ceiling_insulated = true - end + if floor.insulation_assembly_r_value > 5 + foundation_ceiling_insulated = true end + end + + # Approximate indoor temperature + conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get + if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized + # Building has HVAC system + setpoint_sch = conditioned_zone.thermostatSetpointDualSetpoint.get + sim_begin_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(sim_begin_month), sim_begin_day, sim_year) + sim_begin_hour = (Calendar.get_day_num_from_month_day(sim_year, sim_begin_month, sim_begin_day) - 1) * 24 - # Approximate indoor temperature - conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get - if conditioned_zone.thermostatSetpointDualSetpoint.is_initialized - # Building has HVAC system - setpoint_sch = conditioned_zone.thermostatSetpointDualSetpoint.get - sim_begin_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(sim_begin_month), sim_begin_day, sim_year) - sim_begin_hour = (Calendar.get_day_num_from_month_day(sim_year, sim_begin_month, sim_begin_day) - 1) * 24 + # Get heating/cooling setpoints for the simulation start + htg_setpoint_sch = setpoint_sch.heatingSetpointTemperatureSchedule.get + if htg_setpoint_sch.to_ScheduleRuleset.is_initialized + htg_day_sch = htg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] + heat_setpoint = UnitConversions.convert(htg_day_sch.values[0], 'C', 'F') + else + heat_setpoint = schedules_file.schedules[SchedulesFile::Columns[:HeatingSetpoint].name][sim_begin_hour] + end + clg_setpoint_sch = setpoint_sch.coolingSetpointTemperatureSchedule.get + if clg_setpoint_sch.to_ScheduleRuleset.is_initialized + clg_day_sch = clg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] + cool_setpoint = UnitConversions.convert(clg_day_sch.values[0], 'C', 'F') + else + cool_setpoint = schedules_file.schedules[SchedulesFile::Columns[:CoolingSetpoint].name][sim_begin_hour] + end + + # Methodology adapted from https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L303-L313 + heat_balance_temp = UnitConversions.convert(10.0, 'C', 'F') + cool_balance_temp = UnitConversions.convert(15.0, 'C', 'F') + if outdoor_temp < heat_balance_temp + indoor_temp = heat_setpoint + elsif outdoor_temp > cool_balance_temp + indoor_temp = cool_setpoint + elsif cool_balance_temp == heat_balance_temp + indoor_temp = heat_balance_temp + else + weight = (cool_balance_temp - outdoor_temp) / (cool_balance_temp - heat_balance_temp) + indoor_temp = heat_setpoint * weight + cool_setpoint * (1.0 - weight) + end + else + # Building does not have HVAC system + indoor_temp = outdoor_temp + end - # Get heating/cooling setpoints for the simulation start - htg_setpoint_sch = setpoint_sch.heatingSetpointTemperatureSchedule.get - if htg_setpoint_sch.to_ScheduleRuleset.is_initialized - htg_day_sch = htg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] - heat_setpoint = UnitConversions.convert(htg_day_sch.values[0], 'C', 'F') + # Determine initial temperature + # For unconditioned spaces, this overrides EnergyPlus's built-in assumption of 22C (71.6F); + # see https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L257-L259 + # For conditioned spaces, this avoids an E+ 22.2 bug; see https://github.com/NREL/EnergyPlus/issues/9692 + if HPXML::conditioned_locations.include? interior_adjacent_to + initial_temp = indoor_temp + else + # Space temperature assumptions from ASHRAE 152 - Duct Efficiency Calculations.xls, Zone temperatures + ground_temp = weather.data.ShallowGroundMonthlyTemps[sim_begin_month - 1] + if interior_adjacent_to == HPXML::LocationBasementUnconditioned + if foundation_ceiling_insulated + # Insulated ceiling: 75% ground, 25% outdoor, 0% indoor + ground_weight, outdoor_weight, indoor_weight = 0.75, 0.25, 0.0 + elsif foundation_walls_insulated + # Insulated walls: 50% ground, 0% outdoor, 50% indoor (case not in ASHRAE 152) + ground_weight, outdoor_weight, indoor_weight = 0.5, 0.0, 0.5 else - heat_setpoint = schedules_file.schedules[SchedulesFile::Columns[:HeatingSetpoint].name][sim_begin_hour] + # Uninsulated: 50% ground, 20% outdoor, 30% indoor + ground_weight, outdoor_weight, indoor_weight = 0.5, 0.2, 0.3 end - clg_setpoint_sch = setpoint_sch.coolingSetpointTemperatureSchedule.get - if clg_setpoint_sch.to_ScheduleRuleset.is_initialized - clg_day_sch = clg_setpoint_sch.to_ScheduleRuleset.get.getDaySchedules(sim_begin_date, sim_begin_date)[0] - cool_setpoint = UnitConversions.convert(clg_day_sch.values[0], 'C', 'F') + initial_temp = outdoor_temp * outdoor_weight + ground_temp * ground_weight + indoor_weight * indoor_temp + elsif interior_adjacent_to == HPXML::LocationCrawlspaceVented + if foundation_ceiling_insulated + # Insulated ceiling: 90% outdoor, 10% indoor + outdoor_weight, indoor_weight = 0.9, 0.1 + elsif foundation_walls_insulated + # Insulated walls: 25% outdoor, 75% indoor (case not in ASHRAE 152) + outdoor_weight, indoor_weight = 0.25, 0.75 else - cool_setpoint = schedules_file.schedules[SchedulesFile::Columns[:CoolingSetpoint].name][sim_begin_hour] + # Uninsulated: 50% outdoor, 50% indoor + outdoor_weight, indoor_weight = 0.5, 0.5 end - - # Methodology adapted from https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L303-L313 - heat_balance_temp = UnitConversions.convert(10.0, 'C', 'F') - cool_balance_temp = UnitConversions.convert(15.0, 'C', 'F') - if outdoor_temp < heat_balance_temp - indoor_temp = heat_setpoint - elsif outdoor_temp > cool_balance_temp - indoor_temp = cool_setpoint - elsif cool_balance_temp == heat_balance_temp - indoor_temp = heat_balance_temp + initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp + elsif interior_adjacent_to == HPXML::LocationCrawlspaceUnvented + if foundation_ceiling_insulated + # Insulated ceiling: 85% outdoor, 15% indoor + outdoor_weight, indoor_weight = 0.85, 0.15 + elsif foundation_walls_insulated + # Insulated walls: 25% outdoor, 75% indoor + outdoor_weight, indoor_weight = 0.25, 0.75 else - weight = (cool_balance_temp - outdoor_temp) / (cool_balance_temp - heat_balance_temp) - indoor_temp = heat_setpoint * weight + cool_setpoint * (1.0 - weight) + # Uninsulated: 40% outdoor, 60% indoor + outdoor_weight, indoor_weight = 0.4, 0.6 end + initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp + elsif interior_adjacent_to == HPXML::LocationGarage + initial_temp = outdoor_temp + 11.0 else - # Building does not have HVAC system - indoor_temp = outdoor_temp + fail "Unhandled space: #{interior_adjacent_to}" end - - # Determine initial temperature - # For unconditioned spaces, this overrides EnergyPlus's built-in assumption of 22C (71.6F); - # see https://github.com/NREL/EnergyPlus/blob/b18a2733c3131db808feac44bc278a14b05d8e1f/src/EnergyPlus/HeatBalanceKivaManager.cc#L257-L259 - # For conditioned spaces, this avoids an E+ 22.2 bug; see https://github.com/NREL/EnergyPlus/issues/9692 - if HPXML::conditioned_locations.include? interior_adjacent_to - initial_temp = indoor_temp - else - # Space temperature assumptions from ASHRAE 152 - Duct Efficiency Calculations.xls, Zone temperatures - ground_temp = weather.data.ShallowGroundMonthlyTemps[sim_begin_month - 1] - if interior_adjacent_to == HPXML::LocationBasementUnconditioned - if foundation_ceiling_insulated - # Insulated ceiling: 75% ground, 25% outdoor, 0% indoor - ground_weight, outdoor_weight, indoor_weight = 0.75, 0.25, 0.0 - elsif foundation_walls_insulated - # Insulated walls: 50% ground, 0% outdoor, 50% indoor (case not in ASHRAE 152) - ground_weight, outdoor_weight, indoor_weight = 0.5, 0.0, 0.5 - else - # Uninsulated: 50% ground, 20% outdoor, 30% indoor - ground_weight, outdoor_weight, indoor_weight = 0.5, 0.2, 0.3 - end - initial_temp = outdoor_temp * outdoor_weight + ground_temp * ground_weight + indoor_weight * indoor_temp - elsif interior_adjacent_to == HPXML::LocationCrawlspaceVented - if foundation_ceiling_insulated - # Insulated ceiling: 90% outdoor, 10% indoor - outdoor_weight, indoor_weight = 0.9, 0.1 - elsif foundation_walls_insulated - # Insulated walls: 25% outdoor, 75% indoor (case not in ASHRAE 152) - outdoor_weight, indoor_weight = 0.25, 0.75 - else - # Uninsulated: 50% outdoor, 50% indoor - outdoor_weight, indoor_weight = 0.5, 0.5 - end - initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp - elsif interior_adjacent_to == HPXML::LocationCrawlspaceUnvented - if foundation_ceiling_insulated - # Insulated ceiling: 85% outdoor, 15% indoor - outdoor_weight, indoor_weight = 0.85, 0.15 - elsif foundation_walls_insulated - # Insulated walls: 25% outdoor, 75% indoor - outdoor_weight, indoor_weight = 0.25, 0.75 - else - # Uninsulated: 40% outdoor, 60% indoor - outdoor_weight, indoor_weight = 0.4, 0.6 - end - initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp - elsif interior_adjacent_to == HPXML::LocationGarage - initial_temp = outdoor_temp + 11.0 - else - fail "Unhandled space: #{interior_adjacent_to}" - end - end - - foundation.setInitialIndoorAirTemperature(UnitConversions.convert(initial_temp, 'F', 'C')) end + + foundation.setInitialIndoorAirTemperature(UnitConversions.convert(initial_temp, 'F', 'C')) end # TODO diff --git a/HPXMLtoOpenStudio/resources/geometry.rb b/HPXMLtoOpenStudio/resources/geometry.rb index df8eeb887c..1667b171f8 100644 --- a/HPXMLtoOpenStudio/resources/geometry.rb +++ b/HPXMLtoOpenStudio/resources/geometry.rb @@ -386,9 +386,12 @@ def self.apply_floors(runner, model, spaces, hpxml_bldg, hpxml_header) # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings # @param model [OpenStudio::Model::Model] OpenStudio Model object # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects + # @param weather [WeatherFile] Weather object containing EPW information # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_foundation_walls_slabs(runner, model, spaces, hpxml_bldg) + def self.apply_foundation_walls_slabs(runner, model, spaces, weather, hpxml_bldg, hpxml_header, schedules_file) default_azimuths = HPXMLDefaults.get_default_azimuths(hpxml_bldg) foundation_types = hpxml_bldg.slabs.map { |s| s.interior_adjacent_to }.uniq @@ -408,7 +411,7 @@ def self.apply_foundation_walls_slabs(runner, model, spaces, hpxml_bldg) if ext_fnd_walls.empty? # Slab w/o foundation walls - apply_foundation_slab(model, spaces, hpxml_bldg, slab, -1 * slab.depth_below_grade.to_f, slab.exposed_perimeter, nil, default_azimuths) + apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, -1 * slab.depth_below_grade.to_f, slab.exposed_perimeter, nil, default_azimuths, schedules_file) else # Slab w/ foundation walls ext_fnd_walls_length = ext_fnd_walls.map { |fw| fw.area / fw.height }.sum @@ -426,14 +429,14 @@ def self.apply_foundation_walls_slabs(runner, model, spaces, hpxml_bldg) remaining_exposed_length -= exposed_length kiva_foundation = apply_foundation_wall(runner, model, spaces, hpxml_bldg, fnd_wall, exposed_length, fnd_wall_length, default_azimuths) - apply_foundation_slab(model, spaces, hpxml_bldg, slab, -1 * fnd_wall.depth_below_grade, exposed_length, kiva_foundation, default_azimuths) + apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, -1 * fnd_wall.depth_below_grade, exposed_length, kiva_foundation, default_azimuths, schedules_file) end if remaining_exposed_length > 1 # Skip if a small length (e.g., due to rounding) # The slab's exposed perimeter exceeds the sum of attached exterior foundation wall lengths. # This may legitimately occur for a walkout basement, where a portion of the slab has no # adjacent foundation wall. - apply_foundation_slab(model, spaces, hpxml_bldg, slab, 0, remaining_exposed_length, nil, default_azimuths) + apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, 0, remaining_exposed_length, nil, default_azimuths, schedules_file) end end end @@ -589,15 +592,19 @@ def self.apply_foundation_wall(runner, model, spaces, hpxml_bldg, foundation_wal # Adds an HPXML Slab to the OpenStudio model. # # @param model [OpenStudio::Model::Model] OpenStudio Model object + # @param weather [WeatherFile] Weather object containing EPW information # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit + # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param slab [HPXML::Slab] HPXML Slab object # @param z_origin [Double] The z-coordinate for which the slab is relative (ft) # @param exposed_length [Double] TODO # @param kiva_foundation [OpenStudio::Model::FoundationKiva] OpenStudio Foundation Kiva object # @param default_azimuths [TODO] TODO + # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @return [nil] - def self.apply_foundation_slab(model, spaces, hpxml_bldg, slab, z_origin, exposed_length, kiva_foundation, default_azimuths) + def self.apply_foundation_slab(model, weather, spaces, hpxml_bldg, hpxml_header, slab, z_origin, + exposed_length, kiva_foundation, default_azimuths, schedules_file) exposed_fraction = exposed_length / slab.exposed_perimeter slab_tot_perim = exposed_length slab_area = slab.area * exposed_fraction @@ -659,6 +666,11 @@ def self.apply_foundation_slab(model, spaces, hpxml_bldg, slab, z_origin, expose exposed_length, mat_carpet, soil_k_in, kiva_foundation, ext_horiz_r, ext_horiz_width, ext_horiz_depth) + kiva_foundation = surface.adjacentFoundation.get + + Constructions.apply_kiva_initial_temperature(kiva_foundation, weather, hpxml_bldg, hpxml_header, + spaces, schedules_file, slab.interior_adjacent_to) + return kiva_foundation end diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb index 937a99c348..04cdd65ab4 100644 --- a/HPXMLtoOpenStudio/resources/hvac.rb +++ b/HPXMLtoOpenStudio/resources/hvac.rb @@ -17,45 +17,28 @@ module HVAC # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @return [Array] Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil), TODO - def self.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) + # @param hvac_days [TODO] TODO + # @return [Hash] Map of HPXML System ID -> AirLoopHVAC (or ZoneHVACFourPipeFanCoil) + def self.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, hvac_days) # Init - @remaining_heat_load_frac = 1.0 - @remaining_cool_load_frac = 1.0 + remaining_load_fracs = { htg: 1.0, clg: 1.0 } @hp_backup_system_object = nil - @hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, hpxml_header.unavailable_periods) airloop_map = {} if hpxml_bldg.hvac_controls.size == 0 - return airloop_map, @hvac_unavailable_periods + return airloop_map end - # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling - # season begin/end month/day, respectively. - hvac_control = hpxml_bldg.hvac_controls[0] - htg_start_month = hvac_control.seasons_heating_begin_month - htg_start_day = hvac_control.seasons_heating_begin_day - htg_end_month = hvac_control.seasons_heating_end_month - htg_end_day = hvac_control.seasons_heating_end_day - clg_start_month = hvac_control.seasons_cooling_begin_month - clg_start_day = hvac_control.seasons_cooling_begin_day - clg_end_month = hvac_control.seasons_cooling_end_month - clg_end_day = hvac_control.seasons_cooling_end_day - if (htg_start_month != 1) || (htg_start_day != 1) || (htg_end_month != 12) || (htg_end_day != 31) || (clg_start_month != 1) || (clg_start_day != 1) || (clg_end_month != 12) || (clg_end_day != 31) - runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.') - end - @heating_days = Calendar.get_daily_season(hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) - @cooling_days = Calendar.get_daily_season(hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) + hvac_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:HVAC].name, hpxml_header.unavailable_periods) apply_unit_multiplier(hpxml_bldg, hpxml_header) ensure_nonzero_sizing_values(hpxml_bldg) - apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) - apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header) - apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) - apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) - apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header, hvac_days, hvac_unavailable_periods, remaining_load_fracs) + apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map, hvac_days, hvac_unavailable_periods, remaining_load_fracs) + apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map, hvac_days, hvac_unavailable_periods, remaining_load_fracs) + apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map, hvac_days, hvac_unavailable_periods, remaining_load_fracs) - return airloop_map, @hvac_unavailable_periods + return airloop_map end # Adds any HPXML Cooling Systems to the OpenStudio model. @@ -69,8 +52,12 @@ def self.apply_hvac_systems(runner, model, weather, spaces, hpxml_bldg, hpxml_he # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @param hvac_days [TODO] TODO + # @param hvac_unavailable_periods [TODO] TODO + # @param remaining_load_fracs [TODO] TODO # @return [nil] - def self.apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + def self.apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map, + hvac_days, hvac_unavailable_periods, remaining_load_fracs) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get get_hpxml_hvac_systems(hpxml_bldg).each do |hvac_system| @@ -83,16 +70,16 @@ def self.apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ check_distribution_system(cooling_system.distribution_system, cooling_system.cooling_system_type) # Calculate cooling sequential load fractions - sequential_cool_load_fracs = calc_sequential_load_fractions(cooling_system.fraction_cool_load_served.to_f, @remaining_cool_load_frac, @cooling_days) - @remaining_cool_load_frac -= cooling_system.fraction_cool_load_served.to_f + sequential_cool_load_fracs = calc_sequential_load_fractions(cooling_system.fraction_cool_load_served.to_f, remaining_load_fracs[:clg], hvac_days[:clg]) + remaining_load_fracs[:clg] -= cooling_system.fraction_cool_load_served.to_f # Calculate heating sequential load fractions if not heating_system.nil? - sequential_heat_load_fracs = calc_sequential_load_fractions(heating_system.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= heating_system.fraction_heat_load_served + sequential_heat_load_fracs = calc_sequential_load_fractions(heating_system.fraction_heat_load_served, remaining_load_fracs[:htg], hvac_days[:htg]) + remaining_load_fracs[:htg] -= heating_system.fraction_heat_load_served elsif cooling_system.has_integrated_heating - sequential_heat_load_fracs = calc_sequential_load_fractions(cooling_system.integrated_heating_system_fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= cooling_system.integrated_heating_system_fraction_heat_load_served + sequential_heat_load_fracs = calc_sequential_load_fractions(cooling_system.integrated_heating_system_fraction_heat_load_served, remaining_load_fracs[:htg], hvac_days[:htg]) + remaining_load_fracs[:htg] -= cooling_system.integrated_heating_system_fraction_heat_load_served else sequential_heat_load_fracs = [0] end @@ -105,11 +92,11 @@ def self.apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ airloop_map[sys_id] = apply_air_source_hvac_systems(model, runner, cooling_system, heating_system, sequential_cool_load_fracs, sequential_heat_load_fracs, weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, conditioned_zone, - @hvac_unavailable_periods, schedules_file, hpxml_bldg, hpxml_header) + hvac_unavailable_periods, schedules_file, hpxml_bldg, hpxml_header) elsif [HPXML::HVACTypeEvaporativeCooler].include? cooling_system.cooling_system_type - airloop_map[sys_id] = apply_evaporative_cooler(model, cooling_system, sequential_cool_load_fracs, conditioned_zone, @hvac_unavailable_periods, + airloop_map[sys_id] = apply_evaporative_cooler(model, cooling_system, sequential_cool_load_fracs, conditioned_zone, hvac_unavailable_periods, hpxml_bldg.building_construction.number_of_units) end end @@ -126,8 +113,12 @@ def self.apply_cooling_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @param hvac_days [TODO] TODO + # @param hvac_unavailable_periods [TODO] TODO + # @param remaining_load_fracs [TODO] TODO # @return [nil] - def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map, + hvac_days, hvac_unavailable_periods, remaining_load_fracs) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get get_hpxml_hvac_systems(hpxml_bldg).each do |hvac_system| @@ -147,13 +138,13 @@ def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ if heating_system.is_heat_pump_backup_system # Heating system will be last in the EquipmentList and should meet entirety of # remaining load during the heating season. - sequential_heat_load_fracs = @heating_days.map(&:to_f) + sequential_heat_load_fracs = hvac_days[:htg].map(&:to_f) if not heating_system.fraction_heat_load_served.nil? fail 'Heat pump backup system cannot have a fraction heat load served specified.' end else - sequential_heat_load_fracs = calc_sequential_load_fractions(heating_system.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= heating_system.fraction_heat_load_served + sequential_heat_load_fracs = calc_sequential_load_fractions(heating_system.fraction_heat_load_served, remaining_load_fracs[:htg], hvac_days[:htg]) + remaining_load_fracs[:htg] -= heating_system.fraction_heat_load_served end sys_id = heating_system.id @@ -161,18 +152,18 @@ def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ airloop_map[sys_id] = apply_air_source_hvac_systems(model, runner, nil, heating_system, [0], sequential_heat_load_fracs, weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, - conditioned_zone, @hvac_unavailable_periods, schedules_file, hpxml_bldg, + conditioned_zone, hvac_unavailable_periods, schedules_file, hpxml_bldg, hpxml_header) elsif [HPXML::HVACTypeBoiler].include? heating_system.heating_system_type airloop_map[sys_id] = apply_boiler(model, runner, heating_system, sequential_heat_load_fracs, conditioned_zone, - @hvac_unavailable_periods) + hvac_unavailable_periods) elsif [HPXML::HVACTypeElectricResistance].include? heating_system.heating_system_type apply_electric_baseboard(model, heating_system, - sequential_heat_load_fracs, conditioned_zone, @hvac_unavailable_periods) + sequential_heat_load_fracs, conditioned_zone, hvac_unavailable_periods) elsif [HPXML::HVACTypeStove, HPXML::HVACTypeSpaceHeater, @@ -181,7 +172,7 @@ def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ HPXML::HVACTypeFireplace].include? heating_system.heating_system_type apply_unit_heater(model, heating_system, - sequential_heat_load_fracs, conditioned_zone, @hvac_unavailable_periods) + sequential_heat_load_fracs, conditioned_zone, hvac_unavailable_periods) end next unless heating_system.is_heat_pump_backup_system @@ -202,8 +193,12 @@ def self.apply_heating_system(runner, model, weather, spaces, hpxml_bldg, hpxml_ # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects + # @param hvac_days [TODO] TODO + # @param hvac_unavailable_periods [TODO] TODO + # @param remaining_load_fracs [TODO] TODO # @return [nil] - def self.apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map) + def self.apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map, + hvac_days, hvac_unavailable_periods, remaining_load_fracs) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get get_hpxml_hvac_systems(hpxml_bldg).each do |hvac_system| @@ -215,33 +210,33 @@ def self.apply_heat_pump(runner, model, weather, spaces, hpxml_bldg, hpxml_heade check_distribution_system(heat_pump.distribution_system, heat_pump.heat_pump_type) # Calculate heating sequential load fractions - sequential_heat_load_fracs = calc_sequential_load_fractions(heat_pump.fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= heat_pump.fraction_heat_load_served + sequential_heat_load_fracs = calc_sequential_load_fractions(heat_pump.fraction_heat_load_served, remaining_load_fracs[:htg], hvac_days[:htg]) + remaining_load_fracs[:htg] -= heat_pump.fraction_heat_load_served # Calculate cooling sequential load fractions - sequential_cool_load_fracs = calc_sequential_load_fractions(heat_pump.fraction_cool_load_served, @remaining_cool_load_frac, @cooling_days) - @remaining_cool_load_frac -= heat_pump.fraction_cool_load_served + sequential_cool_load_fracs = calc_sequential_load_fractions(heat_pump.fraction_cool_load_served, remaining_load_fracs[:clg], hvac_days[:clg]) + remaining_load_fracs[:clg] -= heat_pump.fraction_cool_load_served sys_id = heat_pump.id if [HPXML::HVACTypeHeatPumpWaterLoopToAir].include? heat_pump.heat_pump_type airloop_map[sys_id] = apply_water_loop_to_air_heat_pump(model, heat_pump, sequential_heat_load_fracs, sequential_cool_load_fracs, - conditioned_zone, @hvac_unavailable_periods) + conditioned_zone, hvac_unavailable_periods) elsif [HPXML::HVACTypeHeatPumpAirToAir, HPXML::HVACTypeHeatPumpMiniSplit, HPXML::HVACTypeHeatPumpPTHP, HPXML::HVACTypeHeatPumpRoom].include? heat_pump.heat_pump_type airloop_map[sys_id] = apply_air_source_hvac_systems(model, runner, heat_pump, heat_pump, sequential_cool_load_fracs, sequential_heat_load_fracs, weather.data.AnnualMaxDrybulb, weather.data.AnnualMinDrybulb, - conditioned_zone, @hvac_unavailable_periods, schedules_file, hpxml_bldg, + conditioned_zone, hvac_unavailable_periods, schedules_file, hpxml_bldg, hpxml_header) elsif [HPXML::HVACTypeHeatPumpGroundToAir].include? heat_pump.heat_pump_type airloop_map[sys_id] = apply_ground_to_air_heat_pump(model, runner, weather, heat_pump, sequential_heat_load_fracs, sequential_cool_load_fracs, conditioned_zone, hpxml_bldg.site.ground_conductivity, hpxml_bldg.site.ground_diffusivity, - @hvac_unavailable_periods, hpxml_bldg.building_construction.number_of_units) + hvac_unavailable_periods, hpxml_bldg.building_construction.number_of_units) end @@ -1099,8 +1094,12 @@ def self.apply_unit_heater(model, heating_system, sequential_heat_load_fracs, co # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) + # @param hvac_days [TODO] TODO + # @param hvac_unavailable_periods [TODO] TODO + # @param remaining_load_fracs [TODO] TODO # @return [nil] - def self.apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header) + def self.apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header, + hvac_days, hvac_unavailable_periods, remaining_load_fracs) conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get if hpxml_header.apply_ashrae140_assumptions && (hpxml_bldg.total_fraction_heat_load_served + hpxml_bldg.total_fraction_heat_load_served == 0.0) @@ -1116,27 +1115,27 @@ def self.apply_ideal_air_system(model, weather, spaces, hpxml_bldg, hpxml_header end end apply_ideal_air_loads(model, [cooling_load_frac], [heating_load_frac], - conditioned_zone, @hvac_unavailable_periods) + conditioned_zone, hvac_unavailable_periods) return end if (hpxml_bldg.total_fraction_heat_load_served < 1.0) && (hpxml_bldg.total_fraction_heat_load_served > 0.0) - sequential_heat_load_fracs = calc_sequential_load_fractions(@remaining_heat_load_frac - hpxml_bldg.total_fraction_heat_load_served, @remaining_heat_load_frac, @heating_days) - @remaining_heat_load_frac -= (1.0 - hpxml_bldg.total_fraction_heat_load_served) + sequential_heat_load_fracs = calc_sequential_load_fractions(remaining_load_fracs[:htg] - hpxml_bldg.total_fraction_heat_load_served, remaining_load_fracs[:htg], hvac_days[:htg]) + remaining_load_fracs[:htg] -= (1.0 - hpxml_bldg.total_fraction_heat_load_served) else sequential_heat_load_fracs = [0.0] end if (hpxml_bldg.total_fraction_cool_load_served < 1.0) && (hpxml_bldg.total_fraction_cool_load_served > 0.0) - sequential_cool_load_fracs = calc_sequential_load_fractions(@remaining_cool_load_frac - hpxml_bldg.total_fraction_cool_load_served, @remaining_cool_load_frac, @cooling_days) - @remaining_cool_load_frac -= (1.0 - hpxml_bldg.total_fraction_cool_load_served) + sequential_cool_load_fracs = calc_sequential_load_fractions(remaining_load_fracs[:clg] - hpxml_bldg.total_fraction_cool_load_served, remaining_load_fracs[:clg], hvac_days[:clg]) + remaining_load_fracs[:clg] -= (1.0 - hpxml_bldg.total_fraction_cool_load_served) else sequential_cool_load_fracs = [0.0] end if (sequential_heat_load_fracs.sum > 0.0) || (sequential_cool_load_fracs.sum > 0.0) apply_ideal_air_loads(model, sequential_cool_load_fracs, sequential_heat_load_fracs, - conditioned_zone, @hvac_unavailable_periods) + conditioned_zone, hvac_unavailable_periods) end end @@ -1343,14 +1342,32 @@ def self.apply_ceiling_fans(runner, model, spaces, weather, hpxml_bldg, hpxml_he # @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file) # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files - # @return [nil] + # @param hvac_days [TODO] TODO + # @return [TODO] TODO def self.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_header, schedules_file) - return if hpxml_bldg.hvac_controls.size == 0 + return {} if hpxml_bldg.hvac_controls.size == 0 hvac_control = hpxml_bldg.hvac_controls[0] conditioned_zone = spaces[HPXML::LocationConditionedSpace].thermalZone.get has_ceiling_fan = (hpxml_bldg.ceiling_fans.size > 0) + # Set 365 (or 366 for a leap year) heating/cooling day arrays based on heating/cooling + # season begin/end month/day, respectively. + htg_start_month = hvac_control.seasons_heating_begin_month + htg_start_day = hvac_control.seasons_heating_begin_day + htg_end_month = hvac_control.seasons_heating_end_month + htg_end_day = hvac_control.seasons_heating_end_day + clg_start_month = hvac_control.seasons_cooling_begin_month + clg_start_day = hvac_control.seasons_cooling_begin_day + clg_end_month = hvac_control.seasons_cooling_end_month + clg_end_day = hvac_control.seasons_cooling_end_day + if (htg_start_month != 1) || (htg_start_day != 1) || (htg_end_month != 12) || (htg_end_day != 31) || (clg_start_month != 1) || (clg_start_day != 1) || (clg_end_month != 12) || (clg_end_day != 31) + runner.registerWarning('It is not possible to eliminate all HVAC energy use (e.g. crankcase/defrost energy) in EnergyPlus outside of an HVAC season.') + end + hvac_days = {} + hvac_days[:htg] = Calendar.get_daily_season(hpxml_header.sim_calendar_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day) + hvac_days[:clg] = Calendar.get_daily_season(hpxml_header.sim_calendar_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day) + heating_sch = nil cooling_sch = nil year = hpxml_header.sim_calendar_year @@ -1364,29 +1381,29 @@ def self.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_heade # permit mixing detailed schedules with simple schedules if heating_sch.nil? - htg_weekday_setpoints, htg_weekend_setpoints = get_heating_setpoints(hvac_control, year, onoff_thermostat_ddb) + htg_wd_setpoints, htg_we_setpoints = get_heating_setpoints(hvac_control, year, onoff_thermostat_ddb) else runner.registerWarning("Both '#{SchedulesFile::Columns[:HeatingSetpoint].name}' schedule file and heating setpoint temperature provided; the latter will be ignored.") if !hvac_control.heating_setpoint_temp.nil? end if cooling_sch.nil? - clg_weekday_setpoints, clg_weekend_setpoints = get_cooling_setpoints(hvac_control, has_ceiling_fan, year, weather, onoff_thermostat_ddb) + clg_wd_setpoints, clg_we_setpoints = get_cooling_setpoints(hvac_control, has_ceiling_fan, year, weather, onoff_thermostat_ddb) else runner.registerWarning("Both '#{SchedulesFile::Columns[:CoolingSetpoint].name}' schedule file and cooling setpoint temperature provided; the latter will be ignored.") if !hvac_control.cooling_setpoint_temp.nil? end # only deal with deadband issue if both schedules are simple if heating_sch.nil? && cooling_sch.nil? - htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints = create_setpoint_schedules(runner, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, year) + htg_wd_setpoints, htg_we_setpoints, clg_wd_setpoints, clg_we_setpoints = create_setpoint_schedules(runner, htg_wd_setpoints, htg_we_setpoints, clg_wd_setpoints, clg_we_setpoints, year, hvac_days) end if heating_sch.nil? - heating_setpoint = HourlyByDaySchedule.new(model, 'heating setpoint', htg_weekday_setpoints, htg_weekend_setpoints, nil, false) + heating_setpoint = HourlyByDaySchedule.new(model, 'heating setpoint', htg_wd_setpoints, htg_we_setpoints, nil, false) heating_sch = heating_setpoint.schedule end if cooling_sch.nil? - cooling_setpoint = HourlyByDaySchedule.new(model, 'cooling setpoint', clg_weekday_setpoints, clg_weekend_setpoints, nil, false) + cooling_setpoint = HourlyByDaySchedule.new(model, 'cooling setpoint', clg_wd_setpoints, clg_we_setpoints, nil, false) cooling_sch = cooling_setpoint.schedule end @@ -1397,18 +1414,22 @@ def self.apply_setpoints(model, runner, weather, spaces, hpxml_bldg, hpxml_heade thermostat_setpoint.setCoolingSetpointTemperatureSchedule(cooling_sch) thermostat_setpoint.setTemperatureDifferenceBetweenCutoutAndSetpoint(UnitConversions.convert(onoff_thermostat_ddb, 'deltaF', 'deltaC')) conditioned_zone.setThermostatSetpointDualSetpoint(thermostat_setpoint) + + return hvac_days end # TODO # # @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings - # @param htg_weekday_setpoints [TODO] TODO - # @param htg_weekend_setpoints [TODO] TODO - # @param clg_weekday_setpoints [TODO] TODO - # @param clg_weekend_setpoints [TODO] TODO + # @param htg_wd_setpoints [TODO] TODO + # @param htg_we_setpoints [TODO] TODO + # @param clg_wd_setpoints [TODO] TODO + # @param clg_we_setpoints [TODO] TODO # @param year [Integer] the calendar year + # @param hvac_days [TODO] TODO # @return [TODO] TODO - def self.create_setpoint_schedules(runner, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, year) + def self.create_setpoint_schedules(runner, htg_wd_setpoints, htg_we_setpoints, clg_wd_setpoints, clg_we_setpoints, year, + hvac_days) # Create setpoint schedules # This method ensures that we don't construct a setpoint schedule where the cooling setpoint # is less than the heating setpoint, which would result in an E+ error. @@ -1419,38 +1440,38 @@ def self.create_setpoint_schedules(runner, htg_weekday_setpoints, htg_weekend_se warning = false for i in 0..(Calendar.num_days_in_year(year) - 1) - if (@heating_days[i] == @cooling_days[i]) # both (or neither) heating/cooling seasons - htg_wkdy = htg_weekday_setpoints[i].zip(clg_weekday_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : h } - htg_wked = htg_weekend_setpoints[i].zip(clg_weekend_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : h } - clg_wkdy = htg_weekday_setpoints[i].zip(clg_weekday_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : c } - clg_wked = htg_weekend_setpoints[i].zip(clg_weekend_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : c } - elsif @heating_days[i] == 1 # heating only seasons; cooling has minimum of heating - htg_wkdy = htg_weekday_setpoints[i] - htg_wked = htg_weekend_setpoints[i] - clg_wkdy = htg_weekday_setpoints[i].zip(clg_weekday_setpoints[i]).map { |h, c| c < h ? h : c } - clg_wked = htg_weekend_setpoints[i].zip(clg_weekend_setpoints[i]).map { |h, c| c < h ? h : c } - elsif @cooling_days[i] == 1 # cooling only seasons; heating has maximum of cooling - htg_wkdy = clg_weekday_setpoints[i].zip(htg_weekday_setpoints[i]).map { |c, h| c < h ? c : h } - htg_wked = clg_weekend_setpoints[i].zip(htg_weekend_setpoints[i]).map { |c, h| c < h ? c : h } - clg_wkdy = clg_weekday_setpoints[i] - clg_wked = clg_weekend_setpoints[i] + if (hvac_days[:htg][i] == hvac_days[:clg][i]) # both (or neither) heating/cooling seasons + htg_wkdy = htg_wd_setpoints[i].zip(clg_wd_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : h } + htg_wked = htg_we_setpoints[i].zip(clg_we_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : h } + clg_wkdy = htg_wd_setpoints[i].zip(clg_wd_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : c } + clg_wked = htg_we_setpoints[i].zip(clg_we_setpoints[i]).map { |h, c| c < h ? (h + c) / 2.0 : c } + elsif hvac_days[:htg][i] == 1 # heating only seasons; cooling has minimum of heating + htg_wkdy = htg_wd_setpoints[i] + htg_wked = htg_we_setpoints[i] + clg_wkdy = htg_wd_setpoints[i].zip(clg_wd_setpoints[i]).map { |h, c| c < h ? h : c } + clg_wked = htg_we_setpoints[i].zip(clg_we_setpoints[i]).map { |h, c| c < h ? h : c } + elsif hvac_days[:clg][i] == 1 # cooling only seasons; heating has maximum of cooling + htg_wkdy = clg_wd_setpoints[i].zip(htg_wd_setpoints[i]).map { |c, h| c < h ? c : h } + htg_wked = clg_we_setpoints[i].zip(htg_we_setpoints[i]).map { |c, h| c < h ? c : h } + clg_wkdy = clg_wd_setpoints[i] + clg_wked = clg_we_setpoints[i] else fail 'HeatingSeason and CoolingSeason, when combined, must span the entire year.' end - if (htg_wkdy != htg_weekday_setpoints[i]) || (htg_wked != htg_weekend_setpoints[i]) || (clg_wkdy != clg_weekday_setpoints[i]) || (clg_wked != clg_weekend_setpoints[i]) + if (htg_wkdy != htg_wd_setpoints[i]) || (htg_wked != htg_we_setpoints[i]) || (clg_wkdy != clg_wd_setpoints[i]) || (clg_wked != clg_we_setpoints[i]) warning = true end - htg_weekday_setpoints[i] = htg_wkdy - htg_weekend_setpoints[i] = htg_wked - clg_weekday_setpoints[i] = clg_wkdy - clg_weekend_setpoints[i] = clg_wked + htg_wd_setpoints[i] = htg_wkdy + htg_we_setpoints[i] = htg_wked + clg_wd_setpoints[i] = clg_wkdy + clg_we_setpoints[i] = clg_wked end if warning runner.registerWarning('HVAC setpoints have been automatically adjusted to prevent periods where the heating setpoint is greater than the cooling setpoint.') end - return htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints + return htg_wd_setpoints, htg_we_setpoints, clg_wd_setpoints, clg_we_setpoints end # TODO @@ -1465,7 +1486,7 @@ def self.get_heating_setpoints(hvac_control, year, offset_db) if hvac_control.weekday_heating_setpoints.nil? || hvac_control.weekend_heating_setpoints.nil? # Base heating setpoint htg_setpoint = hvac_control.heating_setpoint_temp - htg_weekday_setpoints = [[htg_setpoint] * 24] * num_days + htg_wd_setpoints = [[htg_setpoint] * 24] * num_days # Apply heating setback? htg_setback = hvac_control.heating_setback_temp if not htg_setback.nil? @@ -1473,26 +1494,26 @@ def self.get_heating_setpoints(hvac_control, year, offset_db) htg_setback_start_hr = hvac_control.heating_setback_start_hour for d in 1..num_days for hr in htg_setback_start_hr..htg_setback_start_hr + Integer(htg_setback_hrs_per_week / 7.0) - 1 - htg_weekday_setpoints[d - 1][hr % 24] = htg_setback + htg_wd_setpoints[d - 1][hr % 24] = htg_setback end end end - htg_weekend_setpoints = htg_weekday_setpoints.dup + htg_we_setpoints = htg_wd_setpoints.dup else # 24-hr weekday/weekend heating setpoint schedules - htg_weekday_setpoints = hvac_control.weekday_heating_setpoints.split(',').map { |i| Float(i) } - htg_weekday_setpoints = [htg_weekday_setpoints] * num_days - htg_weekend_setpoints = hvac_control.weekend_heating_setpoints.split(',').map { |i| Float(i) } - htg_weekend_setpoints = [htg_weekend_setpoints] * num_days + htg_wd_setpoints = hvac_control.weekday_heating_setpoints.split(',').map { |i| Float(i) } + htg_wd_setpoints = [htg_wd_setpoints] * num_days + htg_we_setpoints = hvac_control.weekend_heating_setpoints.split(',').map { |i| Float(i) } + htg_we_setpoints = [htg_we_setpoints] * num_days end # Apply thermostat offset due to onoff control - htg_weekday_setpoints = htg_weekday_setpoints.map { |i| i.map { |j| j - offset_db / 2.0 } } - htg_weekend_setpoints = htg_weekend_setpoints.map { |i| i.map { |j| j - offset_db / 2.0 } } + htg_wd_setpoints = htg_wd_setpoints.map { |i| i.map { |j| j - offset_db / 2.0 } } + htg_we_setpoints = htg_we_setpoints.map { |i| i.map { |j| j - offset_db / 2.0 } } - htg_weekday_setpoints = htg_weekday_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } - htg_weekend_setpoints = htg_weekend_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } + htg_wd_setpoints = htg_wd_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } + htg_we_setpoints = htg_we_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } - return htg_weekday_setpoints, htg_weekend_setpoints + return htg_wd_setpoints, htg_we_setpoints end # TODO @@ -1509,7 +1530,7 @@ def self.get_cooling_setpoints(hvac_control, has_ceiling_fan, year, weather, off if hvac_control.weekday_cooling_setpoints.nil? || hvac_control.weekend_cooling_setpoints.nil? # Base cooling setpoint clg_setpoint = hvac_control.cooling_setpoint_temp - clg_weekday_setpoints = [[clg_setpoint] * 24] * num_days + clg_wd_setpoints = [[clg_setpoint] * 24] * num_days # Apply cooling setup? clg_setup = hvac_control.cooling_setup_temp if not clg_setup.nil? @@ -1517,17 +1538,17 @@ def self.get_cooling_setpoints(hvac_control, has_ceiling_fan, year, weather, off clg_setup_start_hr = hvac_control.cooling_setup_start_hour for d in 1..num_days for hr in clg_setup_start_hr..clg_setup_start_hr + Integer(clg_setup_hrs_per_week / 7.0) - 1 - clg_weekday_setpoints[d - 1][hr % 24] = clg_setup + clg_wd_setpoints[d - 1][hr % 24] = clg_setup end end end - clg_weekend_setpoints = clg_weekday_setpoints.dup + clg_we_setpoints = clg_wd_setpoints.dup else # 24-hr weekday/weekend cooling setpoint schedules - clg_weekday_setpoints = hvac_control.weekday_cooling_setpoints.split(',').map { |i| Float(i) } - clg_weekday_setpoints = [clg_weekday_setpoints] * num_days - clg_weekend_setpoints = hvac_control.weekend_cooling_setpoints.split(',').map { |i| Float(i) } - clg_weekend_setpoints = [clg_weekend_setpoints] * num_days + clg_wd_setpoints = hvac_control.weekday_cooling_setpoints.split(',').map { |i| Float(i) } + clg_wd_setpoints = [clg_wd_setpoints] * num_days + clg_we_setpoints = hvac_control.weekend_cooling_setpoints.split(',').map { |i| Float(i) } + clg_we_setpoints = [clg_we_setpoints] * num_days end # Apply cooling setpoint offset due to ceiling fan? if has_ceiling_fan @@ -1537,19 +1558,19 @@ def self.get_cooling_setpoints(hvac_control, has_ceiling_fan, year, weather, off Calendar.months_to_days(year, months).each_with_index do |operation, d| next if operation != 1 - clg_weekday_setpoints[d] = [clg_weekday_setpoints[d], Array.new(24, clg_ceiling_fan_offset)].transpose.map { |i| i.sum } - clg_weekend_setpoints[d] = [clg_weekend_setpoints[d], Array.new(24, clg_ceiling_fan_offset)].transpose.map { |i| i.sum } + clg_wd_setpoints[d] = [clg_wd_setpoints[d], Array.new(24, clg_ceiling_fan_offset)].transpose.map { |i| i.sum } + clg_we_setpoints[d] = [clg_we_setpoints[d], Array.new(24, clg_ceiling_fan_offset)].transpose.map { |i| i.sum } end end end # Apply thermostat offset due to onoff control - clg_weekday_setpoints = clg_weekday_setpoints.map { |i| i.map { |j| j + offset_db / 2.0 } } - clg_weekend_setpoints = clg_weekend_setpoints.map { |i| i.map { |j| j + offset_db / 2.0 } } - clg_weekday_setpoints = clg_weekday_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } - clg_weekend_setpoints = clg_weekend_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } + clg_wd_setpoints = clg_wd_setpoints.map { |i| i.map { |j| j + offset_db / 2.0 } } + clg_we_setpoints = clg_we_setpoints.map { |i| i.map { |j| j + offset_db / 2.0 } } + clg_wd_setpoints = clg_wd_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } + clg_we_setpoints = clg_we_setpoints.map { |i| i.map { |j| UnitConversions.convert(j, 'F', 'C') } } - return clg_weekday_setpoints, clg_weekend_setpoints + return clg_wd_setpoints, clg_we_setpoints end # TODO @@ -1559,20 +1580,20 @@ def self.get_cooling_setpoints(hvac_control, has_ceiling_fan, year, weather, off # @return [TODO] TODO def self.get_default_heating_setpoint(control_type, eri_version) # Per ANSI/RESNET/ICC 301 - htg_weekday_setpoints = '68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68' - htg_weekend_setpoints = '68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68' + htg_wd_setpoints = '68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68' + htg_we_setpoints = '68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68' if control_type == HPXML::HVACControlTypeProgrammable if Constants::ERIVersions.index(eri_version) >= Constants::ERIVersions.index('2022') - htg_weekday_setpoints = '66, 66, 66, 66, 66, 67, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' - htg_weekend_setpoints = '66, 66, 66, 66, 66, 67, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' + htg_wd_setpoints = '66, 66, 66, 66, 66, 67, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' + htg_we_setpoints = '66, 66, 66, 66, 66, 67, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' else - htg_weekday_setpoints = '66, 66, 66, 66, 66, 66, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' - htg_weekend_setpoints = '66, 66, 66, 66, 66, 66, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' + htg_wd_setpoints = '66, 66, 66, 66, 66, 66, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' + htg_we_setpoints = '66, 66, 66, 66, 66, 66, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 66' end elsif control_type != HPXML::HVACControlTypeManual fail "Unexpected control type #{control_type}." end - return htg_weekday_setpoints, htg_weekend_setpoints + return htg_wd_setpoints, htg_we_setpoints end # TODO @@ -1582,20 +1603,20 @@ def self.get_default_heating_setpoint(control_type, eri_version) # @return [TODO] TODO def self.get_default_cooling_setpoint(control_type, eri_version) # Per ANSI/RESNET/ICC 301 - clg_weekday_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78' - clg_weekend_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78' + clg_wd_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78' + clg_we_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78' if control_type == HPXML::HVACControlTypeProgrammable if Constants::ERIVersions.index(eri_version) >= Constants::ERIVersions.index('2022') - clg_weekday_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 79, 78, 78, 78, 78, 78, 78, 78, 78, 78' - clg_weekend_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 79, 78, 78, 78, 78, 78, 78, 78, 78, 78' + clg_wd_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 79, 78, 78, 78, 78, 78, 78, 78, 78, 78' + clg_we_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 79, 78, 78, 78, 78, 78, 78, 78, 78, 78' else - clg_weekday_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 80, 78, 78, 78, 78, 78, 78, 78, 78, 78' - clg_weekend_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 80, 78, 78, 78, 78, 78, 78, 78, 78, 78' + clg_wd_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 80, 78, 78, 78, 78, 78, 78, 78, 78, 78' + clg_we_setpoints = '78, 78, 78, 78, 78, 78, 78, 78, 78, 80, 80, 80, 80, 80, 80, 78, 78, 78, 78, 78, 78, 78, 78, 78' end elsif control_type != HPXML::HVACControlTypeManual fail "Unexpected control type #{control_type}." end - return clg_weekday_setpoints, clg_weekend_setpoints + return clg_wd_setpoints, clg_we_setpoints end # TODO @@ -4595,13 +4616,13 @@ def self.set_gshp_assumptions(heat_pump, weather) # Returns the EnergyPlus sequential load fractions for every day of the year. # - # @param load_fraction [TODO] TODO - # @param remaining_fraction [TODO] TODO + # @param load_frac [TODO] TODO + # @param remaining_load_frac [TODO] TODO # @param availability_days [TODO] TODO # @return [TODO] TODO - def self.calc_sequential_load_fractions(load_fraction, remaining_fraction, availability_days) - if remaining_fraction > 0 - sequential_load_frac = load_fraction / remaining_fraction # Fraction of remaining load served by this system + def self.calc_sequential_load_fractions(load_frac, remaining_load_frac, availability_days) + if remaining_load_frac > 0 + sequential_load_frac = load_frac / remaining_load_frac # Fraction of remaining load served by this system else sequential_load_frac = 0.0 end From 9896e0e2f4c07e006284150dc0149b86809fbf83 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 17 Sep 2024 04:31:45 +0000 Subject: [PATCH 16/16] Latest results. --- workflow/tests/base_results/results_simulations_bills.csv | 8 ++++---- .../tests/base_results/results_simulations_energy.csv | 8 ++++---- workflow/tests/base_results/results_simulations_loads.csv | 6 +++--- workflow/tests/base_results/results_simulations_misc.csv | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/workflow/tests/base_results/results_simulations_bills.csv b/workflow/tests/base_results/results_simulations_bills.csv index 6bb033b2ec..a055b949c6 100644 --- a/workflow/tests/base_results/results_simulations_bills.csv +++ b/workflow/tests/base_results/results_simulations_bills.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,1842.17,144.0,1298.15,0.0,1442.15,144.0,256.02,400.02, base-battery-scheduled-power-outage.xml,1771.64,144.0,1234.11,0.0,1378.11,144.0,249.53,393.53,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-battery-scheduled.xml,1903.62,144.0,1366.31,0.0,1510.31,144.0,249.31,393.31,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-battery.xml,1840.48,144.0,1303.17,0.0,1447.17,144.0,249.31,393.31,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,1276.91,144.0,869.71,0.0,1013.71,144.0,119.2,263.2,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,1303.15,144.0,879.44,0.0,1023.44,144.0,135.71,279.71,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,962.69,144.0,818.69,0.0,962.69,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -base-bldgtype-mf-unit-adjacent-to-multiple.xml,1268.26,144.0,904.84,0.0,1048.84,144.0,75.42,219.42,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +base-bldgtype-mf-unit-adjacent-to-multiple.xml,1276.77,144.0,911.65,0.0,1055.65,144.0,77.12,221.12,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,1436.25,144.0,871.62,0.0,1015.62,144.0,276.63,420.63,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1182.34,144.0,874.77,0.0,1018.77,144.0,19.57,163.57,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1191.78,144.0,884.21,0.0,1028.21,144.0,19.57,163.57,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,1212.56,144.0,907.58,0.0,1051.58,144.0,16.98,160.98,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-infil-compartmentalization-test.xml,1241.99,144.0,945.94,0.0,1089.94,144.0,8.05,152.05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, base-bldgtype-mf-unit-infil-leakiness-description.xml,1244.17,144.0,950.99,0.0,1094.99,144.0,5.18,149.18,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -306,7 +306,7 @@ base-hvac-mini-split-heat-pump-ductless-detailed-performance.xml,1566.96,144.0,1 base-hvac-mini-split-heat-pump-ductless-heating-capacity-17f.xml,1537.59,144.0,1393.59,0.0,1537.59,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-mini-split-heat-pump-ductless.xml,1537.59,144.0,1393.59,0.0,1537.59,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-multiple.xml,2524.05,144.0,1937.6,0.0,2081.6,144.0,81.1,225.1,0.0,110.65,110.65,0.0,106.7,106.7,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-hvac-none.xml,2608.49,144.0,2464.49,0.0,2608.49,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-hvac-none.xml,2608.55,144.0,2464.55,0.0,2608.55,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-electricity.xml,2022.13,144.0,1878.13,0.0,2022.13,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-natural-gas.xml,1763.47,144.0,1249.48,0.0,1393.48,144.0,225.99,369.99,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac.xml,1384.75,144.0,1240.75,0.0,1384.75,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 diff --git a/workflow/tests/base_results/results_simulations_energy.csv b/workflow/tests/base_results/results_simulations_energy.csv index 0605256411..79431b1979 100644 --- a/workflow/tests/base_results/results_simulations_energy.csv +++ b/workflow/tests/base_results/results_simulations_energy.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,60.121,60.121,35.664,35.664,24.457,0.0,0.0,0.0,0.0,0.0 base-battery-scheduled-power-outage.xml,57.742,57.742,33.905,33.905,23.837,0.0,0.0,0.0,0.0,0.0,0.0,0.591,0.0,0.0,3.406,0.484,8.401,0.0,0.0,4.2,0.0,0.311,0.0,0.0,0.0,0.0,1.882,0.0,0.0,0.292,0.335,1.386,1.401,0.0,1.94,7.686,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.587,23.837,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-battery-scheduled.xml,61.353,61.353,37.537,37.537,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.591,0.0,0.0,4.398,0.662,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.735,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-battery.xml,59.618,59.618,35.802,35.802,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.591,0.0,0.0,4.398,0.662,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,23.816,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,35.281,35.281,23.894,23.894,11.387,0.0,0.0,0.0,0.0,0.0,0.0,0.126,0.0,0.0,1.414,0.124,9.67,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.694,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,11.387,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,37.125,37.125,24.161,24.161,12.964,0.0,0.0,0.0,0.0,0.0,0.0,0.143,0.0,0.0,1.628,0.163,9.672,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.688,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,12.964,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,22.492,22.492,22.492,22.492,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,9.564,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.061,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multiple.xml,32.063,32.063,24.859,24.859,7.205,0.0,0.0,0.0,0.0,0.0,0.0,0.079,0.0,0.0,2.039,0.233,9.558,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.082,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.205,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-bldgtype-mf-unit-adjacent-to-multiple.xml,32.413,32.413,25.046,25.046,7.367,0.0,0.0,0.0,0.0,0.0,0.0,0.081,0.0,0.0,2.195,0.262,9.558,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.083,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.367,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,50.372,50.372,23.946,23.946,26.426,0.0,0.0,0.0,0.0,0.0,0.0,0.291,0.0,0.0,1.495,0.139,9.755,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.399,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,26.426,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,25.902,25.902,24.032,24.032,1.869,0.0,0.0,0.0,0.0,0.0,0.0,0.021,0.0,0.0,1.452,0.131,9.59,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,1.972,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.869,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,26.161,26.161,24.292,24.292,1.87,0.0,0.0,0.0,0.0,0.0,0.0,0.021,0.0,0.0,1.655,0.169,9.582,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.0,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.87,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,26.556,26.556,24.934,24.934,1.622,0.0,0.0,0.0,0.0,0.0,0.0,0.018,0.0,0.0,2.11,0.256,9.541,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.142,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.622,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-infil-compartmentalization-test.xml,26.757,26.757,25.988,25.988,0.769,0.0,0.0,0.0,0.0,0.0,0.0,0.008,0.0,0.0,2.973,0.423,9.526,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.191,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.769,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-bldgtype-mf-unit-infil-leakiness-description.xml,26.621,26.621,26.127,26.127,0.494,0.0,0.0,0.0,0.0,0.0,0.0,0.005,0.0,0.0,3.083,0.446,9.523,0.0,0.0,2.025,0.0,0.206,0.0,0.0,0.0,0.0,2.203,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,2.795,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.494,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 @@ -306,7 +306,7 @@ base-hvac-mini-split-heat-pump-ductless-detailed-performance.xml,39.093,39.093,3 base-hvac-mini-split-heat-pump-ductless-heating-capacity-17f.xml,38.286,38.286,38.286,38.286,0.0,0.0,0.0,0.0,0.0,0.0,5.974,0.057,0.0,0.0,2.098,0.006,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-mini-split-heat-pump-ductless.xml,38.286,38.286,38.286,38.286,0.0,0.0,0.0,0.0,0.0,0.0,5.974,0.057,0.0,0.0,2.098,0.006,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-multiple.xml,68.904,68.904,53.232,53.232,7.747,3.92,4.005,0.0,0.0,0.0,14.851,1.067,0.322,0.019,6.363,0.46,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.07,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,7.747,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.92,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.005,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-hvac-none.xml,20.45,20.45,20.45,20.45,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.543,0.0,0.0,2.646,0.0,0.238,0.0,0.0,0.0,0.0,2.991,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,4.192,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +base-hvac-none.xml,20.451,20.451,20.451,20.451,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.543,0.0,0.0,2.646,0.0,0.238,0.0,0.0,0.0,0.0,2.991,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,4.192,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-electricity.xml,51.598,51.598,51.598,51.598,0.0,0.0,0.0,0.0,0.0,0.0,17.271,0.0,0.0,0.0,4.176,0.0,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac-with-heating-natural-gas.xml,55.916,55.916,34.327,34.327,21.589,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.176,0.0,9.014,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,2.072,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,21.589,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 base-hvac-ptac.xml,34.087,34.087,34.087,34.087,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,4.058,0.0,9.061,0.0,0.0,4.507,0.0,0.334,0.0,0.0,0.0,0.0,1.903,0.0,0.0,0.319,0.365,1.513,1.529,0.0,2.116,8.384,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 diff --git a/workflow/tests/base_results/results_simulations_loads.csv b/workflow/tests/base_results/results_simulations_loads.csv index ec17d6e88e..1de2a855db 100644 --- a/workflow/tests/base_results/results_simulations_loads.csv +++ b/workflow/tests/base_results/results_simulations_loads.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,23.106,0.0,12.576,9.071,0.804,0.0,0.0,0.0,4.166,3.889, base-battery-scheduled-power-outage.xml,22.523,0.0,10.138,8.445,0.582,0.0,0.0,0.0,3.818,3.882,0.545,7.57,0.682,10.754,-13.574,0.0,0.0,0.0,8.353,-0.113,5.216,0.0,0.841,0.0,5.327,-8.473,-2.662,0.0,0.009,-0.253,-0.023,2.661,0.018,-0.837,10.841,0.0,0.0,0.0,-6.39,-0.109,-0.914,-4.426,-0.121,0.0,2.268,5.99,1.538 base-battery-scheduled.xml,22.503,0.0,13.745,9.071,0.615,0.0,0.0,0.0,3.819,3.882,0.545,7.57,0.682,10.76,-13.574,0.0,0.0,0.0,8.363,-0.116,5.259,0.0,0.77,0.0,5.323,-8.475,-2.662,0.0,0.029,-0.188,-0.014,2.827,0.035,-0.632,10.839,0.0,0.0,0.0,-6.138,-0.112,-0.847,-3.884,-0.117,0.0,3.113,7.106,1.845 base-battery.xml,22.503,0.0,13.745,9.071,0.615,0.0,0.0,0.0,3.819,3.882,0.545,7.57,0.682,10.76,-13.574,0.0,0.0,0.0,8.363,-0.116,5.259,0.0,0.77,0.0,5.323,-8.475,-2.662,0.0,0.029,-0.188,-0.014,2.827,0.035,-0.632,10.839,0.0,0.0,0.0,-6.138,-0.112,-0.847,-3.884,-0.117,0.0,3.113,7.106,1.845 -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,10.598,0.0,2.224,9.37,0.729,0.0,0.0,0.0,2.782,3.858,0.0,0.0,0.604,1.403,-1.814,0.0,0.0,2.828,0.0,-0.045,1.646,0.0,0.0,0.0,4.543,-3.926,-1.307,0.0,-0.973,-0.132,0.0,0.0,-0.068,-0.035,1.089,0.0,0.0,-0.98,0.0,-0.042,-0.176,-0.312,0.0,0.0,0.458,2.691,0.718 +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,12.066,0.0,3.023,9.37,0.731,0.0,0.0,0.0,3.139,3.901,0.0,0.0,0.632,1.39,-1.81,0.0,0.0,3.183,0.0,-0.038,1.638,0.0,0.0,0.0,5.25,-3.938,-1.31,0.0,-0.688,-0.109,0.0,0.0,-0.048,-0.049,1.092,0.0,0.0,-0.692,0.0,-0.034,-0.182,-0.336,0.0,0.0,0.69,2.679,0.715 base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,0.0,0.0,0.0,9.37,0.618,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multiple.xml,6.709,0.0,4.541,9.37,0.612,0.0,0.0,0.0,-0.0,3.369,0.0,0.0,1.342,3.805,-4.269,0.0,0.0,4.496,0.0,-0.24,1.164,0.0,0.77,0.0,2.728,-5.413,-1.091,0.0,0.003,-0.493,0.0,0.0,-0.534,-0.158,3.936,0.0,0.0,-3.431,0.0,-0.235,-0.209,-1.06,-0.138,0.0,0.553,5.396,0.934 +base-bldgtype-mf-unit-adjacent-to-multiple.xml,6.86,0.0,5.135,9.37,0.612,0.0,0.0,0.0,-0.001,3.377,0.0,0.0,1.431,3.799,-4.284,0.0,0.0,4.539,0.0,-0.236,1.163,0.0,0.772,0.0,2.781,-5.429,-1.095,0.0,0.003,-0.481,0.0,0.0,-0.407,-0.186,3.921,0.0,0.0,-2.92,0.0,-0.231,-0.211,-1.105,-0.137,0.0,0.61,5.381,0.93 base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,24.602,0.0,2.53,9.37,0.818,0.0,0.0,0.0,5.728,4.447,0.0,0.0,0.845,1.334,-1.983,0.0,0.0,5.801,0.0,-0.071,1.651,0.0,0.0,0.0,12.544,-4.3,-1.415,0.0,-0.789,0.045,0.0,0.0,-0.041,0.017,0.92,0.0,0.0,-0.771,0.0,-0.068,-0.116,-0.245,0.0,0.0,0.653,2.317,0.61 -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1.738,0.0,2.366,9.37,0.645,0.0,0.0,0.0,0.268,3.38,0.0,0.0,0.404,1.446,-1.626,0.0,0.0,0.297,0.0,-0.068,1.638,0.0,0.0,0.0,0.54,-3.443,-1.153,0.0,-1.051,-0.345,0.0,0.0,-0.097,-0.145,1.276,0.0,0.0,-1.064,0.0,-0.065,-0.286,-0.362,0.0,0.0,0.484,3.174,0.872 +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,1.738,0.0,3.126,9.37,0.637,0.0,0.0,0.0,0.269,3.377,0.0,0.0,0.403,1.443,-1.627,0.0,0.0,0.298,0.0,-0.065,1.636,0.0,0.0,0.0,0.54,-3.44,-1.153,0.0,-0.796,-0.314,0.0,0.0,-0.079,-0.16,1.275,0.0,0.0,-0.803,0.0,-0.062,-0.292,-0.379,0.0,0.0,0.71,3.176,0.872 base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,1.508,0.0,4.938,9.37,0.594,0.0,0.0,0.0,-0.002,2.727,0.0,0.0,0.312,1.172,-1.175,0.0,0.0,-0.001,0.0,-0.099,1.341,0.0,0.0,0.0,0.473,-2.442,-0.833,0.0,-0.001,-1.062,0.0,0.0,-0.13,-0.6,1.728,0.0,0.0,0.0,0.0,-0.097,-0.738,-0.539,0.0,0.0,1.072,4.174,1.192 base-bldgtype-mf-unit-infil-compartmentalization-test.xml,0.713,0.0,8.513,9.37,0.579,0.0,0.0,0.0,-0.001,1.554,0.0,0.0,0.25,2.239,-1.533,0.0,0.0,0.007,0.0,-0.259,0.539,0.0,0.403,0.0,0.0,-2.138,-0.428,0.0,0.003,-2.108,0.0,0.0,-0.258,-2.871,6.67,0.0,0.0,0.012,0.0,-0.25,-0.731,-1.408,-0.647,0.0,0.0,8.747,1.598 base-bldgtype-mf-unit-infil-leakiness-description.xml,0.458,0.0,8.879,9.37,0.575,0.0,0.0,0.0,0.002,1.375,0.0,0.0,0.215,1.92,-1.32,0.0,0.0,0.011,0.0,-0.195,0.175,0.0,0.353,0.0,0.0,-1.792,-0.357,0.0,0.006,-2.345,0.0,0.0,-0.305,-3.282,6.882,0.0,0.0,0.016,0.0,-0.186,-0.306,-1.44,-0.714,0.0,0.0,9.101,1.668 diff --git a/workflow/tests/base_results/results_simulations_misc.csv b/workflow/tests/base_results/results_simulations_misc.csv index 0eb0731ce0..cbb1e09223 100644 --- a/workflow/tests/base_results/results_simulations_misc.csv +++ b/workflow/tests/base_results/results_simulations_misc.csv @@ -22,11 +22,11 @@ base-atticroof-vented.xml,0.0,0.0,1354.7,998.0,11171.5,2563.5,2094.8,3429.5,3429 base-battery-scheduled-power-outage.xml,0.0,5.0,1241.4,914.9,10291.7,2361.6,2082.8,6804.8,6804.8,23.713,21.153,1.339 base-battery-scheduled.xml,0.0,0.0,1354.7,998.0,11171.6,2563.5,2088.3,3823.9,3823.9,23.71,18.744,1.435 base-battery.xml,0.0,0.0,1354.7,998.0,11171.6,2563.5,2088.3,3823.9,3823.9,23.71,18.744,0.0 -base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1499.0,1923.6,1923.6,8.362,5.891,0.0 +base-bldgtype-mf-unit-adjacent-to-multifamily-buffer-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1502.3,1938.7,1938.7,8.367,6.536,0.0 base-bldgtype-mf-unit-adjacent-to-multiple-hvac-none.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1458.7,1399.8,1458.7,0.0,0.0,0.0 -base-bldgtype-mf-unit-adjacent-to-multiple.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1523.5,2220.7,2220.7,10.241,10.607,0.0 +base-bldgtype-mf-unit-adjacent-to-multiple.xml,0.0,2.0,1354.7,998.0,11171.5,3093.4,1523.5,2272.2,2272.2,10.242,10.719,0.0 base-bldgtype-mf-unit-adjacent-to-non-freezing-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1501.5,2244.1,2244.1,11.889,9.307,0.0 -base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1507.5,1920.9,1920.9,3.969,5.909,0.0 +base-bldgtype-mf-unit-adjacent-to-other-heated-space.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1510.0,1994.1,1994.1,3.969,6.537,0.0 base-bldgtype-mf-unit-adjacent-to-other-housing-unit.xml,0.0,0.0,1354.7,998.0,11171.6,3093.4,1510.4,1965.1,1965.1,4.282,4.96,0.0 base-bldgtype-mf-unit-infil-compartmentalization-test.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1610.2,2275.8,2275.8,3.654,7.722,0.0 base-bldgtype-mf-unit-infil-leakiness-description.xml,0.0,0.0,1354.7,998.0,11171.5,3093.4,1636.2,2159.9,2159.9,3.199,7.672,0.0