diff --git a/BuildResidentialHPXML/measure.rb b/BuildResidentialHPXML/measure.rb
index 12549ccc86..1d9836dad0 100644
--- a/BuildResidentialHPXML/measure.rb
+++ b/BuildResidentialHPXML/measure.rb
@@ -3500,7 +3500,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()
@@ -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)
@@ -6070,7 +6069,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 b56436bc72..a4f5998811 100644
--- a/BuildResidentialHPXML/measure.xml
+++ b/BuildResidentialHPXML/measure.xml
@@ -3,8 +3,8 @@
3.1
build_residential_hpxml
a13a8983-2b01-4930-8af2-42030b6e4233
- 75f26ae9-db88-4eb8-90c5-10ba2b9e7cd8
- 2024-09-14T01:35:49Z
+ 9bf8fac2-4f2e-4adf-9cda-80bc44e44283
+ 2024-09-17T00:18:31Z
2C38F48B
BuildResidentialHPXML
HPXML Builder
@@ -7441,7 +7441,7 @@
measure.rb
rb
script
- 310113A2
+ 58283E8D
constants.rb
@@ -7453,7 +7453,49 @@
geometry.rb
rb
resource
- C62D3E76
+ F80359E3
+
+
+ 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
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 e949558d0a..7d1b27cd02 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,151 +108,57 @@ def run(model, runner, user_arguments)
return false
end
- Geometry.tear_down_model(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(runner, args)
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'
-
- # 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 = {}
- 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])
- 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, eri_version, 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
- 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, args[:debug], schedules_file, eri_version, 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, args[:debug], schedules_file, eri_version, 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
- add_unit_model_to_model(model, hpxml_osm_map)
- 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)
+ Model.merge_unit_models(model, hpxml_osm_map)
end
- add_total_airflows_output(model, hpxml_osm_map)
- set_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)
-
- 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
+ # 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], 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
rescue Exception => e
runner.registerError("#{e.message}\n#{e.backtrace.join("\n")}")
return false
@@ -261,207 +167,130 @@ def run(model, runner, user_arguments)
return true
end
- # TODO
+ # Updates the args hash with final paths for various input/output files.
#
- # @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_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|
- shift_geometry(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
+ # @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
-
- 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)
+ 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
- # 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
+ if not (Pathname.new args[:output_dir]).absolute?
+ args[:output_dir] = File.expand_path(args[:output_dir])
end
- end
-
- # TODO
- #
- # @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
- 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'
+ 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])
- unit_model.getEnergyManagementSystemActuators.each do |actuator|
- ems_map[actuator.name.to_s] = make_variable_name(actuator.name, unit_number)
+ 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])
- 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
+ args[:hpxml_defaults_path] = File.join(args[:output_dir], 'in.xml')
+ end
- unit_model.getEnergyManagementSystemGlobalVariables.each do |global_variable|
- ems_map[global_variable.name.to_s] = make_variable_name(global_variable.name, unit_number)
+ # 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(runner, 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
- 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)
+ 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
-
- unit_model.getEnergyManagementSystemSubroutines.each do |subroutine|
- ems_map[subroutine.name.to_s] = make_variable_name(subroutine.name, unit_number)
+ hpxml.warnings.each do |warning|
+ runner.registerWarning(warning)
end
+ return hpxml
+ 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)
+ # 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
- line.gsub!("#{lhs}#{old_name}#{rhs}", "#{lhs}#{new_name}#{rhs}")
- end
- end
- end
- new_lines << line
- end
- program.setLines(new_lines)
+ fail 'Weather station EPW filepath has different values across dwelling units.'
end
- # All model objects
- unit_model.objects.each do |model_object|
- next if model_object.name.nil?
+ return epw_path, weather
+ end
- 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
+ # 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
-
- model_object.setName(make_variable_name(model_object.name, unit_number))
end
end
- # TODO
+ # Processes HPXML defaults, schedules, and emissions files upfront.
#
- # @param obj_name [TODO] TODO
- # @param unit_number [TODO] TODO
- # @return [TODO] TODO
- def make_variable_name(obj_name, unit_number)
- return "unit#{unit_number + 1}_#{obj_name}".gsub(' ', '_').gsub('-', '_')
+ # @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
@@ -473,21 +302,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 schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files
+ # @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
- @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
@@ -497,3043 +315,99 @@ 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)
- add_simulation_params(model)
-
- # Conditioned space/zone
- spaces = {}
- create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)
- set_foundation_and_walls_top()
- set_heating_and_cooling_seasons(runner)
- add_setpoints(runner, model, weather, 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)
- 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)
+ init(hpxml_bldg, hpxml.header)
+ SimControls.apply(model, hpxml.header)
+ Location.apply(model, weather, hpxml_bldg, hpxml.header, epw_path)
+
+ # 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, 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)
+ 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)
# 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_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)
-
- # Hot Water
- add_hot_water_and_appliances(runner, model, weather, spaces)
+ 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)
- # Plug Loads & Fuel Loads & Lighting
- add_mels(runner, model, spaces)
- add_mfls(runner, model, spaces)
- add_lighting(runner, model, spaces)
+ # Hot Water & Appliances
+ Waterheater.apply_dhw_appliances(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file)
- # Pools & Permanent Spas
- add_pools_and_permanent_spas(runner, model, spaces)
+ # Lighting
+ Lighting.apply(runner, model, spaces, hpxml_bldg, hpxml.header, 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)
- end
+ # 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)
- # TODO
- #
- # @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}."
- end
- next if scenario.elec_schedule_filepath.nil?
+ # Internal Gains
+ 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)
- scenario.elec_schedule_filepath = FilePath.check_path(scenario.elec_schedule_filepath,
- File.dirname(hpxml_path),
- 'Emissions File')
- end
- end
+ # Airflow (e.g., ducts, infiltration, ventilation)
+ Airflow.apply(runner, model, weather, spaces, hpxml_bldg, hpxml.header, schedules_file, airloop_map)
- # TODO
- #
- # @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),
- 'Schedules')
- }
+ # 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)
- # @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
- def set_inits_and_globals()
- # 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)
-
- # 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)
- # 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
-
- 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)
- end
- end
- end
-
- # TODO
- #
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @return [TODO] TODO
- def add_simulation_params(model)
- SimControls.apply(model, @hpxml_header)
- end
-
- # TODO
- #
- # @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
- def add_num_occupants(model, runner, 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
- end
-
- Geometry.apply_occupants(model, runner, @hpxml_bldg, num_occ, spaces[HPXML::LocationConditionedSpace],
- @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
- # @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)
- set_surface_interior(model, spaces, surface, roof)
- 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)
- 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)
- set_surface_interior(model, spaces, surface, wall)
- set_surface_exterior(model, spaces, surface, wall)
- if wall.is_interior
- surface.setSunExposure(EPlus::SurfaceSunExposureNo)
- surface.setWindExposure(EPlus::SurfaceWindExposureNo)
- end
- end
-
- next if surfaces.empty?
+ 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()
- # 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 @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)
+ # Make adjustments for modeling purposes
+ hpxml_bldg.collapse_enclosure_surfaces() # Speeds up simulation
+ hpxml_bldg.delete_adiabatic_subsurfaces() # EnergyPlus doesn't allow this
- 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)
+ # 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
- 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)
- set_surface_interior(model, spaces, surface, rim_joist)
- set_surface_exterior(model, spaces, surface, rim_joist)
- 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)
+ if hpxml_header.eri_calculation_version == 'latest'
+ hpxml_header.eri_calculation_version = Constants::ERIVersions[-1]
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)
- set_surface_interior(model, spaces, surface, floor)
- set_surface_exterior(model, spaces, surface, floor)
- 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)
+ # 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
- end
-
- # 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
- def add_foundation_walls_slabs(runner, model, weather, spaces)
- 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
- 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
- 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)
- set_surface_interior(model, spaces, surface, fnd_wall)
- set_surface_exterior(model, spaces, surface, fnd_wall)
- 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 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)
+ 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
- #
- # @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
- # @return [TODO] TODO
- 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
-
- 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 = 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)
- set_surface_interior(model, spaces, surface, foundation_wall)
- set_surface_exterior(model, spaces, surface, foundation_wall)
-
- 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
-
- # TODO
- #
- # @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 z_origin [TODO] TODO
- # @param exposed_length [TODO] TODO
- # @param kiva_foundation [TODO] TODO
- # @return [TODO] TODO
- 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')
- set_surface_interior(model, spaces, surface, slab)
- 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
-
- # TODO
- #
- # @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
- 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(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace))
- 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(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace))
- ceiling_surface.setOutsideBoundaryCondition(EPlus::BoundaryConditionAdiabatic)
- ceiling_surface.additionalProperties.setFeature('SurfaceType', 'InferredCeiling')
- ceiling_surface.additionalProperties.setFeature('Tilt', 0.0)
-
- # Apply Construction
- apply_adiabatic_construction(model, [floor_surface, ceiling_surface], 'floor')
- end
-
- # TODO
- #
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects
- # @return [TODO] TODO
- 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)
- set_surface_interior(model, spaces, surface, window.wall)
-
- 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)
-
- set_subsurface_exterior(surface, spaces, model, window.wall)
- 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)
- set_surface_interior(model, spaces, surface, window.wall)
-
- 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)
-
- set_subsurface_exterior(surface, spaces, model, window.wall)
- 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
-
- 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(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace))
- 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)
- set_surface_interior(model, spaces, surface, skylight.floor)
- set_surface_exterior(model, spaces, surface, skylight.floor)
- 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
-
- 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)
- set_surface_interior(model, spaces, surface, door.wall)
-
- 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)
-
- set_subsurface_exterior(surface, spaces, model, door.wall)
- 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
-
- apply_adiabatic_construction(model, surfaces, 'wall')
- end
-
- # TODO
- #
- # @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'
- 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
-
- # 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
- 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)
- end
- @hpxml_bldg.clothes_dryers.each do |clothes_dryer|
- clothes_dryer.additional_properties.space = 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)
- end
- @hpxml_bldg.refrigerators.each do |refrigerator|
- loc_space, loc_schedule = 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)
- 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)
- 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 = 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,
- @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)
- 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
-
- # 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
- 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]
-
- 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
-
- # 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
- 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]
-
- 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
-
- # 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
- 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]
-
- 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
-
- # 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
- 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
-
- # 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
- 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
-
- # TODO
- #
- # @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
- 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
-
- # TODO
- #
- # @param hvac_distribution [TODO] TODO
- # @param system_type [TODO] TODO
- # @return [TODO] TODO
- 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
-
- # TODO
- #
- # @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
- 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
-
- # TODO
- #
- # @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
- 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
-
- # TODO
- #
- # @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
- 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
- #
- # @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
- 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
-
- # 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
- 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 = 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
-
- # 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
-
- # TODO
- #
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @param hvac_distribution [TODO] TODO
- # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects
- # @return [TODO] TODO
- 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 = 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
-
- # TODO
- #
- # @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
-
- # TODO
- #
- # @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
-
- # TODO
- #
- # @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 = 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
-
- # TODO
- #
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @param unit_num [TODO] TODO
- # @return [TODO] TODO
- 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
-
- # TODO
- #
- # @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 [TODO] TODO
- # @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)
- 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
- # @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 = {}, {}, {}
- 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 [TODO] TODO
- 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
-
- # TODO
- #
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @return [TODO] TODO
- def set_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 [TODO] TODO
- def add_ems_debug_output(model)
- oems = model.getOutputEnergyManagementSystem
- oems.setActuatorAvailabilityDictionaryReporting('Verbose')
- 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 [TODO] TODO
- # @param hpxml_surface [TODO] TODO
- # @return [TODO] TODO
- 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))
- else
- surface.setSpace(create_or_get_space(model, spaces, interior_adjacent_to))
- end
- end
-
- # TODO
- #
- # @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
- 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(create_or_get_space(model, spaces, HPXML::LocationConditionedSpace)).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.additionalProperties.setFeature('SurfaceType', surface.additionalProperties.getFeatureAsString('SurfaceType').get)
- end
- end
-
- # TODO
- #
- # @param surface [TODO] TODO
- # @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
- 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
-
- # TODO
- #
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @param location [TODO] TODO
- # @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
-
- 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 [TODO] TODO
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects
- # @return [TODO] TODO
- 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 [TODO] TODO
- # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects
- # @return [TODO] TODO
- 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
-
- # TODO
- #
- # @param surface [TODO] TODO
- # @param spaces [Hash] Map of HPXML locations => OpenStudio Space objects
- # @param model [OpenStudio::Model::Model] OpenStudio Model object
- # @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)
- else
- set_surface_exterior(model, spaces, surface, hpxml_surface)
- 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_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 e5fbb25375..3356eb2fd7 100644
--- a/HPXMLtoOpenStudio/measure.xml
+++ b/HPXMLtoOpenStudio/measure.xml
@@ -3,8 +3,8 @@
3.1
hpxm_lto_openstudio
b1543b30-9465-45ff-ba04-1d1f85e763bc
- 73606eff-afec-47e9-b62e-d909a38b6d01
- 2024-09-14T03:13:06Z
+ bd97e343-382a-458f-8841-a4074e467691
+ 2024-09-17T03:41:38Z
D8922A73
HPXMLtoOpenStudio
HPXML to OpenStudio Translator
@@ -183,19 +183,19 @@
measure.rb
rb
script
- 8DB1CD62
+ AE303996
airflow.rb
rb
resource
- 8D4E35DF
+ FB90CDF2
battery.rb
rb
resource
- 520825A4
+ 01C783CF
calendar.rb
@@ -213,7 +213,7 @@
constructions.rb
rb
resource
- F2B9F3E6
+ 867600F4
data/Xing_okstate_0664D_13659_Table_A-3.csv
@@ -333,19 +333,19 @@
generator.rb
rb
resource
- A4B07257
+ B5787B92
geometry.rb
rb
resource
- D4DA7927
+ 095FE2B7
hotwater_appliances.rb
rb
resource
- 36092007
+ 12F8E8D4
hpxml.rb
@@ -357,7 +357,7 @@
hpxml_defaults.rb
rb
resource
- 834FF6AE
+ 4F6E5BAE
hpxml_schema/HPXML.xsd
@@ -387,25 +387,31 @@
hvac.rb
rb
resource
- 64AEF468
+ 13C7BF7E
hvac_sizing.rb
rb
resource
- BA28B74A
+ 9E3F6A59
+
+
+ internal_gains.rb
+ rb
+ resource
+ 29DFFEE4
lighting.rb
rb
resource
- 65744C8E
+ 327965F6
location.rb
rb
resource
- 2CB41E3C
+ BC2B3366
materials.rb
@@ -435,13 +441,19 @@
misc_loads.rb
rb
resource
- 8E650A1B
+ EF6685E4
+
+
+ model.rb
+ rb
+ resource
+ C620569D
output.rb
rb
resource
- 1AF3410C
+ 67DCE0D1
psychrometrics.rb
@@ -453,7 +465,7 @@
pv.rb
rb
resource
- 6ACFD5FF
+ C87470E8
schedule_files/battery.csv
@@ -579,7 +591,7 @@
schedules.rb
rb
resource
- E251B0F9
+ 19EC4B9B
simcontrols.rb
@@ -615,7 +627,7 @@
waterheater.rb
rb
resource
- 20E8AE0A
+ 51541A42
weather.rb
@@ -627,7 +639,7 @@
xmlhelper.rb
rb
resource
- 7CBAECF6
+ D1BB113E
xmlvalidator.rb
@@ -651,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 d128534b93..68edc887a8 100644
--- a/HPXMLtoOpenStudio/resources/airflow.rb
+++ b/HPXMLtoOpenStudio/resources/airflow.rb
@@ -8,44 +8,34 @@ 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 spaces [Hash] Map of HPXML locations => OpenStudio Space objects
# @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 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
- # @param hvac_availability_sensor [TODO] TODO
+ # @param airloop_map [Hash] Map of HPXML System ID => OpenStudio AirLoopHVAC (or ZoneHVACFourPipeFanCoil or ZoneHVACBaseboardConvectiveWater) objects
# @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(runner, model, weather, spaces, hpxml_bldg, hpxml_header, schedules_file, airloop_map)
# Global variables
-
@runner = runner
@spaces = spaces
@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
+ 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
@@ -72,6 +62,17 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, cfa,
@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 = []
@@ -113,6 +114,8 @@ def self.apply(model, runner, weather, spaces, hpxml_header, hpxml_bldg, cfa,
# 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
@@ -164,7 +167,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
@@ -879,7 +882,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)
@@ -976,6 +979,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
@@ -1138,7 +1188,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 +2344,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 +2395,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 +2442,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 +2456,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 +2564,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
@@ -2732,6 +2784,130 @@ 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] 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
+ 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
+ # @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
diff --git a/HPXMLtoOpenStudio/resources/battery.rb b/HPXMLtoOpenStudio/resources/battery.rb
index 2ff2a16938..ac807a24b0 100644
--- a/HPXMLtoOpenStudio/resources/battery.rb
+++ b/HPXMLtoOpenStudio/resources/battery.rb
@@ -1,7 +1,23 @@
# 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.
+ #
+ # @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(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/constructions.rb b/HPXMLtoOpenStudio/resources/constructions.rb
index c4ced8b388..6d02d4d2f2 100644
--- a/HPXMLtoOpenStudio/resources/constructions.rb
+++ b/HPXMLtoOpenStudio/resources/constructions.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
-# TODO
+# 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
@@ -25,24 +23,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 +116,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 +221,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 +318,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 +399,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 +490,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 +581,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 +675,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 +753,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 +844,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 +931,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 +1045,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 +1156,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 +1247,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 +1344,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 +1387,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?
@@ -1618,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
@@ -1628,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
@@ -2126,27 +1997,47 @@ def self.apply_kiva_settings(model, soil_k_in)
settings.setSimulationTimestep('Timestep')
end
- # TODO
+ # Sets Kiva foundation initial temperature.
#
# @param foundation [TODO] TODO
- # @param slab [TODO] TODO
# @param weather [WeatherFile] Weather object containing EPW information
- # @param conditioned_zone [TODO] TODO
- # @param sim_begin_month [TODO] TODO
- # @param sim_begin_day [TODO] TODO
- # @param sim_year [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 spaces [Hash] Map of HPXML locations => OpenStudio Space objects
# @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
+ # @param interior_adjacent_to [String] Interior adjacent to location (HPXML::LocationXXX)
+ # @return [nil]
+ 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]
+ 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
+
+ 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
+
# 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
@@ -2191,12 +2082,12 @@ def self.apply_kiva_initial_temp(foundation, slab, weather, conditioned_zone,
# 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
+ 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 slab.interior_adjacent_to == HPXML::LocationBasementUnconditioned
+ 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
@@ -2208,7 +2099,7 @@ def self.apply_kiva_initial_temp(foundation, slab, weather, conditioned_zone,
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 slab.interior_adjacent_to == HPXML::LocationCrawlspaceVented
+ elsif interior_adjacent_to == HPXML::LocationCrawlspaceVented
if foundation_ceiling_insulated
# Insulated ceiling: 90% outdoor, 10% indoor
outdoor_weight, indoor_weight = 0.9, 0.1
@@ -2220,7 +2111,7 @@ def self.apply_kiva_initial_temp(foundation, slab, weather, conditioned_zone,
outdoor_weight, indoor_weight = 0.5, 0.5
end
initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp
- elsif slab.interior_adjacent_to == HPXML::LocationCrawlspaceUnvented
+ elsif interior_adjacent_to == HPXML::LocationCrawlspaceUnvented
if foundation_ceiling_insulated
# Insulated ceiling: 85% outdoor, 15% indoor
outdoor_weight, indoor_weight = 0.85, 0.15
@@ -2232,10 +2123,10 @@ def self.apply_kiva_initial_temp(foundation, slab, weather, conditioned_zone,
outdoor_weight, indoor_weight = 0.4, 0.6
end
initial_temp = outdoor_temp * outdoor_weight + indoor_weight * indoor_temp
- elsif slab.interior_adjacent_to == HPXML::LocationGarage
+ elsif interior_adjacent_to == HPXML::LocationGarage
initial_temp = outdoor_temp + 11.0
else
- fail "Unhandled space: #{slab.interior_adjacent_to}"
+ fail "Unhandled space: #{interior_adjacent_to}"
end
end
@@ -2712,6 +2603,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/generator.rb b/HPXMLtoOpenStudio/resources/generator.rb
index b5be879640..16b1a65290 100644
--- a/HPXMLtoOpenStudio/resources/generator.rb
+++ b/HPXMLtoOpenStudio/resources/generator.rb
@@ -1,17 +1,31 @@
# 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(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 a6abbba192..1667b171f8 100644
--- a/HPXMLtoOpenStudio/resources/geometry.rb
+++ b/HPXMLtoOpenStudio/resources/geometry.rb
@@ -1,24 +1,1048 @@
# 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
- # Tear down the existing model if it exists.
+ # 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.tear_down_model(model:,
- runner:)
- handles = OpenStudio::UUIDVector.new
- model.objects.each do |obj|
- handles << obj.handle
+ 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
- if !handles.empty?
- runner.registerWarning('The model contains existing objects and is being reset.')
- model.removeObjects(handles)
+ 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 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, 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
+ 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, default_azimuths, schedules_file)
+ 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, 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, weather, spaces, hpxml_bldg, hpxml_header, slab, 0, remaining_exposed_length, nil, default_azimuths, schedules_file)
+ 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 default_azimuths [TODO] TODO
+ # @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files
+ # @return [nil]
+ 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
+ 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
+
+ Constructions.apply_kiva_initial_temperature(kiva_foundation, weather, hpxml_bldg, hpxml_header,
+ spaces, schedules_file, slab.interior_adjacent_to)
+
+ 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
@@ -86,18 +1110,15 @@ 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
+ # @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
- 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)
@@ -121,18 +1142,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
@@ -201,18 +1217,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
@@ -255,27 +1266,21 @@ 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:,
- 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
#
# @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:,
- 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')
@@ -302,15 +1307,16 @@ 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
- def self.set_zone_volumes(spaces:,
- hpxml_bldg:,
- apply_ashrae140_assumptions:)
+ # @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file)
+ # @return [nil]
+ 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)
@@ -320,7 +1326,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
@@ -332,7 +1338,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)
@@ -345,11 +1351,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')
@@ -392,7 +1395,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 = {}
@@ -486,73 +1489,42 @@ def self.explode_surfaces(model:,
end
end
- # TODO
+ # 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
-
- 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)
+ def self.shift_surfaces(model, unit_number)
+ y_shift = 200.0 * unit_number # meters
+
+ # 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
+
+ # 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)
+
+ 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
#
# @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')
@@ -568,10 +1540,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')
@@ -593,19 +1562,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 [TODO] TODO
- def self.add_neighbor_shading(model:,
- length:,
- hpxml_bldg:,
- walls_top:)
+ # @return [nil]
+ 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)
@@ -626,10 +1592,9 @@ def self.add_neighbor_shading(model:,
# TODO
#
# @param hpxml_bldg [HPXML::Building] HPXML Building object representing an individual dwelling unit
- # @param location [TODO] TODO
- # @return [TODO] TODO
- def self.calculate_zone_volume(hpxml_bldg:,
- location:)
+ # @param location [String] the location of interest (HPXML::LocationXXX)
+ # @return [Double] TODO
+ def self.calculate_zone_volume(hpxml_bldg, location)
if [HPXML::LocationBasementUnconditioned,
HPXML::LocationCrawlspaceUnvented,
HPXML::LocationCrawlspaceVented,
@@ -658,8 +1623,8 @@ def self.calculate_zone_volume(hpxml_bldg:,
# TODO
#
# @param location [String] the general HPXML location
- # @return [TODO] TODO
- def self.get_temperature_scheduled_space_values(location:)
+ # @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
return { temp_min: 68,
@@ -715,6 +1680,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)
+
+ 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
@@ -746,20 +1963,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".
@@ -767,9 +1970,38 @@ def self.get_occupancy_default_values()
# @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
+
+ # 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, 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 4bc17bb3e3..f361bb20bd 100644
--- a/HPXMLtoOpenStudio/resources/hotwater_appliances.rb
+++ b/HPXMLtoOpenStudio/resources/hotwater_appliances.rb
@@ -1,39 +1,31 @@
# 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 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 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)
-
+ 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
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
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?
@@ -64,7 +56,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
@@ -75,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
@@ -88,14 +81,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
@@ -107,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
@@ -121,7 +114,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)
@@ -129,7 +121,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
@@ -140,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
@@ -153,14 +146,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
@@ -171,12 +164,12 @@ 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?
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
@@ -194,7 +187,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)
@@ -202,7 +194,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
@@ -213,12 +206,12 @@ 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?
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
@@ -236,7 +229,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)
@@ -244,7 +236,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
@@ -256,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
@@ -270,12 +263,14 @@ 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)
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
@@ -336,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
@@ -350,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
@@ -388,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
@@ -458,38 +453,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
@@ -882,13 +845,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
@@ -910,10 +873,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)
@@ -926,21 +891,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")
@@ -967,42 +932,6 @@ def self.refrigerator_or_freezer_coefficients_schedule(model, col_name, obj_name
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
@@ -1309,18 +1238,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
@@ -1388,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 23dad1eb5f..8620343aeb 100644
--- a/HPXMLtoOpenStudio/resources/hpxml_defaults.rb
+++ b/HPXMLtoOpenStudio/resources/hpxml_defaults.rb
@@ -22,20 +22,27 @@ 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 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
unit_num = hpxml.buildings.index(hpxml_bldg) + 1
diff --git a/HPXMLtoOpenStudio/resources/hvac.rb b/HPXMLtoOpenStudio/resources/hvac.rb
index 5736b7f28c..04cdd65ab4 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
@@ -8,6 +8,248 @@ module HVAC
AirSourceCoolRatedIWB = 67.0 # degF, Rated indoor wetbulb for air-source systems, cooling
CrankcaseHeaterTemp = 50.0 # degF
+ # 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
+ # @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_load_fracs = { htg: 1.0, clg: 1.0 }
+ @hp_backup_system_object = nil
+ airloop_map = {}
+
+ if hpxml_bldg.hvac_controls.size == 0
+ return airloop_map
+ end
+
+ 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_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
+ 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
+ # @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,
+ 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|
+ 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_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_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_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
+
+ 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
+ # @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,
+ 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|
+ 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 = 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_load_fracs[:htg], hvac_days[:htg])
+ remaining_load_fracs[:htg] -= 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
+ # @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,
+ 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|
+ 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_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_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)
+ 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 +576,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 +779,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
@@ -785,10 +1025,8 @@ 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
- def self.apply_electric_baseboard(model, heating_system,
- sequential_heat_load_fracs, control_zone, hvac_unavailable_periods)
-
+ # @return [nil]
+ def self.apply_electric_baseboard(model, heating_system, sequential_heat_load_fracs, control_zone, hvac_unavailable_periods)
obj_name = Constants::ObjectTypeElectricBaseboard
# Baseboard
@@ -811,9 +1049,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 +1084,61 @@ 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)
+ # @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,
+ 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)
+ 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_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_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)
+ end
+ end
+
# TODO
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
@@ -887,16 +1178,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 +1249,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 +1270,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 +1301,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 +1324,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 +1333,41 @@ 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
+ # @param hvac_days [TODO] TODO
# @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)
+ 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)
+
+ # 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
@@ -1061,29 +1381,29 @@ def self.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone,
# 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, heating_days, cooling_days, 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
@@ -1094,20 +1414,22 @@ def self.apply_setpoints(model, runner, weather, hvac_control, conditioned_zone,
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 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 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, heating_days, cooling_days, 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.
@@ -1118,38 +1440,38 @@ 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
- 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
@@ -1164,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?
@@ -1172,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
@@ -1208,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?
@@ -1216,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
@@ -1236,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
@@ -1258,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
@@ -1281,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
@@ -2362,7 +2684,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)
@@ -4294,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
@@ -5202,7 +5524,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
@@ -5250,15 +5572,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
@@ -5351,4 +5672,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/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
new file mode 100644
index 0000000000..a6fafe4525
--- /dev/null
+++ b/HPXMLtoOpenStudio/resources/internal_gains.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+# Collection of methods related to internal gains.
+module InternalGains
+ # Create an OpenStudio People object using number of occupants and people/activity schedules.
+ #
+ # @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] 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)
+ 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()
+ 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, 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)
+ 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(spaces[HPXML::LocationConditionedSpace])
+ 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
+
+ # Gets the default values associated with occupant internal gains.
+ #
+ # @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
+ 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
+
+ # 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] 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)
+ 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 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
+ 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, 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
+ 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, 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
+
+ # 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 [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)
+ # 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
+ end
+end
diff --git a/HPXMLtoOpenStudio/resources/lighting.rb b/HPXMLtoOpenStudio/resources/lighting.rb
index d78447afb3..400eb80bd8 100644
--- a/HPXMLtoOpenStudio/resources/lighting.rb
+++ b/HPXMLtoOpenStudio/resources/lighting.rb
@@ -1,22 +1,23 @@
# frozen_string_literal: true
-# TODO
+# Collection of methods related to lighting.
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 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 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..cda083fdb8 100644
--- a/HPXMLtoOpenStudio/resources/misc_loads.rb
+++ b/HPXMLtoOpenStudio/resources/misc_loads.rb
@@ -1,19 +1,49 @@
# frozen_string_literal: true
-# TODO
+# Collection of methods related to miscellaneous plug/fuel loads.
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/model.rb b/HPXMLtoOpenStudio/resources/model.rb
new file mode 100644
index 0000000000..77e89cbac8
--- /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.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 = []
+ 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
diff --git a/HPXMLtoOpenStudio/resources/output.rb b/HPXMLtoOpenStudio/resources/output.rb
index 2281f77ad1..8a4a6dc3e9 100644
--- a/HPXMLtoOpenStudio/resources/output.rb
+++ b/HPXMLtoOpenStudio/resources/output.rb
@@ -158,6 +158,847 @@ 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] 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
+ 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 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/pv.rb b/HPXMLtoOpenStudio/resources/pv.rb
index 0fbf1942af..f25ed070af 100644
--- a/HPXMLtoOpenStudio/resources/pv.rb
+++ b/HPXMLtoOpenStudio/resources/pv.rb
@@ -1,17 +1,38 @@
# 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(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
diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb
index c057bbea7d..b5b742d7dc 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 [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)
+ 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.
diff --git a/HPXMLtoOpenStudio/resources/waterheater.rb b/HPXMLtoOpenStudio/resources/waterheater.rb
index 937706ad63..a9c36cbd36 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 = 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
+ 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