Class: NECB2011

Inherits:
Standard show all
Defined in:
lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb,
lib/openstudio-standards/standards/necb/NECB2011/autozone.rb,
lib/openstudio-standards/standards/necb/NECB2011/lighting.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_4.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb,
lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb,
lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_2_and_5.rb,
lib/openstudio-standards/standards/necb/NECB2011/beps_compliance_path.rb,
lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_multi_speed.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_single_speed.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb,
lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb

Overview

This class holds methods that apply NECB2011 rules.

Constant Summary

Constants inherited from Standard

Standard::STANDARDS_LIST

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Standard

#adjust_sizing_system, #afue_to_thermal_eff, #air_loop_hvac_add_motorized_oa_damper, #air_loop_hvac_adjust_minimum_vav_damper_positions, #air_loop_hvac_adjust_minimum_vav_damper_positions_outpatient, #air_loop_hvac_allowable_system_brake_horsepower, #air_loop_hvac_apply_baseline_fan_pressure_rise, #air_loop_hvac_apply_economizer_limits, #air_loop_hvac_apply_energy_recovery_ventilator_efficiency, #air_loop_hvac_apply_maximum_reheat_temperature, #air_loop_hvac_apply_minimum_vav_damper_positions, #air_loop_hvac_apply_prm_baseline_controls, #air_loop_hvac_apply_prm_baseline_economizer, #air_loop_hvac_apply_prm_baseline_fan_power, #air_loop_hvac_apply_prm_sizing_temperatures, #air_loop_hvac_apply_standard_controls, #air_loop_hvac_data_center_area_served, #air_loop_hvac_dcv_required_when_erv, #air_loop_hvac_demand_control_ventilation_limits, #air_loop_hvac_disable_multizone_vav_optimization, #air_loop_hvac_dx_cooling?, #air_loop_hvac_economizer?, #air_loop_hvac_economizer_limits, #air_loop_hvac_economizer_type_allowable?, #air_loop_hvac_enable_demand_control_ventilation, #air_loop_hvac_enable_multizone_vav_optimization, #air_loop_hvac_enable_optimum_start, #air_loop_hvac_enable_supply_air_temperature_reset_delta, #air_loop_hvac_enable_supply_air_temperature_reset_outdoor_temperature, #air_loop_hvac_enable_supply_air_temperature_reset_warmest_zone, #air_loop_hvac_energy_recovery?, #air_loop_hvac_energy_recovery_ventilator_flow_limit, #air_loop_hvac_energy_recovery_ventilator_heat_exchanger_type, #air_loop_hvac_energy_recovery_ventilator_type, #air_loop_hvac_fan_power_limitation_pressure_drop_adjustment_brake_horsepower, #air_loop_hvac_find_design_supply_air_flow_rate, #air_loop_hvac_floor_area_served, #air_loop_hvac_floor_area_served_exterior_zones, #air_loop_hvac_floor_area_served_interior_zones, #air_loop_hvac_get_occupancy_schedule, #air_loop_hvac_get_relief_fan_power, #air_loop_hvac_get_return_fan_power, #air_loop_hvac_get_supply_fan, #air_loop_hvac_get_supply_fan_power, #air_loop_hvac_has_parallel_piu_air_terminals?, #air_loop_hvac_has_simple_transfer_air?, #air_loop_hvac_humidifier_count, #air_loop_hvac_include_cooling_coil?, #air_loop_hvac_include_economizer?, #air_loop_hvac_include_evaporative_cooler?, #air_loop_hvac_include_hydronic_cooling_coil?, #air_loop_hvac_include_unitary_system?, #air_loop_hvac_include_wshp?, #air_loop_hvac_integrated_economizer_required?, #air_loop_hvac_minimum_zone_ventilation_efficiency, #air_loop_hvac_motorized_oa_damper_required?, #air_loop_hvac_multi_stage_dx_cooling?, #air_loop_hvac_multizone_vav_optimization_required?, #air_loop_hvac_multizone_vav_system?, #air_loop_hvac_optimum_start_required?, #air_loop_hvac_prm_baseline_economizer_required?, #air_loop_hvac_prm_economizer_type_and_limits, #air_loop_hvac_remove_erv, #air_loop_hvac_remove_motorized_oa_damper, #air_loop_hvac_residential_area_served, #air_loop_hvac_return_air_plenum, #air_loop_hvac_set_minimum_damper_position, #air_loop_hvac_set_vsd_curve_type, #air_loop_hvac_single_zone_controls_num_stages, #air_loop_hvac_standby_mode_occupancy_control, #air_loop_hvac_supply_air_temperature_reset_required?, #air_loop_hvac_supply_return_exhaust_relief_fans, #air_loop_hvac_system_fan_brake_horsepower, #air_loop_hvac_system_multiplier, #air_loop_hvac_terminal_reheat?, #air_loop_hvac_total_cooling_capacity, #air_loop_hvac_unitary_system?, #air_loop_hvac_unoccupied_fan_shutoff_required?, #air_loop_hvac_unoccupied_threshold, #air_loop_hvac_vav_damper_action, #air_loop_hvac_vav_system?, #air_terminal_single_duct_parallel_piu_reheat_apply_minimum_primary_airflow_fraction, #air_terminal_single_duct_parallel_piu_reheat_apply_prm_baseline_fan_power, #air_terminal_single_duct_parallel_piu_reheat_fan_on_flow_fraction, #air_terminal_single_duct_parallel_reheat_piu_minimum_primary_airflow_fraction, #air_terminal_single_duct_vav_reheat_apply_initial_prototype_damper_position, #air_terminal_single_duct_vav_reheat_apply_minimum_damper_position, #air_terminal_single_duct_vav_reheat_minimum_damper_position, #air_terminal_single_duct_vav_reheat_reheat_type, #apply_lighting_schedule, #apply_limit_to_subsurface_ratio, #boiler_get_eff_fplr, #boiler_hot_water_find_capacity, #boiler_hot_water_find_design_water_flow_rate, #boiler_hot_water_standard_minimum_thermal_efficiency, build, #chiller_electric_eir_find_capacity, #chiller_electric_eir_find_search_criteria, #chiller_electric_eir_get_cap_f_t_curve_name, #chiller_electric_eir_get_eir_f_plr_curve_name, #chiller_electric_eir_get_eir_f_t_curve_name, #chiller_electric_eir_standard_minimum_full_load_efficiency, #chw_sizing_control, #coil_cooling_dx_multi_speed_find_capacity, #coil_cooling_dx_multi_speed_standard_minimum_cop, #coil_cooling_dx_single_speed_apply_efficiency_and_curves, #coil_cooling_dx_single_speed_find_capacity, #coil_cooling_dx_single_speed_standard_minimum_cop, #coil_cooling_dx_two_speed_apply_efficiency_and_curves, #coil_cooling_dx_two_speed_find_capacity, #coil_cooling_dx_two_speed_standard_minimum_cop, #coil_cooling_water_to_air_heat_pump_apply_efficiency_and_curves, #coil_cooling_water_to_air_heat_pump_find_capacity, #coil_cooling_water_to_air_heat_pump_standard_minimum_cop, #coil_heating_dx_multi_speed_apply_efficiency_and_curves, #coil_heating_dx_single_speed_apply_defrost_eir_curve_limits, #coil_heating_dx_single_speed_apply_efficiency_and_curves, #coil_heating_dx_single_speed_standard_minimum_cop, #coil_heating_gas_additional_search_criteria, #coil_heating_gas_apply_prototype_efficiency, #coil_heating_gas_multi_stage_find_capacity, #coil_heating_gas_multi_stage_find_search_criteria, #coil_heating_water_to_air_heat_pump_apply_efficiency_and_curves, #coil_heating_water_to_air_heat_pump_find_capacity, #coil_heating_water_to_air_heat_pump_standard_minimum_cop, #combustion_eff_to_thermal_eff, #controller_water_coil_set_convergence_limits, #convert_curve_biquadratic, #cooling_tower_single_speed_apply_efficiency_and_curves, #cooling_tower_two_speed_apply_efficiency_and_curves, #cooling_tower_variable_speed_apply_efficiency_and_curves, #cop_heating_to_cop_heating_no_fan, #cop_no_fan_to_eer, #cop_no_fan_to_seer, #cop_to_eer, #cop_to_kw_per_ton, #cop_to_seer, #create_air_conditioner_variable_refrigerant_flow, #create_boiler_hot_water, #create_central_air_source_heat_pump, #create_coil_cooling_dx_single_speed, #create_coil_cooling_dx_two_speed, #create_coil_cooling_water, #create_coil_cooling_water_to_air_heat_pump_equation_fit, #create_coil_heating_dx_single_speed, #create_coil_heating_electric, #create_coil_heating_gas, #create_coil_heating_water, #create_coil_heating_water_to_air_heat_pump_equation_fit, #create_curve_bicubic, #create_curve_biquadratic, #create_curve_cubic, #create_curve_exponent, #create_curve_quadratic, #create_fan_constant_volume, #create_fan_constant_volume_from_json, #create_fan_on_off, #create_fan_on_off_from_json, #create_fan_variable_volume, #create_fan_variable_volume_from_json, #create_fan_zone_exhaust, #create_fan_zone_exhaust_from_json, #define_space_multiplier, #eer_to_cop, #eer_to_cop_no_fan, #ems_friendly_name, #enthalpy_recovery_ratio_design_to_typical_adjustment, #fan_constant_volume_airloop_fan_pressure_rise, #fan_on_off_airloop_or_unitary_fan_pressure_rise, #fan_on_off_apply_prototype_fan_pressure_rise, #fan_variable_volume_airloop_fan_pressure_rise, #fan_variable_volume_cooling_system_type, #fan_variable_volume_part_load_fan_power_limitation_capacity_limit, #fan_variable_volume_part_load_fan_power_limitation_hp_limit, #fan_variable_volume_set_control_type, #fan_zone_exhaust_apply_prototype_fan_pressure_rise, #find_exposed_conditioned_roof_surfaces, #find_exposed_conditioned_vertical_surfaces, #find_highest_roof_centre, #fluid_cooler_apply_minimum_power_per_flow, #get_avg_of_other_zones, #get_default_surface_cons_from_surface_type, #get_fan_object_for_airloop, #get_fan_schedule_for_each_zone, #get_group_heat_types, #get_outdoor_subsurface_ratio, #get_weekday_values_from_8760, #get_wtd_avg_of_other_zones, #headered_pumps_variable_speed_set_control_type, #heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency, #heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_efficiency_enthalpy_recovery_ratio, #heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_nominal_electric_power, #heat_exchanger_air_to_air_sensible_and_latent_enthalpy_recovery_ratio_to_effectiveness, #heat_exchanger_air_to_air_sensible_and_latent_minimum_effectiveness, #heat_exchanger_air_to_air_sensible_and_latent_prototype_default_fan_efficiency, #hspf_to_cop, #hspf_to_cop_no_fan, #interior_lighting_get_prm_data, #kw_per_ton_to_cop, #load_hvac_map, #load_initial_osm, #load_standards_database, #make_ruleset_sched_from_8760, #make_week_ruleset_sched_from_168, #model_add_baseboard, #model_add_booster_swh_end_uses, #model_add_cav, #model_add_central_air_source_heat_pump, #model_add_chw_loop, #model_add_construction, #model_add_construction_set, #model_add_crac, #model_add_crah, #model_add_curve, #model_add_cw_loop, #model_add_data_center_hvac, #model_add_data_center_load, #model_add_district_ambient_loop, #model_add_doas, #model_add_doas_cold_supply, #model_add_elevator, #model_add_elevators, #model_add_evap_cooler, #model_add_exhaust_fan, #model_add_four_pipe_fan_coil, #model_add_furnace_central_ac, #model_add_ground_hx_loop, #model_add_heatpump_water_heater, #model_add_high_temp_radiant, #model_add_hp_loop, #model_add_hvac_system, #model_add_hw_loop, #model_add_ideal_air_loads, #model_add_low_temp_radiant, #model_add_material, #model_add_minisplit_hp, #model_add_piping_losses_to_swh_system, #model_add_plant_supply_water_temperature_control, #model_add_prm_baseline_system, #model_add_prm_elevators, #model_add_psz_ac, #model_add_psz_vav, #model_add_ptac, #model_add_pthp, #model_add_pvav, #model_add_pvav_pfp_boxes, #model_add_radiant_basic_controls, #model_add_radiant_proportional_controls, #model_add_refrigeration_case, #model_add_refrigeration_compressor, #model_add_refrigeration_system, #model_add_refrigeration_walkin, #model_add_residential_erv, #model_add_residential_ventilator, #model_add_split_ac, #model_add_swh_booster, #model_add_swh_end_uses, #model_add_swh_end_uses_by_space, #model_add_swh_loop, #model_add_transformer, #model_add_typical_exterior_lights, #model_add_typical_refrigeration, #model_add_typical_swh, #model_add_unitheater, #model_add_vav_pfp_boxes, #model_add_vav_reheat, #model_add_vrf, #model_add_water_heater, #model_add_water_source_hp, #model_add_waterside_economizer, #model_add_window_ac, #model_add_zone_erv, #model_add_zone_heat_cool_request_count_program, #model_add_zone_ventilation, #model_apply_baseline_exterior_lighting, #model_apply_hvac_efficiency_standard, #model_apply_infiltration_standard, #model_apply_multizone_vav_outdoor_air_sizing, #model_apply_prm_baseline_sizing_schedule, #model_apply_prm_baseline_skylight_to_roof_ratio, #model_apply_prm_baseline_window_to_wall_ratio, #model_apply_prm_construction_types, #model_apply_prm_sizing_parameters, #model_apply_standard_constructions, #model_apply_standard_infiltration, #model_baseline_system_vav_fan_type, #model_create_exterior_lighting_area_length_count_hash, #model_create_multizone_fan_schedule, #model_create_prm_any_baseline_building, #model_create_prm_baseline_building, #model_create_prm_baseline_building_requires_proposed_model_sizing_run, #model_create_prm_baseline_building_requires_vlt_sizing_run, #model_create_prm_proposed_building, #model_create_prm_stable_baseline_building, #model_create_space_type_hash, #model_create_story_hash, #model_cw_loop_cooling_tower_fan_type, #model_differentiate_primary_secondary_thermal_zones, #model_effective_num_stories, #model_elevator_fan_pwr, #model_elevator_lift_power, #model_elevator_lighting_pct_incandescent, #model_eliminate_outlier_zones, #model_find_and_add_construction, #model_find_ashrae_hot_water_demand, #model_find_icc_iecc_2015_hot_water_demand, #model_find_icc_iecc_2015_internal_loads, #model_find_object, #model_find_objects, #model_find_prototype_floor_area, #model_find_target_eui, #model_find_target_eui_by_end_use, #model_find_water_heater_capacity_volume_and_parasitic, #model_get_baseline_system_type_by_zone, #model_get_building_properties, #model_get_climate_zone_set_from_list, #model_get_construction_properties, #model_get_construction_set, #model_get_district_heating_zones, #model_get_lookup_name, #model_get_or_add_ambient_water_loop, #model_get_or_add_chilled_water_loop, #model_get_or_add_ground_hx_loop, #model_get_or_add_heat_pump_loop, #model_get_or_add_hot_water_loop, #model_is_hvac_autosized, #model_legacy_results_by_end_use_and_fuel_type, #model_make_name, #model_prm_baseline_system_change_fuel_type, #model_prm_baseline_system_groups, #model_prm_baseline_system_number, #model_prm_baseline_system_type, #model_prm_skylight_to_roof_ratio_limit, #model_process_results_for_datapoint, #model_remap_office, #model_remove_external_shading_devices, #model_remove_prm_ems_objects, #model_remove_prm_hvac, #model_remove_unused_resource_objects, #model_set_vav_terminals_to_control_for_outdoor_air, #model_system_outdoor_air_sizing_vrp_method, #model_two_pipe_loop, #model_typical_display_case_zone, #model_typical_hvac_system_type, #model_typical_walkin_zone, #model_validate_standards_spacetypes_in_model, #model_ventilation_method, #model_walkin_freezer_latent_case_credit_curve, #model_zones_with_occ_and_fuel_type, #planar_surface_apply_standard_construction, #plant_loop_adiabatic_pipes_only, #plant_loop_apply_prm_baseline_chilled_water_pumping_type, #plant_loop_apply_prm_baseline_chilled_water_temperatures, #plant_loop_apply_prm_baseline_condenser_water_pumping_type, #plant_loop_apply_prm_baseline_condenser_water_temperatures, #plant_loop_apply_prm_baseline_hot_water_pumping_type, #plant_loop_apply_prm_baseline_hot_water_temperatures, #plant_loop_apply_prm_baseline_pump_power, #plant_loop_apply_prm_baseline_pumping_type, #plant_loop_apply_prm_baseline_temperatures, #plant_loop_apply_prm_number_of_boilers, #plant_loop_apply_prm_number_of_chillers, #plant_loop_apply_prm_number_of_cooling_towers, #plant_loop_apply_standard_controls, #plant_loop_capacity_w_by_maxflow_and_delta_t_forwater, #plant_loop_enable_supply_water_temperature_reset, #plant_loop_find_maximum_loop_flow_rate, #plant_loop_prm_baseline_condenser_water_temperatures, #plant_loop_set_chw_pri_sec_configuration, #plant_loop_supply_water_temperature_reset_required?, #plant_loop_swh_loop?, #plant_loop_swh_system_type, #plant_loop_total_cooling_capacity, #plant_loop_total_floor_area_served, #plant_loop_total_heating_capacity, #plant_loop_total_rated_w_per_gpm, #plant_loop_variable_flow_system?, #prototype_apply_condenser_water_temperatures, #prototype_condenser_water_temperatures, #pump_variable_speed_get_control_type, #pump_variable_speed_set_control_type, register_standard, #remove_air_loops, #remove_all_hvac, #remove_all_plant_loops, #remove_all_zone_equipment, #remove_hvac, #remove_plant_loops, #remove_unused_curves, #remove_vrf, #remove_zone_equipment, #rename_air_loop_nodes, #rename_plant_loop_nodes, #safe_load_model, #seer_to_cop, #seer_to_cop_no_fan, #set_maximum_fraction_outdoor_air_schedule, #space_add_daylighting_controls, #space_conditioning_category, #space_daylighted_area_window_width, #space_daylighted_areas, #space_daylighting_control_required?, #space_daylighting_fractions_and_windows, #space_get_equip_annual_array, #space_get_loads_for_all_equips, #space_infiltration_rate_75_pa, #space_internal_load_annual_array, #space_occupancy_annual_array, #space_remove_daylighting_controls, #space_set_baseline_daylighting_controls, #space_sidelighting_effective_aperture, #space_skylight_effective_aperture, #space_type_apply_int_loads_prm, #space_type_apply_internal_load_schedules, #space_type_apply_rendering_color, #space_type_get_construction_properties, #space_type_get_standards_data, #space_type_light_sch_change, #standard_design_sizing_temperatures, #standards_lookup_table_first, #standards_lookup_table_many, #strip_model, #sub_surface_create_centered_subsurface_from_scaled_surface, #sub_surface_create_scaled_subsurfaces_from_surface, #surface_adjust_fenestration_in_a_surface, #surface_subsurface_ua, #thermal_eff_to_afue, #thermal_eff_to_comb_eff, #thermal_zone_add_exhaust, #thermal_zone_add_exhaust_fan_dcv, #thermal_zone_apply_prm_baseline_supply_temperatures, #thermal_zone_conditioning_category, #thermal_zone_demand_control_ventilation_limits, #thermal_zone_exhaust_fan_dcv_required?, #thermal_zone_fossil_or_electric_type, #thermal_zone_get_annual_operating_hours, #thermal_zone_get_zone_fuels_for_occ_and_fuel_type, #thermal_zone_infer_system_type, #thermal_zone_occupancy_eflh, #thermal_zone_occupancy_type, #thermal_zone_peak_internal_load, #thermal_zone_prm_baseline_cooling_design_supply_temperature, #thermal_zone_prm_baseline_heating_design_supply_temperature, #thermal_zone_prm_lab_delta_t, #thermal_zone_prm_unitheater_design_supply_temperature, #true?, #validate_initial_model, #water_heater_convert_energy_factor_to_thermal_efficiency_and_ua, #water_heater_convert_uniform_energy_factor_to_energy_factor, #water_heater_determine_sub_type, #water_heater_mixed_additional_search_criteria, #water_heater_mixed_apply_prm_baseline_fuel_type, #water_heater_mixed_find_capacity, #water_heater_mixed_get_efficiency_requirement, #zone_hvac_component_apply_prm_baseline_fan_power, #zone_hvac_component_apply_standard_controls, #zone_hvac_component_apply_vestibule_heating_control, #zone_hvac_component_prm_baseline_fan_efficacy, #zone_hvac_component_vestibule_heating_control_required?, #zone_hvac_get_fan_object, #zone_hvac_model_standby_mode_occupancy_control, #zone_hvac_unoccupied_threshold

Methods included from PrototypeFan

apply_base_fan_variables, #create_fan_by_name, #get_fan_from_standards, #lookup_fan_curve_coefficients_from_json, #prototype_fan_apply_prototype_fan_efficiency

Methods included from CoilDX

#coil_dx_find_search_criteria, #coil_dx_heat_pump?, #coil_dx_subcategory

Methods included from CoolingTower

#cooling_tower_apply_minimum_power_per_flow, #cooling_tower_apply_minimum_power_per_flow_gpm_limit

Methods included from Pump

#pump_apply_prm_pressure_rise_and_motor_efficiency, #pump_apply_standard_minimum_motor_efficiency, #pump_brake_horsepower, #pump_motor_horsepower, #pump_pumppower, #pump_rated_w_per_gpm

Methods included from Fan

#fan_adjust_pressure_rise_to_meet_fan_power, #fan_apply_standard_minimum_motor_efficiency, #fan_brake_horsepower, #fan_change_impeller_efficiency, #fan_change_motor_efficiency, #fan_design_air_flow, #fan_fanpower, #fan_motor_horsepower, #fan_rated_w_per_cfm, #fan_small_fan?

Constructor Details

#initializeNECB2011

Returns a new instance of NECB2011.



134
135
136
137
138
139
140
141
142
143
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 134

def initialize
  super()
  @template = self.class.name
  @standards_data = load_standards_database_new
  corrupt_standards_database
  @tbd = nil
  # puts "loaded these tables..."
  # puts @standards_data.keys.size
  # raise("tables not all loaded in parent #{}") if @standards_data.keys.size < 24
end

Instance Attribute Details

#fuel_type_setObject

Returns the value of attribute fuel_type_set.



14
15
16
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 14

def fuel_type_set
  @fuel_type_set
end

#qaqc_dataObject

Returns the value of attribute qaqc_data.



2
3
4
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 2

def qaqc_data
  @qaqc_data
end

#space_multiplier_mapObject

Returns the value of attribute space_multiplier_map.



13
14
15
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 13

def space_multiplier_map
  @space_multiplier_map
end

#space_type_mapObject

Returns the value of attribute space_type_map.



12
13
14
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 12

def space_type_map
  @space_type_map
end

#standards_dataObject

Returns the value of attribute standards_data.



11
12
13
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 11

def standards_data
  @standards_data
end

#tbdObject (readonly)

Returns the value of attribute tbd.



9
10
11
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 9

def tbd
  @tbd
end

#templateObject (readonly)

Returns the value of attribute template.



10
11
12
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 10

def template
  @template
end

Instance Method Details

#add_all_spacetypes_to_model(model) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/openstudio-standards/standards/necb/NECB2011/beps_compliance_path.rb', line 2

def add_all_spacetypes_to_model(model)
  # Get the space Type data from @standards data
  spacetype_data = nil
  if @standards_data['space_types'].is_a?(Hash) == true
    spacetype_data = @standards_data['space_types']['table']
  else
    spacetype_data = @standards_data['space_types']
  end
  spacetype_data.each do |spacedata|
    space_type = OpenStudio::Model::SpaceType.new(model)
    space_type.setStandardsSpaceType(spacedata['space_type'])
    space_type.setStandardsBuildingType(spacedata['building_type'])
    space_type.setName("#{spacedata['building_type']} #{spacedata['space_type']}")
    # Loads
    space_type_apply_internal_loads(space_type: space_type)

    # Schedules
    space_type_apply_internal_load_schedules(space_type,
                                             true,
                                             true,
                                             true,
                                             true,
                                             true,
                                             true,
                                             true)
  end
end

#add_onespeed_DX_coil(model, always_on) ⇒ Object

Create a new DX cooling coil with NECB curve characteristics



1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1925

def add_onespeed_DX_coil(model, always_on)
  # clg_cap_f_of_temp = OpenStudio::Model::CurveBiquadratic.new(model)
  # clg_cap_f_of_temp = model_add_curve("DXCOOL-NECB2011-REF-CAPFT")
  clg_cap_f_of_temp = OpenStudio::Model::CurveBiquadratic.new(model)
  clg_cap_f_of_temp.setCoefficient1Constant(0.867905)
  clg_cap_f_of_temp.setCoefficient2x(0.0142459)
  clg_cap_f_of_temp.setCoefficient3xPOW2(0.000554364)
  clg_cap_f_of_temp.setCoefficient4y(-0.00755748)
  clg_cap_f_of_temp.setCoefficient5yPOW2(3.3048e-05)
  clg_cap_f_of_temp.setCoefficient6xTIMESY(-0.000191808)
  clg_cap_f_of_temp.setMinimumValueofx(13.0)
  clg_cap_f_of_temp.setMaximumValueofx(24.0)
  clg_cap_f_of_temp.setMinimumValueofy(24.0)
  clg_cap_f_of_temp.setMaximumValueofy(46.0)

  # clg_cap_f_of_flow = OpenStudio::Model::CurveQuadratic.new(model)
  clg_cap_f_of_flow = OpenStudio::Model::CurveQuadratic.new(model)
  clg_cap_f_of_flow.setCoefficient1Constant(1.0)
  clg_cap_f_of_flow.setCoefficient2x(0.0)
  clg_cap_f_of_flow.setCoefficient3xPOW2(0.0)
  clg_cap_f_of_flow.setMinimumValueofx(0.0)
  clg_cap_f_of_flow.setMaximumValueofx(1.0)

  # clg_energy_input_ratio_f_of_temp = = model_add_curve(""DXCOOL-NECB2011-REF-COOLEIRFT")
  # clg_energy_input_ratio_f_of_temp = OpenStudio::Model::CurveBiquadratic.new(model)
  clg_energy_input_ratio_f_of_temp = OpenStudio::Model::CurveBiquadratic.new(model)
  clg_energy_input_ratio_f_of_temp.setCoefficient1Constant(0.116936)
  clg_energy_input_ratio_f_of_temp.setCoefficient2x(0.0284933)
  clg_energy_input_ratio_f_of_temp.setCoefficient3xPOW2(-0.000411156)
  clg_energy_input_ratio_f_of_temp.setCoefficient4y(0.0214108)
  clg_energy_input_ratio_f_of_temp.setCoefficient5yPOW2(0.000161028)
  clg_energy_input_ratio_f_of_temp.setCoefficient6xTIMESY(-0.000679104)
  clg_energy_input_ratio_f_of_temp.setMinimumValueofx(13.0)
  clg_energy_input_ratio_f_of_temp.setMaximumValueofx(24.0)
  clg_energy_input_ratio_f_of_temp.setMinimumValueofy(24.0)
  clg_energy_input_ratio_f_of_temp.setMaximumValueofy(46.0)

  # clg_energy_input_ratio_f_of_flow = OpenStudio::Model::CurveQuadratic.new(model)
  # clg_energy_input_ratio_f_of_flow = = model_add_curve("DXCOOL-NECB2011-REF-CAPFFLOW")
  clg_energy_input_ratio_f_of_flow = OpenStudio::Model::CurveQuadratic.new(model)
  clg_energy_input_ratio_f_of_flow.setCoefficient1Constant(1.0)
  clg_energy_input_ratio_f_of_flow.setCoefficient2x(0.0)
  clg_energy_input_ratio_f_of_flow.setCoefficient3xPOW2(0.0)
  clg_energy_input_ratio_f_of_flow.setMinimumValueofx(0.0)
  clg_energy_input_ratio_f_of_flow.setMaximumValueofx(1.0)

  # NECB curve modified to take into account how PLF is used in E+, and PLF ranges (> 0.7)
  # clg_part_load_ratio = model_add_curve("DXCOOL-NECB2011-REF-COOLPLFFPLR")
  clg_part_load_ratio = OpenStudio::Model::CurveCubic.new(model)
  clg_part_load_ratio.setCoefficient1Constant(0.0277)
  clg_part_load_ratio.setCoefficient2x(4.9151)
  clg_part_load_ratio.setCoefficient3xPOW2(-8.184)
  clg_part_load_ratio.setCoefficient4xPOW3(4.2702)
  clg_part_load_ratio.setMinimumValueofx(0.7)
  clg_part_load_ratio.setMaximumValueofx(1.0)

  return OpenStudio::Model::CoilCoolingDXSingleSpeed.new(model,
                                                         always_on,
                                                         clg_cap_f_of_temp,
                                                         clg_cap_f_of_flow,
                                                         clg_energy_input_ratio_f_of_temp,
                                                         clg_energy_input_ratio_f_of_flow,
                                                         clg_part_load_ratio)
end

#add_onespeed_htg_DX_coil(model, sch) ⇒ Object



1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1990

def add_onespeed_htg_DX_coil(model, sch)


  htg_cap_f_of_temp = OpenStudio::Model::CurveCubic.new(model)
  htg_cap_f_of_temp.setCoefficient1Constant(0.729009)
  htg_cap_f_of_temp.setCoefficient2x(0.0319275)
  htg_cap_f_of_temp.setCoefficient3xPOW2(0.000136404)
  htg_cap_f_of_temp.setCoefficient4xPOW3(-8.748e-06)
  htg_cap_f_of_temp.setMinimumValueofx(-20.0)
  htg_cap_f_of_temp.setMaximumValueofx(20.0)

  htg_cap_f_of_flow = OpenStudio::Model::CurveCubic.new(model)
  htg_cap_f_of_flow.setCoefficient1Constant(0.84)
  htg_cap_f_of_flow.setCoefficient2x(0.16)
  htg_cap_f_of_flow.setCoefficient3xPOW2(0.0)
  htg_cap_f_of_flow.setCoefficient4xPOW3(0.0)
  htg_cap_f_of_flow.setMinimumValueofx(0.5)
  htg_cap_f_of_flow.setMaximumValueofx(1.5)

  htg_energy_input_ratio_f_of_temp = OpenStudio::Model::CurveCubic.new(model)
  htg_energy_input_ratio_f_of_temp.setCoefficient1Constant(1.2183)
  htg_energy_input_ratio_f_of_temp.setCoefficient2x(-0.03612)
  htg_energy_input_ratio_f_of_temp.setCoefficient3xPOW2(0.00142)
  htg_energy_input_ratio_f_of_temp.setCoefficient4xPOW3(-2.68e-05)
  htg_energy_input_ratio_f_of_temp.setMinimumValueofx(-20.0)
  htg_energy_input_ratio_f_of_temp.setMaximumValueofx(20.0)

  htg_energy_input_ratio_f_of_flow = OpenStudio::Model::CurveQuadratic.new(model)
  htg_energy_input_ratio_f_of_flow.setCoefficient1Constant(1.3824)
  htg_energy_input_ratio_f_of_flow.setCoefficient2x(-0.4336)
  htg_energy_input_ratio_f_of_flow.setCoefficient3xPOW2(0.0512)
  htg_energy_input_ratio_f_of_flow.setMinimumValueofx(0.0)
  htg_energy_input_ratio_f_of_flow.setMaximumValueofx(1.0)

  htg_part_load_ratio = OpenStudio::Model::CurveCubic.new(model)
  htg_part_load_ratio.setCoefficient1Constant(0.3696)
  htg_part_load_ratio.setCoefficient2x(2.3362)
  htg_part_load_ratio.setCoefficient3xPOW2(-2.9577)
  htg_part_load_ratio.setCoefficient4xPOW3(1.2596)
  htg_part_load_ratio.setMinimumValueofx(0.7)
  htg_part_load_ratio.setMaximumValueofx(1.0)

  dx_htg_coil = OpenStudio::Model::CoilHeatingDXSingleSpeed.new(model,
                                                                sch,
                                                                htg_cap_f_of_temp,
                                                                htg_cap_f_of_flow,
                                                                htg_energy_input_ratio_f_of_temp,
                                                                htg_energy_input_ratio_f_of_flow,
                                                                htg_part_load_ratio)
  dx_htg_coil.setMinimumOutdoorDryBulbTemperatureforCompressorOperation(-10)

  return dx_htg_coil
end

#add_ptac_dx_cooling(model, zone, zero_outdoor_air) ⇒ Object



2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2065

def add_ptac_dx_cooling(model, zone, zero_outdoor_air)
  # Create a PTAC for each zone:
  # PTAC DX Cooling with electric heating coil; electric heating coil is always off

  # TO DO: need to apply this system to space types:
  # (1) data processing area: control room, data centre
  # when cooling capacity <= 20kW and
  # (2) residential/accommodation: murb, hotel/motel guest room
  # when building/space heated only (this as per NECB; apply to
  # all for initial work? CAN-QUEST limitation)

  # TO DO: PTAC characteristics: sizing, fan schedules, temperature setpoints, interaction with MAU
  always_on = model.alwaysOnDiscreteSchedule
  always_off = BTAP::Resources::Schedules::StandardSchedules::ON_OFF.always_off(model)
  htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_off)

  # Set up PTAC DX coil with NECB performance curve characteristics;
  clg_coil = add_onespeed_DX_coil(model, always_on)

  # Set up PTAC constant volume supply fan
  fan = OpenStudio::Model::FanOnOff.new(model)
  fan.setPressureRise(640)

  # This method will seem like an error in number of args..but this is due to swig voodoo.
  ptac = OpenStudio::Model::ZoneHVACPackagedTerminalAirConditioner.new(model,
                                                                       always_on,
                                                                       fan,
                                                                       htg_coil,
                                                                       clg_coil)
  ptac.setName("#{zone.name} PTAC")
  ptac.setSupplyAirFanOperatingModeSchedule(always_off)
  if zero_outdoor_air
    ptac.setOutdoorAirFlowRateWhenNoCoolingorHeatingisNeeded 1.0e-5
    ptac.setOutdoorAirFlowRateDuringCoolingOperation(1.0e-5)
    ptac.setOutdoorAirFlowRateDuringHeatingOperation(1.0e-5)
  end
  ptac.addToThermalZone(zone)
end

#add_sys1_unitary_ac_baseboard_heating(model:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', zones:, mau_type:, mau_heating_coil_type:, baseboard_type:, hw_loop:, multispeed: false) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_single_speed.rb', line 2

def add_sys1_unitary_ac_baseboard_heating(model:,
                                          necb_reference_hp:false,
                                          necb_reference_hp_supp_fuel:'DefaultFuel',
                                          zones:,
                                          mau_type:,
                                          mau_heating_coil_type:,
                                          baseboard_type:,
                                          hw_loop:,
                                          multispeed: false)
  if multispeed
    add_sys1_unitary_ac_baseboard_heating_multi_speed(model: model,
                                                      zones: zones,
                                                      mau_type: mau_type,
                                                      mau_heating_coil_type: mau_heating_coil_type,
                                                      baseboard_type: baseboard_type,
                                                      hw_loop: hw_loop)
  else
    add_sys1_unitary_ac_baseboard_heating_single_speed(model: model,
                                                       necb_reference_hp: necb_reference_hp,
                                                       necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                       zones: zones,
                                                       mau_type: mau_type,
                                                       mau_heating_coil_type: mau_heating_coil_type,
                                                       baseboard_type: baseboard_type,
                                                       hw_loop: hw_loop)
  end
end

#add_sys1_unitary_ac_baseboard_heating_multi_speed(model:, zones:, mau_type:, mau_heating_coil_type:, baseboard_type:, hw_loop:) ⇒ Object

At this point the only way to implement multi-stage cooling and heating in OS is through the use of object “AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed”. This component uses as an argument a control zone and then it responds to a call for heating or cooling for that control zone. This aspect of this component makes it incompatible with how a system_1 make up air unit works where a constant supply air temperature is delivered to the spaces. It is therefore not recommended to use this method and to use the single speed implementation of systems_1.



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_multi_speed.rb', line 8

def add_sys1_unitary_ac_baseboard_heating_multi_speed(model:,
                                                      zones:,
                                                      mau_type:,
                                                      mau_heating_coil_type:,
                                                      baseboard_type:,
                                                      hw_loop:)

  # Keep all data and assumptions for both systems on the top here for easy reference.
  system_data = {}
  system_data[:name] = 'Sys_1_Make-up air unit'
  system_data[:PreheatDesignTemperature] = 7.0
  system_data[:PreheatDesignHumidityRatio] = 0.008
  system_data[:PrecoolDesignTemperature] = 13.0
  system_data[:PrecoolDesignHumidityRatio] = 0.008
  system_data[:SizingOption] = 'NonCoincident'
  system_data[:CoolingDesignAirFlowMethod] = 'DesignDay'
  system_data[:CoolingDesignAirFlowRate] = 0.0
  system_data[:HeatingDesignAirFlowMethod] = 'DesignDay'
  system_data[:HeatingDesignAirFlowRate] = 0.0
  system_data[:SystemOutdoorAirMethod] = 'ZoneSum'
  system_data[:CentralCoolingDesignSupplyAirHumidityRatio] = 0.0085
  system_data[:CentralHeatingDesignSupplyAirHumidityRatio] = 0.0080
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 43.0
  system_data[:AllOutdoorAirinCooling] = true
  system_data[:AllOutdoorAirinHeating] = true
  system_data[:TypeofLoadtoSizeOn] = 'VentilationRequirement'
  system_data[:MinimumSystemAirFlowRatio] = 1.0
  system_data[:MinimumOutdoorDryBulbTemperatureforCompressorOperation] = -10.0
  # Zone data
  system_data[:system_supply_air_temperature] = 20.0
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3

  # System Type 1: PTAC with no heating (unitary AC)
  # Zone baseboards, electric or hot water depending on argument baseboard_type
  # baseboard_type choices are "Hot Water" or "Electric"
  # PSZ to represent make-up air unit (if present)
  # This measure creates:
  # a PTAC  unit for each zone in the building; DX cooling coil
  # and heating coil that is always off
  # Baseboards ("Hot Water or "Electric") in zones connected to hot water loop
  # MAU is present if argument mau == true, not present if argument mau == false
  # MAU is PSZ; DX cooling
  # MAU heating coil: hot water coil or electric, depending on argument mau_heating_coil_type
  # mau_heating_coil_type choices are "Hot Water", "Electric"
  # boiler_fueltype choices match OS choices for Boiler component fuel type, i.e.
  # "NaturalGas","Electricity","PropaneGas","FuelOil#1","FuelOil#2","Coal","Diesel","Gasoline","OtherFuel1"

  # Some system parameters are set after system is set up; by applying method 'apply_hvac_efficiency_standard'

  always_on = model.alwaysOnDiscreteSchedule

  # define always off schedule for ptac heating coil
  always_off = BTAP::Resources::Schedules::StandardSchedules::ON_OFF.always_off(model)

  # Create MAU
  # TO DO: MAU sizing, characteristics (fan operation schedules, temperature setpoints, outdoor air, etc)

  if mau_type == true

    mau_air_loop = common_air_loop(model: model, system_data: system_data)
    mau_fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)

    # Setup heating and cooling coils
    mau_clg_coil = OpenStudio::Model::CoilCoolingDXMultiSpeed.new(model)
    mau_clg_coil.setFuelType('Electricity')
    mau_clg_stage_1 = OpenStudio::Model::CoilCoolingDXMultiSpeedStageData.new(model)
    mau_clg_stage_2 = OpenStudio::Model::CoilCoolingDXMultiSpeedStageData.new(model)
    mau_clg_coil.addStage(mau_clg_stage_1)
    mau_clg_coil.addStage(mau_clg_stage_2)
    mau_clg_coil.setApplyPartLoadFractiontoSpeedsGreaterthan1(false)
    mau_htg_coil = OpenStudio::Model::CoilHeatingGasMultiStage.new(model)
    mau_htg_stage_1 = OpenStudio::Model::CoilHeatingGasMultiStageStageData.new(model)
    mau_htg_coil.addStage(mau_htg_stage_1)
    mau_htg_stage_1.setNominalCapacity(0.001)
    if mau_heating_coil_type == 'Electric'
      mau_supplemental_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
    elsif mau_heating_coil_type == 'Hot Water'
      mau_supplemental_htg_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)
      hw_loop.addDemandBranchForComponent(mau_supplemental_htg_coil)
    else
      raise("#{mau_heating_coil_type} is not a valid heating coil type.)")
    end

    # @todo other fuel-fired heating coil types? (not available in OpenStudio/E+ - may need to play with efficiency to mimic other fuel types)

    # This method will seem like an error in number of args..but this is due to swig voodoo.
    air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed.new(model,
                                                                                              mau_fan,
                                                                                              mau_htg_coil,
                                                                                              mau_clg_coil,
                                                                                              mau_supplemental_htg_coil)
    air_to_air_heatpump.setName("#{zones[0].name} ASHP")
    air_to_air_heatpump.setMinimumOutdoorDryBulbTemperatureforCompressorOperation(system_data[:MinimumOutdoorDryBulbTemperatureforCompressorOperation])
    air_to_air_heatpump.setControllingZoneorThermostatLocation(zones[0])
    air_to_air_heatpump.setSupplyAirFanOperatingModeSchedule(always_on)
    air_to_air_heatpump.setNumberofSpeedsforHeating(1)
    air_to_air_heatpump.setNumberofSpeedsforCooling(2)

    # oa_controller
    oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
    oa_controller.autosizeMinimumOutdoorAirFlowRate

    # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
    # set explicitly)
    oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

    # oa_system
    oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

    # Add the components to the air loop
    # in order from closest to zone to furthest from zone
    supply_inlet_node = mau_air_loop.supplyInletNode
    air_to_air_heatpump.addToNode(supply_inlet_node)
    oa_system.addToNode(supply_inlet_node)

    # Add a setpoint manager to control the supply air temperature
    sat_sch = OpenStudio::Model::ScheduleRuleset.new(model)
    sat_sch.setName('Makeup-Air Unit Supply Air Temp')
    sat_sch.defaultDaySchedule.setName('Makeup Air Unit Supply Air Temp Default')
    sat_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), system_data[:system_supply_air_temperature])
    setpoint_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, sat_sch)
    setpoint_mgr.addToNode(mau_air_loop.supplyOutletNode)
    # Create MAU
  end

  zones.each do |zone|
    # Zone sizing temperature difference
    sizing_zone = zone.sizingZone
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod('TemperatureDifference')
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(11.0)
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod('TemperatureDifference')
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(21.0)
    sizing_zone.setZoneCoolingSizingFactor(1.1)
    sizing_zone.setZoneHeatingSizingFactor(1.3)

    # Set up PTAC heating coil; apply always off schedule

    # htg_coil_elec = OpenStudio::Model::CoilHeatingElectric.new(model,always_on)
    zero_outdoor_air = true # flag to set outside air flow to zero
    add_ptac_dx_cooling(model, zone, zero_outdoor_air)

    # add zone baseboards
    add_zone_baseboards(baseboard_type: baseboard_type, hw_loop: hw_loop, model: model, zone: zone)

    #  # Create a diffuser and attach the zone/diffuser pair to the MAU air loop, if applicable
    if mau_type == true

      diffuser = OpenStudio::Model::AirTerminalSingleDuctUncontrolled.new(model, always_on)
      mau_air_loop.addBranchForZone(zone, diffuser.to_StraightComponent)
      # components for MAU
    end
    # of zone loop
  end
  return true
end

#add_sys1_unitary_ac_baseboard_heating_single_speed(model:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', zones:, mau_type:, mau_heating_coil_type:, baseboard_type:, hw_loop:) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_1_single_speed.rb', line 30

def add_sys1_unitary_ac_baseboard_heating_single_speed(model:,
                                                       necb_reference_hp:false,
                                                       necb_reference_hp_supp_fuel:'DefaultFuel',
                                                       zones:,
                                                       mau_type:,
                                                       mau_heating_coil_type:,
                                                       baseboard_type:,
                                                       hw_loop:)
  # Keep all data and assumptions for both systems on the top here for easy reference.
  system_data = {}
  system_data[:name] = 'Sys_1_Make-up air unit'
  system_data[:PreheatDesignTemperature] = 7.0
  system_data[:PreheatDesignHumidityRatio] = 0.008
  system_data[:PrecoolDesignTemperature] = 13.0
  system_data[:PrecoolDesignHumidityRatio] = 0.008
  system_data[:SizingOption] = 'NonCoincident'
  system_data[:CoolingDesignAirFlowMethod] = 'DesignDay'
  system_data[:CoolingDesignAirFlowRate] = 0.0
  system_data[:HeatingDesignAirFlowMethod] = 'DesignDay'
  system_data[:HeatingDesignAirFlowRate] = 0.0
  system_data[:SystemOutdoorAirMethod] = 'ZoneSum'
  system_data[:CentralCoolingDesignSupplyAirHumidityRatio] = 0.0085
  system_data[:CentralHeatingDesignSupplyAirHumidityRatio] = 0.0080
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 43.0
  system_data[:AllOutdoorAirinCooling] = true
  system_data[:AllOutdoorAirinHeating] = true
  if necb_reference_hp
    system_data[:TypeofLoadtoSizeOn] = 'Total'
  else
    system_data[:TypeofLoadtoSizeOn] = 'VentilationRequirement'
  end
  system_data[:MinimumSystemAirFlowRatio] = 1.0
  # Zone Sizing data
  system_data[:system_supply_air_temperature] = 20.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:ZoneDXCoolingSizingFactor] = 1.0
  system_data[:ZoneDXHeatingSizingFactor] = 1.3
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3

  # System Type 1: PTAC with no heating (unitary AC)
  # Zone baseboards, electric or hot water depending on argument baseboard_type
  # baseboard_type choices are "Hot Water" or "Electric"
  # PSZ to represent make-up air unit (if present)
  # This measure creates:
  # a PTAC  unit for each zone in the building; DX cooling coil
  # and heating coil that is always off
  # Baseboards ("Hot Water or "Electric") in zones connected to hot water loop
  # MAU is present if argument mau == true, not present if argument mau == false
  # MAU is PSZ; DX cooling
  # MAU heating coil: hot water coil or electric, depending on argument mau_heating_coil_type
  # mau_heating_coil_type choices are "Hot Water", "Electric"
  # boiler_fueltype choices match OS choices for Boiler component fuel type, i.e.
  # "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOilNo2","Coal","Diesel","Gasoline","OtherFuel1"

  # If reference_hp = true, NECB 8.4.4.13 Heat Pump System Type 1: CAV Packaged rooftop heat pump with
  # zone baseboard (electric or hot water depending on argument baseboard_type)

  # Some system parameters are set after system is set up; by applying method 'apply_hvac_efficiency_standard'

  always_on = model.alwaysOnDiscreteSchedule
  always_off = BTAP::Resources::Schedules::StandardSchedules::ON_OFF.always_off(model)

  # Create MAU
  # TO DO: MAU sizing, characteristics (fan operation schedules, temperature setpoints, outdoor air, etc)



  if mau_type == true
    mau_air_loop = common_air_loop(model: model, system_data: system_data)

    #if reference_hp
      # AirLoopHVACUnitaryHeatPumpAirToAir needs FanOnOff in order for the fan to turn off during off hours
    #  mau_fan = OpenStudio::Model::FanOnOff.new(model, always_on)
    #else
      mau_fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)
    #end
    # MAU Heating type selection.
    raise("Flag 'necb_reference_hp' is true while 'mau_heating_coil_type' is not set to type DX") if (necb_reference_hp && (mau_heating_coil_type != 'DX'))
    if mau_heating_coil_type == 'Electric' # electric coil
      mau_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
    elsif  mau_heating_coil_type == 'Hot Water'
      mau_htg_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)
      hw_loop.addDemandBranchForComponent(mau_htg_coil)
    elsif mau_heating_coil_type == 'DX'
      mau_htg_coil = add_onespeed_htg_DX_coil(model, always_on)
      mau_htg_coil.setName('CoilHeatingDXSingleSpeed_ashp')
    end

    # Set up Single Speed DX coil with
    mau_clg_coil = add_onespeed_DX_coil(model, always_on)
    mau_clg_coil.setName('CoilCoolingDXSingleSpeed_dx')
    mau_clg_coil.setName('CoilCoolingDXSingleSpeed_ashp') if necb_reference_hp

    # Set up OA system
    oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
    oa_controller.autosizeMinimumOutdoorAirFlowRate

    # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
    # set explicitly)
    oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

    oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

    # Add the components to the air loop
    # in order from closest to zone to furthest from zone
    supply_inlet_node = mau_air_loop.supplyInletNode

    # Reference HP requires slight changes to default MAU heating
    #if reference_hp
      # Create supplemental heating coil based on default regional fuel type
      # epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get)
      #primary_heating_fuel = @standards_data['regional_fuel_use'].detect { |fuel_sources| fuel_sources['state_province_regions'].include?(epw.stateProvinceRegion) }['fueltype_set']
      #if primary_heating_fuel == 'NaturalGas'
      #  supplemental_htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
      #elsif primary_heating_fuel == 'Electricity' or  primary_heating_fuel == 'FuelOilNo2'
      #  supplemental_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
      #else #hot water coils is an option in the future
      #  raise('Invalid fuel type selected for heat pump supplemental coil')
      #end
      #air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitaryHeatPumpAirToAir.new(model, always_on, mau_fan, mau_htg_coil, mau_clg_coil, supplemental_htg_coil)
      #air_to_air_heatpump.setName("#{control_zone.name} ASHP")
      #air_to_air_heatpump.setControllingZone(control_zone)
      #air_to_air_heatpump.setSupplyAirFanOperatingModeSchedule(always_on)
      #air_to_air_heatpump.addToNode(supply_inlet_node)
    #else
      mau_fan.addToNode(supply_inlet_node)
      mau_htg_coil.addToNode(supply_inlet_node)
      mau_clg_coil.addToNode(supply_inlet_node)

    #end
    oa_system.addToNode(supply_inlet_node)

    # Add a setpoint manager to control the supply air temperature
    if necb_reference_hp
      setpoint_mgr = OpenStudio::Model::SetpointManagerWarmest.new(model)
      setpoint_mgr.setMinimumSetpointTemperature(13)
      setpoint_mgr.setMaximumSetpointTemperature(20)
      setpoint_mgr.addToNode(mau_air_loop.supplyOutletNode)
    else
      sat_sch = OpenStudio::Model::ScheduleRuleset.new(model)
      sat_sch.setName('Makeup-Air Unit Supply Air Temp')
      sat_sch.defaultDaySchedule.setName('Makeup Air Unit Supply Air Temp Default')
      sat_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), system_data[:system_supply_air_temperature])
      setpoint_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, sat_sch)
      setpoint_mgr.addToNode(mau_air_loop.supplyOutletNode)
    end
  end

  zones.each do |zone|
    # Zone sizing temperature difference
    sizing_zone = zone.sizingZone
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
    # Different sizing factors for reference HP capacity
    if necb_reference_hp
      sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
      sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])
    else
      sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
      sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])
    end

    # Create a PTAC for each zone:
    # PTAC DX Cooling with electric heating coil; electric heating coil is always off
    # TO DO: need to apply this system to space types:
    # (1) data processing area: control room, data centre
    # when cooling capacity <= 20kW and
    # (2) residential/accommodation: murb, hotel/motel guest room
    # when building/space heated only (this as per NECB; apply to
    # all for initial work? CAN-QUEST limitation)

    # TO DO: PTAC characteristics: sizing, fan schedules, temperature setpoints, interaction with MAU

    # htg_coil_elec = OpenStudio::Model::CoilHeatingElectric.new(model,always_on)
    zero_outdoor_air = true # flag to set outside air flow to 0.0
    # Reference HP system does not use PTAC
    unless necb_reference_hp
      add_ptac_dx_cooling(model, zone, zero_outdoor_air)
    end

    # add zone baseboards
    add_zone_baseboards(baseboard_type: baseboard_type,
                        hw_loop: hw_loop,
                        model: model,
                        zone: zone)

    #  # Create a diffuser and attach the zone/diffuser pair to the MAU air loop, if applicable

    if necb_reference_hp
      # Create CAV RH (RH based on region's default fuel type or user input)
      if necb_reference_hp_supp_fuel == 'DefaultFuel'
        epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get)
        necb_reference_hp_supp_fuel = @standards_data['regional_fuel_use'].detect { |fuel_sources| fuel_sources['state_province_regions'].include?(epw.stateProvinceRegion) }['fueltype_set']
      end
      if necb_reference_hp_supp_fuel == 'NaturalGas'
        rh_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
      elsif necb_reference_hp_supp_fuel == 'Electricity' or  necb_reference_hp_supp_fuel == 'FuelOilNo2'
        rh_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
      else #hot water coils is an option in the future
        raise('Invalid fuel type selected for heat pump supplemental coil')
      end
      cav_rh_terminal = OpenStudio::Model::AirTerminalSingleDuctConstantVolumeReheat.new(model, always_on, rh_coil)
      mau_air_loop.addBranchForZone(zone, cav_rh_terminal.to_StraightComponent)
    elsif mau_type == true
      diffuser = OpenStudio::Model::AirTerminalSingleDuctUncontrolled.new(model, always_on)
      mau_air_loop.addBranchForZone(zone, diffuser.to_StraightComponent)
      # components for MAU
    end
    # of zone loop
  end
  if mau_type
    sys_name_pars = {}
    sys_name_pars['sys_hr'] = 'none'
    sys_name_pars['sys_clg'] = 'dx'
    sys_name_pars['sys_clg'] = 'ashp' if necb_reference_hp
    sys_name_pars['sys_htg'] = mau_heating_coil_type
    sys_name_pars['sys_htg'] = 'ashp' if necb_reference_hp
    sys_name_pars['sys_sf'] = 'cv'
    sys_name_pars['zone_htg'] = baseboard_type
    sys_oa = 'doas'
    if necb_reference_hp
      sys_name_pars['zone_clg'] = 'none'
      sys_oa = 'mixed'
    else
      sys_name_pars['zone_clg'] = 'ptac'
      sys_oa = 'doas'
    end
    sys_name_pars['sys_rf'] = 'none'
    assign_base_sys_name(mau_air_loop,
                         sys_abbr: 'sys_1',
                         sys_oa: sys_oa,
                         sys_name_pars: sys_name_pars)
  end

  return true
end

#add_sys2_FPFC_sys5_TPFC(model:, zones:, chiller_type:, fan_coil_type:, mau_cooling_type:, hw_loop:) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_2_and_5.rb', line 2

def add_sys2_FPFC_sys5_TPFC(model:,
                            zones:,
                            chiller_type:,
                            fan_coil_type:,
                            mau_cooling_type:,
                            hw_loop:)

  # System 2 AHU data
  system_data = {}
  system_data[:name] = 'Sys_2_Make-up air unit'
  system_data[:PreheatDesignTemperature] = 7.0
  system_data[:PreheatDesignHumidityRatio] = 0.008
  system_data[:PrecoolDesignTemperature] = 13.0
  system_data[:PrecoolDesignHumidityRatio] = 0.008
  system_data[:SizingOption] = 'NonCoincident'
  system_data[:CoolingDesignAirFlowMethod] = 'DesignDay'
  system_data[:CoolingDesignAirFlowRate] = 0.0
  system_data[:HeatingDesignAirFlowMethod] = 'DesignDay'
  system_data[:HeatingDesignAirFlowRate] = 0.0
  system_data[:SystemOutdoorAirMethod] = 'ZoneSum'
  system_data[:CentralCoolingDesignSupplyAirHumidityRatio] = 0.0085
  system_data[:CentralHeatingDesignSupplyAirHumidityRatio] = 0.0080

  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 13.1
  system_data[:AllOutdoorAirinCooling] = false
  system_data[:AllOutdoorAirinHeating] = false
  system_data[:TypeofLoadtoSizeOn] = 'Sensible'
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMax] = 13.0
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMin] = 13.1
  system_data[:MinimumSystemAirFlowRatio] = 1.0

  # System 2 Zone data
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3

  # System Type 2: FPFC or System 5: TPFC
  # This measure creates:
  # -a four pipe or a two pipe fan coil unit for each zone in the building;
  # -a make up air-unit to provide ventilation to each zone;
  # -a heating loop, cooling loop and condenser loop to serve four pipe fan coil units
  # Arguments:
  #   boiler_fueltype: "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOilNO2","Coal","Diesel","Gasoline","OtherFuel1"
  #   chiller_type: "Scroll";"Centrifugal";"Rotary Screw";"Reciprocating"
  #   mua_cooling_type: make-up air unit cooling type "DX";"Hydronic"
  #   fan_coil_type options are "TPFC" or "FPFC"

  # @todo Add arguments as needed when the sizing routine is finalized. For example we will need to know the
  # required size of the boilers to decide on how many units are needed based on NECB rules.

  always_on = model.alwaysOnDiscreteSchedule

  # schedule for two-pipe fan coil operation. 3 seasons for heating/cooling.
  tpfc_clg_availability_sch, tpfc_htg_availability_sch = create_heating_cooling_on_off_availability_schedule(model)

  # Create a chilled water loop
  chw_loop = OpenStudio::Model::PlantLoop.new(model)
  chiller1, chiller2 = setup_chw_loop_with_components(model, chw_loop, chiller_type)

  # Create a condenser Loop
  cw_loop = OpenStudio::Model::PlantLoop.new(model)
  ctower = setup_cw_loop_with_components(model, cw_loop, chiller1, chiller2)

  # Set up make-up air unit for ventilation
  # TO DO: Need to investigate characteristics of make-up air unit for NECB reference
  # and define them here

  air_loop = mau_air_loop = common_air_loop(model: model, system_data: system_data)
  air_loop.setName(system_data[:name])

  fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)

  # Assume direct-fired gas heating coil for now; need to add logic
  # to set up hydronic or electric coil depending on proposed?

  htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)

  # Add DX or hydronic cooling coil
  if mau_cooling_type == 'DX'
    clg_coil = add_onespeed_DX_coil(model, tpfc_clg_availability_sch)
    clg_coil.setName('CoilCoolingDXSingleSpeed_dx')
  elsif mau_cooling_type == 'Hydronic'
    clg_coil = OpenStudio::Model::CoilCoolingWater.new(model, tpfc_clg_availability_sch)
    chw_loop.addDemandBranchForComponent(clg_coil)
  end

  # does MAU have an economizer?
  oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
  oa_controller.autosizeMinimumOutdoorAirFlowRate

  # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
  # set explicitly)
  oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

  # oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model,oa_controller)
  oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

  # Add the components to the air loop
  # in order from closest to zone to furthest from zone
  supply_inlet_node = air_loop.supplyInletNode
  fan.addToNode(supply_inlet_node)
  htg_coil.addToNode(supply_inlet_node)
  clg_coil.addToNode(supply_inlet_node)
  oa_system.addToNode(supply_inlet_node)

  # Add a setpoint manager single zone reheat to control the
  # supply air temperature based on the needs of default zone (OpenStudio picks one)
  # TO DO: need to have method to pick appropriate control zone?

  setpoint_mgr_single_zone_reheat = OpenStudio::Model::SetpointManagerSingleZoneReheat.new(model)
  setpoint_mgr_single_zone_reheat.setMinimumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMin])
  setpoint_mgr_single_zone_reheat.setMaximumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMax])
  setpoint_mgr_single_zone_reheat.addToNode(air_loop.supplyOutletNode)

  # Set up zonal FC (ZoneHVAC,cooling coil, heating coil, fan) in each zone
  zones.each do |zone|
    # Zone sizing temperature difference
    sizing_zone = zone.sizingZone
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
    sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
    sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])

    # fc supply fan
    fc_fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)

    if fan_coil_type == 'FPFC'
      # heating coil
      fc_htg_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)

      # cooling coil
      fc_clg_coil = OpenStudio::Model::CoilCoolingWater.new(model, always_on)
    elsif fan_coil_type == 'TPFC'
      # heating coil
      fc_htg_coil = OpenStudio::Model::CoilHeatingWater.new(model, tpfc_htg_availability_sch)

      # cooling coil
      fc_clg_coil = OpenStudio::Model::CoilCoolingWater.new(model, tpfc_clg_availability_sch)
    end

    # connect heating coil to hot water loop and cooling coil to chw loop.
    hw_loop.addDemandBranchForComponent(fc_htg_coil)
    chw_loop.addDemandBranchForComponent(fc_clg_coil)

    # add connections to FPFC.
    # This method will seem like an error in number of args..but this is due to swig voodoo.
    zone_fc = OpenStudio::Model::ZoneHVACFourPipeFanCoil.new(model, always_on, fc_fan, fc_clg_coil, fc_htg_coil)
    zone_fc.addToThermalZone(zone)

    # Create a diffuser and attach the zone/diffuser pair to the air loop (make-up air unit)
    diffuser = OpenStudio::Model::AirTerminalSingleDuctUncontrolled.new(model, always_on)
    air_loop.addBranchForZone(zone, diffuser.to_StraightComponent)
    # zone loop
  end
  sys_abbr = 'sys_2'
  sys_abbr = 'sys_5' if fan_coil_type == 'TPFC'
  sys_name_pars = {}
  sys_name_pars['sys_hr'] = 'none'
  sys_name_pars['sys_clg'] = mau_cooling_type
  sys_name_pars['sys_htg'] = 'g'
  sys_name_pars['sys_sf'] = 'cv'
  sys_name_pars['zone_htg'] = fan_coil_type
  sys_name_pars['zone_clg'] = fan_coil_type
  sys_name_pars['sys_rf'] = 'none'
  assign_base_sys_name(mau_air_loop,
                       sys_abbr: sys_abbr,
                       sys_oa: 'doas',
                       sys_name_pars: sys_name_pars)
end

#add_sys3_and_8_zone_equip(air_loop, baseboard_type, hw_loop, model, zone) ⇒ Object



250
251
252
253
254
255
256
257
258
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb', line 250

def add_sys3_and_8_zone_equip(air_loop,
                              baseboard_type,
                              hw_loop, model,
                              zone)
  always_on = model.alwaysOnDiscreteSchedule
  add_zone_baseboards(baseboard_type: baseboard_type, hw_loop: hw_loop, model: model, zone: zone)
  diffuser = OpenStudio::Model::AirTerminalSingleDuctUncontrolled.new(model, always_on)
  air_loop.addBranchForZone(zone, diffuser.to_StraightComponent)
end

#add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', zones:, heating_coil_type:, baseboard_type:, hw_loop:, new_auto_zoner: true, multispeed: false) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb', line 2

def add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model:,
                                                                          necb_reference_hp:false,
                                                                          necb_reference_hp_supp_fuel:'DefaultFuel',
                                                                          zones:,
                                                                          heating_coil_type:,
                                                                          baseboard_type:,
                                                                          hw_loop:,
                                                                          new_auto_zoner: true,
                                                                          multispeed: false)
  if multispeed
    add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating_multi_speed(model: model,
                                                                                      zones: zones,
                                                                                      heating_coil_type: heating_coil_type,
                                                                                      baseboard_type: baseboard_type,
                                                                                      hw_loop: hw_loop,
                                                                                      new_auto_zoner: new_auto_zoner)
  else
    add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating_single_speed(model: model,
                                                                                       necb_reference_hp: necb_reference_hp,
                                                                                       necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                                                       zones: zones,
                                                                                       heating_coil_type: heating_coil_type,
                                                                                       baseboard_type: baseboard_type,
                                                                                       hw_loop: hw_loop,
                                                                                       new_auto_zoner: new_auto_zoner)

  end
end

#add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating_multi_speed(model:, zones:, heating_coil_type:, baseboard_type:, hw_loop:, new_auto_zoner: true) ⇒ Object

Some tests still require a simple way to set up a system without sizing.. so we are keeping the auto_zoner flag for this method.



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb', line 3

def add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating_multi_speed(model:,
                                                                                      zones:,
                                                                                      heating_coil_type:,
                                                                                      baseboard_type:,
                                                                                      hw_loop:,
                                                                                      new_auto_zoner: true)
  system_data = {}
  system_data[:name] = 'Sys_3_PSZ'
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 43.0
  system_data[:AllOutdoorAirinCooling] = false
  system_data[:AllOutdoorAirinHeating] = false
  system_data[:TypeofLoadtoSizeOn] = 'Sensible'
  system_data[:MinimumSystemAirFlowRatio] = 1.0

  system_data[:PreheatDesignTemperature] = 7.0
  system_data[:PreheatDesignHumidityRatio] = 0.008
  system_data[:PrecoolDesignTemperature] = 13.0
  system_data[:PrecoolDesignHumidityRatio] = 0.008
  system_data[:SizingOption] = 'NonCoincident'
  system_data[:CoolingDesignAirFlowMethod] = 'DesignDay'
  system_data[:CoolingDesignAirFlowRate] = 0.0
  system_data[:HeatingDesignAirFlowMethod] = 'DesignDay'
  system_data[:HeatingDesignAirFlowRate] = 0.0
  system_data[:SystemOutdoorAirMethod] = 'ZoneSum'
  system_data[:CentralCoolingDesignSupplyAirHumidityRatio] = 0.0085
  system_data[:CentralHeatingDesignSupplyAirHumidityRatio] = 0.0080

  # System 3 Zone data
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMin] = 13.0
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMax] = 43.0
  system_data[:ZoneDXCoolingSizingFactor] = 1.0
  system_data[:ZoneDXHeatingSizingFactor] = 1.3
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3
  system_data[:MinimumOutdoorDryBulbTemperatureforCompressorOperation] = -10.0

  if new_auto_zoner == true
    # Create system airloop

    # Add Air Loop
    air_loop = add_system_3_and_8_airloop_multi_speed(heating_coil_type,
                                                      model,
                                                      system_data,
                                                      determine_control_zone(zones))
    # Add Zone equipment
    zones.each do |zone| # Zone sizing temperature difference
      sizing_zone = zone.sizingZone
      sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
      sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
      sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
      sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
      sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
      sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])
      add_sys3_and_8_zone_equip(air_loop,
                                baseboard_type,
                                hw_loop,
                                model,
                                zone)
    end
    return true
  else
    zones.each do |zone|
      air_loop = add_system_3_and_8_airloop_multi_speed(heating_coil_type, model, system_data, zone)
      add_sys3_and_8_zone_equip(air_loop,
                                baseboard_type,
                                hw_loop,
                                model,
                                zone)
    end
    return true
  end
end

#add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating_single_speed(model:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', zones:, heating_coil_type:, baseboard_type:, hw_loop:, new_auto_zoner: true) ⇒ Object

Some tests still require a simple way to set up a system without sizing.. so we are keeping the auto_zoner flag for this method.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb', line 33

def add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating_single_speed(model:,
                                                                                       necb_reference_hp:false,
                                                                                       necb_reference_hp_supp_fuel:'DefaultFuel',
                                                                                       zones:,
                                                                                       heating_coil_type:,
                                                                                       baseboard_type:,
                                                                                       hw_loop:,
                                                                                       new_auto_zoner: true)
  system_data = {}
  system_data[:name] = 'Sys_3_PSZ'
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 43.0
  system_data[:AllOutdoorAirinCooling] = false
  system_data[:AllOutdoorAirinHeating] = false
  system_data[:TypeofLoadtoSizeOn] = 'Sensible'
  system_data[:MinimumSystemAirFlowRatio] = 1.0

  system_data[:PreheatDesignTemperature] = 7.0
  system_data[:PreheatDesignHumidityRatio] = 0.008
  system_data[:PrecoolDesignTemperature] = 13.0
  system_data[:PrecoolDesignHumidityRatio] = 0.008
  system_data[:SizingOption] = 'NonCoincident'
  system_data[:CoolingDesignAirFlowMethod] = 'DesignDay'
  system_data[:CoolingDesignAirFlowRate] = 0.0
  system_data[:HeatingDesignAirFlowMethod] = 'DesignDay'
  system_data[:HeatingDesignAirFlowRate] = 0.0
  system_data[:SystemOutdoorAirMethod] = 'ZoneSum'
  system_data[:CentralCoolingDesignSupplyAirHumidityRatio] = 0.0085
  system_data[:CentralHeatingDesignSupplyAirHumidityRatio] = 0.0080

  # System 3 Zone data
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMin] = 13.0
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMax] = 43.0
  system_data[:ZoneDXCoolingSizingFactor] = 1.0
  system_data[:ZoneDXHeatingSizingFactor] = 1.3
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3
  system_data[:MinimumOutdoorDryBulbTemperatureforCompressorOperation] = -10.0
  if new_auto_zoner == true
    # Create system airloop

    # Add Air Loop
    air_loop = add_system_3_and_8_airloop(heating_coil_type,
                                          model,
                                          system_data,
                                          determine_control_zone(zones),
                                          necb_reference_hp: necb_reference_hp,
                                          necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel)
    # Add Zone equipment
    zones.each do |zone| # Zone sizing temperature difference
      sizing_zone = zone.sizingZone
      sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
      sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
      sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
      sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
      if necb_reference_hp
        sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
        sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])
      else
        sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
        sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])
      end
      add_sys3_and_8_zone_equip(air_loop,
                                baseboard_type,
                                hw_loop,
                                model,
                                zone)

    end
  else
    zones.each do |zone|
      air_loop = add_system_3_and_8_airloop(heating_coil_type, model, system_data, zone, necb_reference_hp: necb_reference_hp, necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel)
      add_sys3_and_8_zone_equip(air_loop,
                                baseboard_type,
                                hw_loop,
                                model,
                                zone)
    end
  end
  sys_name_pars = {}
  sys_name_pars['sys_hr'] = 'none'
  sys_name_pars['sys_clg'] = 'dx'
  sys_name_pars['sys_clg'] = 'ashp' if necb_reference_hp
  sys_name_pars['sys_htg'] = heating_coil_type
  sys_name_pars['sys_htg'] = 'ashp' if necb_reference_hp
  sys_name_pars['sys_sf'] = 'cv'
  sys_name_pars['zone_htg'] = baseboard_type
  sys_name_pars['zone_clg'] = 'none'
  sys_name_pars['sys_rf'] = 'none'
  assign_base_sys_name(air_loop,
                       sys_abbr: 'sys_3',
                       sys_oa: 'mixed',
                       sys_name_pars: sys_name_pars)
  return true
end

#add_sys4_single_zone_make_up_air_unit_with_baseboard_heating(model:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', zones:, heating_coil_type:, baseboard_type:, hw_loop:) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_4.rb', line 2

def add_sys4_single_zone_make_up_air_unit_with_baseboard_heating(model:,
                                                                 necb_reference_hp:false,
                                                                 necb_reference_hp_supp_fuel:'DefaultFuel',
                                                                 zones:,
                                                                 heating_coil_type:,
                                                                 baseboard_type:,
                                                                 hw_loop:)
  system_data = {}
  system_data[:name] = 'Sys_4_PSZ'
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 43.0
  system_data[:AllOutdoorAirinCooling] = false
  system_data[:AllOutdoorAirinHeating] = false
  system_data[:TypeofLoadtoSizeOn] = 'Sensible'
  system_data[:MinimumSystemAirFlowRatio] = 1.0

  system_data[:PreheatDesignTemperature] = 7.0
  system_data[:PreheatDesignHumidityRatio] = 0.008
  system_data[:PrecoolDesignTemperature] = 13.0
  system_data[:PrecoolDesignHumidityRatio] = 0.008
  system_data[:SizingOption] = 'NonCoincident'
  system_data[:CoolingDesignAirFlowMethod] = 'DesignDay'
  system_data[:CoolingDesignAirFlowRate] = 0.0
  system_data[:HeatingDesignAirFlowMethod] = 'DesignDay'
  system_data[:HeatingDesignAirFlowRate] = 0.0
  system_data[:SystemOutdoorAirMethod] = 'ZoneSum'
  system_data[:CentralCoolingDesignSupplyAirHumidityRatio] = 0.0085
  system_data[:CentralHeatingDesignSupplyAirHumidityRatio] = 0.0080

  # zone
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMax] = 43.0
  system_data[:SetpointManagerSingleZoneReheatSupplyTempMin] = 13.0
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:ZoneHeatingDesignSupplyAirTemperature] = 43.0
  system_data[:ZoneDXCoolingSizingFactor] = 1.0
  system_data[:ZoneDXHeatingSizingFactor] = 1.3
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3

  # System Type 4: PSZ-AC
  # This measure creates:
  # -a constant volume packaged single-zone A/C unit
  # for each zone in the building; DX cooling with
  # heating coil: fuel-fired or electric, depending on argument heating_coil_type
  # heating_coil_type choices are "Electric", "Gas"
  # zone baseboards: hot water or electric, depending on argument baseboard_type
  # baseboard_type choices are "Hot Water" or "Electric"
  # boiler_fueltype choices match OS choices for Boiler component fuel type, i.e.
  # "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOil#2","Coal","Diesel","Gasoline","OtherFuel1"
  # NOTE: This is the same as system type 3 (single zone make-up air unit and single zone rooftop unit are both PSZ systems)
  # SHOULD WE COMBINE sys3 and sys4 into one script?
  #
  # control_zone = determine_control_zone(zones)
  # Todo change this when control zone method is working.
  control_zone = zones.first

  always_on = model.alwaysOnDiscreteSchedule

  # Create a PSZ for each zone
  # TO DO: need to apply this system to space types:
  # (1) automotive area: repair/parking garage, fire engine room, indoor truck bay
  # (2) supermarket/food service: food preparation with kitchen hood/vented appliance
  # (3) warehouse area (non-refrigerated spaces)

  air_loop = common_air_loop(model: model, system_data: system_data)
  air_loop.setName("#{system_data[:name]}_#{control_zone.name}")

  # Zone sizing temperature difference
  sizing_zone = control_zone.sizingZone
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
  sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
  if necb_reference_hp
    sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
    sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])
  else
    sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
    sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])
  end

  if necb_reference_hp
    # AirLoopHVACUnitaryHeatPumpAirToAir needs FanOnOff in order for the fan to turn off during off hours
    fan = OpenStudio::Model::FanOnOff.new(model, always_on)
  else
    fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)
  end

  # Set up DX coil with NECB performance curve characteristics;
  clg_coil = add_onespeed_DX_coil(model, always_on)
  clg_coil.setName('CoilCoolingDXSingleSpeed_dx')
  clg_coil.setName('CoilCoolingDXSingleSpeed_ashp') if necb_reference_hp

  raise("Flag 'necb_reference_hp' is set to true while parameter 'heating_coil_type' is not set to DX") if (necb_reference_hp && (heating_coil_type != 'DX'))
  if heating_coil_type == 'Electric' # electric coil
    htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
  elsif heating_coil_type == 'Gas'
    htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
  elsif heating_coil_type == 'DX'
    htg_coil = add_onespeed_htg_DX_coil(model, always_on)
    htg_coil.setName('CoilHeatingDXSingleSpeed_ashp')
  end

  # TO DO: other fuel-fired heating coil types? (not available in OpenStudio/E+ - may need to play with efficiency to mimic other fuel types)

  # oa_controller
  oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
  oa_controller.autosizeMinimumOutdoorAirFlowRate

  # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
  # set explicitly)
  oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

  # oa_system
  oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

  # Add the components to the air loop
  # in order from closest to zone to furthest from zone
  supply_inlet_node = air_loop.supplyInletNode
  if necb_reference_hp
    #create supplemental heating coil based on default regional fuel type
    if necb_reference_hp_supp_fuel == 'DefaultFuel'
      epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get)
      necb_reference_hp_supp_fuel = @standards_data['regional_fuel_use'].detect { |fuel_sources| fuel_sources['state_province_regions'].include?(epw.stateProvinceRegion) }['fueltype_set']
    end
    if necb_reference_hp_supp_fuel == 'NaturalGas'
      supplemental_htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
    elsif necb_reference_hp_supp_fuel == 'Electricity' or  necb_reference_hp_supp_fuel == 'FuelOilNo2'
      supplemental_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
    else #hot water coils is an option in the future
      raise('Invalid fuel type selected for heat pump supplemental coil')
    end
    # This method will seem like an error in number of args..but this is due to swig voodoo.
    air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitaryHeatPumpAirToAir.new(model, always_on, fan, htg_coil, clg_coil, supplemental_htg_coil)
    air_to_air_heatpump.setName("#{control_zone.name} ASHP")
    air_to_air_heatpump.setControllingZone(control_zone)
    air_to_air_heatpump.setSupplyAirFanOperatingModeSchedule(always_on)
    air_to_air_heatpump.addToNode(supply_inlet_node)
  else
    fan.addToNode(supply_inlet_node)
    htg_coil.addToNode(supply_inlet_node)
    clg_coil.addToNode(supply_inlet_node)

  end
  oa_system.addToNode(supply_inlet_node)
  # Add a setpoint manager single zone reheat to control the
  # supply air temperature based on the needs of this zone
  setpoint_mgr_single_zone_reheat = OpenStudio::Model::SetpointManagerSingleZoneReheat.new(model)
  setpoint_mgr_single_zone_reheat.setControlZone(control_zone)
  setpoint_mgr_single_zone_reheat.setMinimumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMin])
  setpoint_mgr_single_zone_reheat.setMaximumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMax])
  setpoint_mgr_single_zone_reheat.addToNode(air_loop.supplyOutletNode)

  # Create sensible heat exchanger
  #              heat_exchanger = BTAP::Resources::HVAC::Plant::add_hrv(model)
  #              heat_exchanger.setSensibleEffectivenessat100HeatingAirFlow(0.5)
  #              heat_exchanger.setSensibleEffectivenessat75HeatingAirFlow(0.5)
  #              heat_exchanger.setSensibleEffectivenessat100CoolingAirFlow(0.5)
  #              heat_exchanger.setSensibleEffectivenessat75CoolingAirFlow(0.5)
  #              heat_exchanger.setLatentEffectivenessat100HeatingAirFlow(0.0)
  #              heat_exchanger.setLatentEffectivenessat75HeatingAirFlow(0.0)
  #              heat_exchanger.setLatentEffectivenessat100CoolingAirFlow(0.0)
  #              heat_exchanger.setLatentEffectivenessat75CoolingAirFlow(0.0)
  #              heat_exchanger.setSupplyAirOutletTemperatureControl(false)
  #
  #              Connect heat exchanger
  #              oa_node = oa_system.outboardOANode
  #              heat_exchanger.addToNode(oa_node.get)
  zones.each do |zone|
    sizing_zone = zone.sizingZone
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
    sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
    sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
    if necb_reference_hp
      sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
      sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])
    else
      sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
      sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])
    end
    # Create a diffuser and attach the zone/diffuser pair to the air loop
    # diffuser = OpenStudio::Model::AirTerminalSingleDuctUncontrolled.new(model,always_on)
    add_zone_baseboards(baseboard_type: baseboard_type,
                        hw_loop: hw_loop,
                        model: model,
                        zone: zone)
    diffuser = OpenStudio::Model::AirTerminalSingleDuctUncontrolled.new(model, always_on)
    air_loop.addBranchForZone(zone, diffuser.to_StraightComponent)
    # zone loop
  end
  sys_name_pars = {}
  sys_name_pars['sys_hr'] = 'none'
  sys_name_pars['sys_clg'] = 'dx'
  sys_name_pars['sys_clg'] = 'ashp' if necb_reference_hp
  sys_name_pars['sys_htg'] = heating_coil_type
  sys_name_pars['sys_htg'] = 'ashp' if necb_reference_hp
  sys_name_pars['sys_sf'] = 'cv'
  sys_name_pars['zone_htg'] = baseboard_type
  sys_name_pars['zone_clg'] = 'none'
  sys_name_pars['sys_rf'] = 'none'
  assign_base_sys_name(air_loop,
                       sys_abbr: 'sys_4',
                       sys_oa: 'mixed',
                       sys_name_pars: sys_name_pars)

  return true
end

#add_sys6_multi_zone_built_up_system_with_baseboard_heating(model:, zones:, heating_coil_type:, baseboard_type:, chiller_type:, fan_type:, hw_loop:) ⇒ Object

end add_sys4_single_zone_make_up_air_unit_with_baseboard_heating



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb', line 4

def add_sys6_multi_zone_built_up_system_with_baseboard_heating(model:,
                                                               zones:,
                                                               heating_coil_type:,
                                                               baseboard_type:,
                                                               chiller_type:,
                                                               fan_type:,
                                                               hw_loop:)
  # System Type 6: VAV w/ Reheat
  # This measure creates:
  # a single hot water loop with a natural gas or electric boiler or for the building
  # a single chilled water loop with water cooled chiller for the building
  # a single condenser water loop for heat rejection from the chiller
  # a VAV system w/ hot water or electric heating, chilled water cooling, and
  # hot water or electric reheat for each story of the building
  # Arguments:
  # "boiler_fueltype" choices match OS choices for boiler fuel type:
  # "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOil#2","Coal","Diesel","Gasoline","OtherFuel1"
  # "heating_coil_type": "Electric" or "Hot Water"
  # "baseboard_type": "Electric" and "Hot Water"
  # "chiller_type": "Scroll";"Centrifugal";""Screw";"Reciprocating"
  # "fan_type": "AF_or_BI_rdg_fancurve";"AF_or_BI_inletvanes";"fc_inletvanes";"var_speed_drive"
  #
  system_data = {}
  system_data[:name] = 'Sys_6_VAV with Reheat'
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 13.1
  system_data[:AllOutdoorAirinCooling] = false
  system_data[:AllOutdoorAirinHeating] = false
  system_data[:MinimumSystemAirFlowRatio] = 0.3

  # zone data
  system_data[:system_supply_air_temperature] = 13.0
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:ZoneCoolingSizingFactor] = 1.1
  system_data[:ZoneHeatingSizingFactor] = 1.3
  system_data[:ZoneVAVMinFlowFactorPerFloorArea] = 0.002
  system_data[:ZoneVAVMaxReheatTemp] = 43.0
  system_data[:ZoneVAVDamperAction] = 'Normal'

  always_on = model.alwaysOnDiscreteSchedule

  # Chilled Water Plant

  chw_loop = OpenStudio::Model::PlantLoop.new(model)
  chiller1, chiller2 = setup_chw_loop_with_components(model, chw_loop, chiller_type)

  # Condenser System

  cw_loop = OpenStudio::Model::PlantLoop.new(model)
  ctower = setup_cw_loop_with_components(model, cw_loop, chiller1, chiller2)

  # Make a Packaged VAV w/ PFP Boxes for each story of the building
  model.getBuildingStorys.sort.each do |story|
    unless (OpenstudioStandards::Geometry.building_story_get_thermal_zones(story) & zones).empty?

      air_loop = common_air_loop(model: model, system_data: system_data)
      air_loop.setName('Sys_6_VAV with Reheat')

      supply_fan = OpenStudio::Model::FanVariableVolume.new(model, always_on)
      supply_fan.setName('Sys6 Supply Fan')
      return_fan = OpenStudio::Model::FanVariableVolume.new(model, always_on)
      return_fan.setName('Sys6 Return Fan')

      if heating_coil_type == 'Hot Water'
        htg_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)
        hw_loop.addDemandBranchForComponent(htg_coil)
      end
      if heating_coil_type == 'Electric'
        htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
      end

      clg_coil = OpenStudio::Model::CoilCoolingWater.new(model, always_on)
      chw_loop.addDemandBranchForComponent(clg_coil)

      oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
      oa_controller.autosizeMinimumOutdoorAirFlowRate

      # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
      # set explicitly)
      oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

      oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

      # Add the components to the air loop
      # in order from closest to zone to furthest from zone
      supply_inlet_node = air_loop.supplyInletNode
      supply_outlet_node = air_loop.supplyOutletNode
      supply_fan.addToNode(supply_inlet_node)
      htg_coil.addToNode(supply_inlet_node)
      clg_coil.addToNode(supply_inlet_node)
      oa_system.addToNode(supply_inlet_node)
      returnAirNode = oa_system.returnAirModelObject.get.to_Node.get
      return_fan.addToNode(returnAirNode)

      # Add a setpoint manager to control the
      # supply air to a constant temperature
      sat_sch = OpenStudio::Model::ScheduleRuleset.new(model)
      sat_sch.setName('Supply Air Temp')
      sat_sch.defaultDaySchedule.setName('Supply Air Temp Default')
      sat_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), system_data[:system_supply_air_temperature])
      sat_stpt_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, sat_sch)
      sat_stpt_manager.addToNode(supply_outlet_node)

      # Make a VAV terminal with HW reheat for each zone on this story that is in intersection with the zones array.
      # and hook the reheat coil to the HW loop
      (OpenstudioStandards::Geometry.building_story_get_thermal_zones(story) & zones).each do |zone|
        # Zone sizing parameters
        sizing_zone = zone.sizingZone
        sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
        sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
        sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
        sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
        sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
        sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])

        if heating_coil_type == 'Hot Water'
          reheat_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)
          hw_loop.addDemandBranchForComponent(reheat_coil)
        elsif heating_coil_type == 'Electric'
          reheat_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
        end

        # Set zone baseboards
        add_zone_baseboards(model: model,
                            zone: zone,
                            baseboard_type: baseboard_type,
                            hw_loop: hw_loop)

        vav_terminal = OpenStudio::Model::AirTerminalSingleDuctVAVReheat.new(model, always_on, reheat_coil)
        air_loop.addBranchForZone(zone, vav_terminal.to_StraightComponent)
        # NECB2011 minimum zone airflow setting
        vav_terminal.setFixedMinimumAirFlowRate(system_data[:ZoneVAVMinFlowFactorPerFloorArea] * zone.floorArea)
        vav_terminal.setMaximumReheatAirTemperature(system_data[:ZoneVAVMaxReheatTemp])
        vav_terminal.setDamperHeatingAction(system_data[:ZoneVAVDamperAction])

      end
      sys_name_pars = {}
      sys_name_pars['sys_hr'] = 'none'
      sys_name_pars['sys_htg'] = heating_coil_type
      sys_name_pars['sys_clg'] = 'Chilled Water'
      sys_name_pars['sys_sf'] = 'vv'
      sys_name_pars['zone_htg'] = baseboard_type
      sys_name_pars['zone_clg'] = 'none'
      sys_name_pars['sys_rf'] = 'vv'
      assign_base_sys_name(air_loop,
                           sys_abbr: 'sys_6',
                           sys_oa: 'mixed',
                           sys_name_pars: sys_name_pars)
    end
    # next story
  end
  # for debugging
  # puts "end add_sys6_multi_zone_built_up_with_baseboard_heating"
  return true
end

#add_sys6_multi_zone_reference_hp_with_baseboard_heating(model:, zones:, heating_coil_type:, baseboard_type:, hw_loop:, necb_reference_hp_supp_fuel: 'DefaultFuel') ⇒ Object



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb', line 316

def add_sys6_multi_zone_reference_hp_with_baseboard_heating(model:,
                                                            zones:,
                                                            heating_coil_type:,
                                                            baseboard_type:,
                                                            hw_loop:,
                                                            necb_reference_hp_supp_fuel:'DefaultFuel')

  #system data
  system_data = {}
  system_data[:name] = 'Sys_6_VAV with Reheat'
  system_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_data[:CentralHeatingDesignSupplyAirTemperature] = 13.1
  system_data[:AllOutdoorAirinCooling] = false
  system_data[:AllOutdoorAirinHeating] = false

  # zone data
  system_data[:system_supply_air_temperature] = 13.0
  system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_data[:ZoneDXCoolingSizingFactor] = 1.0
  system_data[:ZoneDXHeatingSizingFactor] = 1.3


  always_on = model.alwaysOnDiscreteSchedule

  model.getBuildingStorys.sort.each do |story|
    unless (OpenstudioStandards::Geometry.building_story_get_thermal_zones(story) & zones).empty?

      air_loop = common_air_loop(model: model, system_data: system_data)
      air_loop.setName('Sys_6_CAV')

      supply_fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)
      supply_fan.setName('Sys6 Supply Fan')
      return_fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)
      return_fan.setName('Sys6 Return Fan')

      htg_coil = add_onespeed_htg_DX_coil(model, always_on)
      htg_coil.setName('CoilHeatingDXSingleSpeed_ashp')

      clg_coil = add_onespeed_DX_coil(model, always_on)
      clg_coil.setName('CoilCoolingDXSingleSpeed_ashp')

      oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
      oa_controller.autosizeMinimumOutdoorAirFlowRate

      # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
      # set explicitly)
      oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')
      oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

      # Add the components to the air loop
      # in order from closest to zone to furthest from zone
      supply_inlet_node = air_loop.supplyInletNode
      supply_outlet_node = air_loop.supplyOutletNode
      supply_fan.addToNode(supply_inlet_node)
      htg_coil.addToNode(supply_inlet_node)
      clg_coil.addToNode(supply_inlet_node)
      oa_system.addToNode(supply_inlet_node)
      returnAirNode = oa_system.returnAirModelObject.get.to_Node.get
      return_fan.addToNode(returnAirNode)

      # Add a setpoint manager to control the
      # supply air to a constant temperature
      sat_sch = OpenStudio::Model::ScheduleRuleset.new(model)
      sat_sch.setName('Supply Air Temp')
      #sat_sch.defaultDaySchedule.setName('Supply Air Temp Default')
      #sat_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), system_data[:system_supply_air_temperature])
      #sat_stpt_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, sat_sch)
      sat_stpt_manager = OpenStudio::Model::SetpointManagerWarmest.new(model)
      sat_stpt_manager.setControlVariable("Temperature")
      sat_stpt_manager.setMinimumSetpointTemperature(13)
      sat_stpt_manager.setMaximumSetpointTemperature(24)
      sat_stpt_manager.addToNode(supply_outlet_node)

      # Make CAV terminals for each zone on this story that is in intersection with the zones array.
      (OpenstudioStandards::Geometry.building_story_get_thermal_zones(story) & zones).each do |zone|
        # Zone sizing parameters
        sizing_zone = zone.sizingZone
        sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
        sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
        sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
        sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
        sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
        sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])

        # Set zone baseboards
        add_zone_baseboards(model: model,
                            zone: zone,
                            baseboard_type: baseboard_type,
                            hw_loop: hw_loop)

        # Create CAV RH (RH based on region's default fuel type)
        if necb_reference_hp_supp_fuel == 'DefaultFuel'
          epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get)
          necb_reference_hp_supp_fuel = @standards_data['regional_fuel_use'].detect { |fuel_sources| fuel_sources['state_province_regions'].include?(epw.stateProvinceRegion) }['fueltype_set']
        end
        if necb_reference_hp_supp_fuel == 'NaturalGas'
          rh_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
        elsif necb_reference_hp_supp_fuel == 'Electricity' or  necb_reference_hp_supp_fuel == 'FuelOilNo2'
          rh_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
        else #hot water coils is an option in the future
          raise('Invalid fuel type selected for heat pump supplemental coil')
        end
        cav_rh_terminal = OpenStudio::Model::AirTerminalSingleDuctConstantVolumeReheat.new(model, always_on, rh_coil)
        air_loop.addBranchForZone(zone, cav_rh_terminal.to_StraightComponent)
      end
      sys_name_pars = {}
      sys_name_pars['sys_hr'] = 'none'
      sys_name_pars['sys_htg'] = 'ashp'
      sys_name_pars['sys_clg'] = 'ashp'
      sys_name_pars['sys_sf'] = 'cv'
      sys_name_pars['zone_htg'] = baseboard_type
      sys_name_pars['zone_clg'] = 'none'
      sys_name_pars['sys_rf'] = 'cv'
      assign_base_sys_name(air_loop,
                           sys_abbr: 'sys_6',
                           sys_oa: 'mixed',
                           sys_name_pars: sys_name_pars)

    end
  end



end

#add_system_3_and_8_airloop(heating_coil_type, model, system_data, control_zone, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel') ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_single_speed.rb', line 133

def add_system_3_and_8_airloop(heating_coil_type, model, system_data, control_zone, necb_reference_hp:false, necb_reference_hp_supp_fuel:'DefaultFuel')
  # System Type 3: PSZ-AC
  # This measure creates:
  # -a constant volume packaged single-zone A/C unit
  # for each zone in the building; DX cooling with
  # heating coil: fuel-fired or electric, depending on argument heating_coil_type
  # heating_coil_type choices are "Electric", "Gas", "DX"
  # zone baseboards: hot water or electric, depending on argument baseboard_type
  # baseboard_type choices are "Hot Water" or "Electric"
  # boiler_fueltype choices match OS choices for Boiler component fuel type, i.e.
  # "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOilNo2","Coal","Diesel","Gasoline","OtherFuel1"

  always_on = model.alwaysOnDiscreteSchedule
  air_loop = common_air_loop(model: model, system_data: system_data)
  air_loop.setName("#{system_data[:name]} #{control_zone.name}")

  # Zone sizing temperature difference
  sizing_zone = control_zone.sizingZone
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
  sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
  sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
  if necb_reference_hp
    sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
    sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])
  else
    sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
    sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])
  end

  if necb_reference_hp
    #AirLoopHVACUnitaryHeatPumpAirToAir needs FanOnOff in order for the fan to turn off during off hours
    fan = OpenStudio::Model::FanOnOff.new(model, always_on)
  else
    fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)
  end

  # Set up DX coil
  if necb_reference_hp #NECB curve characteristics
    clg_coil = add_onespeed_DX_coil(model, always_on)
    clg_coil.setName('CoilCoolingDXSingleSpeed_ashp')
  else
    clg_coil = OpenStudio::Model::CoilCoolingDXSingleSpeed.new(model) #sets default OS curve (but will be replaced with NECB curves later)
    clg_coil.setName('CoilCoolingDXSingleSpeed_dx')
  end

  raise("Flag 'necb_reference_hp' is set to true while parameter 'heating_coil_type' is not set to DX") if (necb_reference_hp && (heating_coil_type != 'DX'))
  case heating_coil_type
  when 'Electric' # electric coil
    htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
  when 'Gas'
    htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
  when 'DX'
    #create main DX heating coil
    htg_coil = add_onespeed_htg_DX_coil(model, always_on)
    htg_coil.setName('CoilHeatingDXSingleSpeed_ashp')
    sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneDXHeatingSizingFactor])
    sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneDXCoolingSizingFactor])
  else
    raise("#{heating_coil_type} is not a valid heating coil type.)")
  end

  # TO DO: other fuel-fired heating coil types? (not available in OpenStudio/E+ - may need to play with efficiency to mimic other fuel types)



  # oa_controller
  oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
  oa_controller.autosizeMinimumOutdoorAirFlowRate

  # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
  # set explicitly)
  oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

  # oa_system
  oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

  # Add the components to the air loop
  # in order from closest to zone to furthest from zone
  supply_inlet_node = air_loop.supplyInletNode
  if necb_reference_hp

    #create supplemental heating coil based on default regional fuel type
    if necb_reference_hp_supp_fuel == 'DefaultFuel'
      epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get)
      necb_reference_hp_supp_fuel = @standards_data['regional_fuel_use'].detect { |fuel_sources| fuel_sources['state_province_regions'].include?(epw.stateProvinceRegion) }['fueltype_set']
    end
    if necb_reference_hp_supp_fuel == 'NaturalGas'
      supplemental_htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
    elsif necb_reference_hp_supp_fuel == 'Electricity' or  necb_reference_hp_supp_fuel == 'FuelOilNo2'
      supplemental_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
    else #hot water coils is an option in the future
      raise('Invalid fuel type selected for heat pump supplemental coil')
    end
    # This method will seem like an error in number of args..but this is due to swig voodoo.
    air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitaryHeatPumpAirToAir.new(model, always_on, fan, htg_coil, clg_coil, supplemental_htg_coil)
    air_to_air_heatpump.setName("#{control_zone.name} ASHP")
    air_to_air_heatpump.setControllingZone(control_zone)
    air_to_air_heatpump.setSupplyAirFanOperatingModeSchedule(always_on)
    air_to_air_heatpump.addToNode(supply_inlet_node)
  else
    fan.addToNode(supply_inlet_node)
    htg_coil.addToNode(supply_inlet_node)
    clg_coil.addToNode(supply_inlet_node)
  end
  oa_system.addToNode(supply_inlet_node)

  # Add a setpoint manager single zone reheat to control the
  # supply air temperature based on the needs of this zone
  setpoint_mgr_single_zone_reheat = OpenStudio::Model::SetpointManagerSingleZoneReheat.new(model)
  setpoint_mgr_single_zone_reheat.setControlZone(control_zone)
  setpoint_mgr_single_zone_reheat.setMinimumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMin])
  setpoint_mgr_single_zone_reheat.setMaximumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMax])
  setpoint_mgr_single_zone_reheat.addToNode(air_loop.supplyOutletNode)
  return air_loop
end

#add_system_3_and_8_airloop_multi_speed(heating_coil_type, model, system_data, control_zone) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_3_and_8_multi_speed.rb', line 81

def add_system_3_and_8_airloop_multi_speed(heating_coil_type, model, system_data, control_zone)
  # System Type 3: PSZ-AC
  # This measure creates:
  # -a constant volume packaged single-zone A/C unit
  # for each zone in the building; DX cooling with
  # heating coil: fuel-fired or electric, depending on argument heating_coil_type
  # heating_coil_type choices are "Electric", "Gas", "DX"
  # zone baseboards: hot water or electric, depending on argument baseboard_type
  # baseboard_type choices are "Hot Water" or "Electric"
  # boiler_fueltype choices match OS choices for Boiler component fuel type, i.e.
  # "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOilNo2","Coal","Diesel","Gasoline","OtherFuel1"

  always_on = model.alwaysOnDiscreteSchedule
  air_loop = common_air_loop(model: model, system_data: system_data)
  air_loop.setName("#{system_data[:name]} #{control_zone.name}")

  # Zone sizing temperature difference
  sizing_zone = control_zone.sizingZone
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod])
  sizing_zone.setZoneCoolingDesignSupplyAirTemperatureDifference(system_data[:ZoneCoolingDesignSupplyAirTemperatureDifference])
  sizing_zone.setZoneHeatingDesignSupplyAirTemperatureInputMethod(system_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod])
  sizing_zone.setZoneHeatingDesignSupplyAirTemperatureDifference(system_data[:ZoneHeatingDesignSupplyAirTemperatureDifference])
  sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
  sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])

  fan = OpenStudio::Model::FanConstantVolume.new(model, always_on)

  # Setup heating and cooling coils
  if (heating_coil_type == 'Gas') || (heating_coil_type == 'Electric')
    clg_coil = OpenStudio::Model::CoilCoolingDXMultiSpeed.new(model)
    clg_coil.setFuelType('Electricity')
    clg_stage_1 = OpenStudio::Model::CoilCoolingDXMultiSpeedStageData.new(model)
    clg_stage_2 = OpenStudio::Model::CoilCoolingDXMultiSpeedStageData.new(model)
    clg_coil.addStage(clg_stage_1)
    clg_coil.addStage(clg_stage_2)
    clg_coil.setApplyPartLoadFractiontoSpeedsGreaterthan1(false)
    htg_coil = OpenStudio::Model::CoilHeatingGasMultiStage.new(model)
    htg_stage_1 = OpenStudio::Model::CoilHeatingGasMultiStageStageData.new(model)
    htg_coil.addStage(htg_stage_1)
    if heating_coil_type == 'Gas'
      supplemental_htg_coil = OpenStudio::Model::CoilHeatingGas.new(model, always_on)
      supplemental_htg_coil.setNominalCapacity(0.001)
    elsif heating_coil_type == 'Electric'
      supplemental_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
      htg_stage_1.setNominalCapacity(0.001)
    end
  # Single stage DX and Electric heating
  elsif heating_coil_type == 'DX'
    clg_coil = OpenStudio::Model::CoilCoolingDXSingleSpeed.new(model)
    clg_coil.setMinimumOutdoorDryBulbTemperatureforCompressorOperation(system_data[:MinimumOutdoorDryBulbTemperatureforCompressorOperation])
    htg_coil = OpenStudio::Model::CoilHeatingDXSingleSpeed.new(model)
    supplemental_htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
  else
    raise("#{heating_coil_type} is not a valid heating coil type.)")
  end

  # oa_controller
  oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
  oa_controller.autosizeMinimumOutdoorAirFlowRate

  # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
  # set explicitly)
  oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

  # oa_system
  oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

  # Add the components to the air loop
  # in order from closest to zone to furthest from zone
  supply_inlet_node = air_loop.supplyInletNode

  if (heating_coil_type == 'Gas') || (heating_coil_type == 'Electric')
    # This method will seem like an error in number of args..but this is due to swig voodoo.
    air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed.new(model, fan, htg_coil, clg_coil, supplemental_htg_coil)
    air_to_air_heatpump.setControllingZoneorThermostatLocation(control_zone)
    air_to_air_heatpump.setNumberofSpeedsforHeating(1)
    air_to_air_heatpump.setNumberofSpeedsforCooling(2)
    air_to_air_heatpump.setMinimumOutdoorDryBulbTemperatureforCompressorOperation(system_data[:MinimumOutdoorDryBulbTemperatureforCompressorOperation])
  elsif heating_coil_type == 'DX'
    # This method will seem like an error in number of args..but this is due to swig voodoo.
    air_to_air_heatpump = OpenStudio::Model::AirLoopHVACUnitaryHeatPumpAirToAir.new(model, always_on, fan, htg_coil, clg_coil, supplemental_htg_coil)
    air_to_air_heatpump.setControllingZone(zone)
  end
  air_to_air_heatpump.setName("#{control_zone.name} ASHP")
  air_to_air_heatpump.setSupplyAirFanOperatingModeSchedule(always_on)
  air_to_air_heatpump.addToNode(supply_inlet_node)

  oa_system.addToNode(supply_inlet_node)

  # Add a setpoint manager single zone reheat to control the
  # supply air temperature based on the needs of this zone
  setpoint_mgr_single_zone_reheat = OpenStudio::Model::SetpointManagerSingleZoneReheat.new(model)
  setpoint_mgr_single_zone_reheat.setControlZone(control_zone)
  setpoint_mgr_single_zone_reheat.setMinimumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMin])
  setpoint_mgr_single_zone_reheat.setMaximumSupplyAirTemperature(system_data[:SetpointManagerSingleZoneReheatSupplyTempMax])
  setpoint_mgr_single_zone_reheat.addToNode(air_loop.supplyOutletNode)

  return air_loop
end

#add_zone_baseboards(baseboard_type:, hw_loop:, model:, zone:) ⇒ Object

Zonal systems



2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2045

def add_zone_baseboards(baseboard_type:,
                        hw_loop:,
                        model:,
                        zone:)
  always_on = model.alwaysOnDiscreteSchedule
  if baseboard_type == 'Electric'
    zone_elec_baseboard = OpenStudio::Model::ZoneHVACBaseboardConvectiveElectric.new(model)
    zone_elec_baseboard.addToThermalZone(zone)
  end

  return unless baseboard_type == 'Hot Water'

  baseboard_coil = OpenStudio::Model::CoilHeatingWaterBaseboard.new(model)
  # Connect baseboard coil to hot water loop
  hw_loop.addDemandBranchForComponent(baseboard_coil)
  zone_baseboard = OpenStudio::Model::ZoneHVACBaseboardConvectiveWater.new(model, always_on, baseboard_coil)
  # add zone_baseboard to zone
  zone_baseboard.addToThermalZone(zone)
end

#adjust_wildcard_spacetype_schedule(space:, schedule:, lights_type: 'NECB_Default', lights_scale: 1.0) ⇒ Object

Set wildcard spactype schedule to NECB letter index.



726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 726

def adjust_wildcard_spacetype_schedule(space:, schedule:, lights_type: 'NECB_Default', lights_scale: 1.0)
  if space.spaceType.empty?
    OpenStudio.logFree(OpenStudio::Error, "Error: No spacetype assigned for #{space.name.get}. This must be assigned. Aborting.")
  end
  # Get current spacetype name
  space_type_name = space.spaceType.get.standardsSpaceType.get.to_s
  # Determine new spacetype name.
  regex = /^(.*sch-)(\S)$/
  new_spacetype_name = "#{space_type_name.match(regex).captures.first}#{schedule}"
  new_spacetype = nil

  # if the new spacetype does not match the old space type. we gotta update the space with the new spacetype.
  if space_type_name != new_spacetype_name
    new_spacetype = space.model.getSpaceTypes.detect do |spacetype|
      !spacetype.standardsBuildingType.empty? && # need to do this to prevent an exception.
        (spacetype.standardsBuildingType.get == space.spaceType.get.standardsBuildingType.get) &&
        !spacetype.standardsSpaceType.empty? && # need to do this to prevent an exception.
        (spacetype.standardsSpaceType.get == new_spacetype_name)
    end
    if new_spacetype.nil?
      # Space type is not in model. need to create from scratch.
      new_spacetype = OpenStudio::Model::SpaceType.new(space.model)
      new_spacetype.setStandardsBuildingType(space.spaceType.get.standardsBuildingType.get)
      new_spacetype.setStandardsSpaceType(new_spacetype_name)
      new_spacetype.setName("#{space.spaceType.get.standardsBuildingType.get} #{new_spacetype_name}")
      space_type_apply_internal_loads(space_type: new_spacetype, lights_type: lights_type, lights_scale: lights_scale)
      space_type_apply_internal_load_schedules(new_spacetype, true, true, true, true, true, true, true)
    end
    space.setSpaceType(new_spacetype)
    # sanity check.
    raise 'could not reassign space type schedule.' if schedule != space.spaceType.get.name.get.match(regex)[2]
  end
  return space
end

#air_loop_hvac_apply_economizer_integration(air_loop_hvac, climate_zone) ⇒ Boolean

Note:

this method assumes you previously checked that an economizer is required at all via #economizer_required?

NECB always requires an integrated economizer (NoLockout); as per 5.2.2.8(3) this means that compressor allowed to turn on when economizer is open

Returns:

  • (Boolean)

    returns true if successful, false if not



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 90

def air_loop_hvac_apply_economizer_integration(air_loop_hvac, climate_zone)
  # Get the OA system and OA controller
  oa_sys = air_loop_hvac.airLoopHVACOutdoorAirSystem
  # No OA system
  return false if !oa_sys.is_initialized

  oa_sys = oa_sys.get
  oa_control = oa_sys.getControllerOutdoorAir

  # Apply integrated economizer
  oa_control.setLockoutType('NoLockout')

  return true
end

#air_loop_hvac_apply_energy_recovery_ventilator(air_loop_hvac, climate = nil) ⇒ Boolean

TODO:

Add exception logic for systems serving parking garage, warehouse, or multifamily

Add an ERV to this airloop. Will be a rotary-type HX

Returns:

  • (Boolean)

    Returns true if required, false if not.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 290

def air_loop_hvac_apply_energy_recovery_ventilator(air_loop_hvac, climate = nil)
  # Get the oa system
  oa_system = nil
  if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized
    oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
  else
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV cannot be added because the system has no OA intake.")
    return false
  end

  # Create an ERV

  erv = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(air_loop_hvac.model)
  erv.setName("#{air_loop_hvac.name} ERV")
  erv.setSensibleEffectivenessat100HeatingAirFlow(0.5)
  erv.setLatentEffectivenessat100HeatingAirFlow(0.5)
  erv.setSensibleEffectivenessat75HeatingAirFlow(0.5)
  erv.setLatentEffectivenessat75HeatingAirFlow(0.5)
  erv.setSensibleEffectivenessat100CoolingAirFlow(0.5)
  erv.setLatentEffectivenessat100CoolingAirFlow(0.5)
  erv.setSensibleEffectivenessat75CoolingAirFlow(0.5)
  erv.setLatentEffectivenessat75CoolingAirFlow(0.5)
  erv.setSupplyAirOutletTemperatureControl(true)
  erv.setHeatExchangerType('Rotary')
  erv.setFrostControlType('ExhaustOnly')
  erv.setEconomizerLockout(true)
  erv.setThresholdTemperature(-23.3) # -10F
  erv.setInitialDefrostTimeFraction(0.167)
  erv.setRateofDefrostTimeFractionIncrease(1.44)

  # Add the ERV to the OA system
  erv.addToNode(oa_system.outboardOANode.get)

  # Add a setpoint manager OA pretreat
  # to control the ERV
  spm_oa_pretreat = OpenStudio::Model::SetpointManagerOutdoorAirPretreat.new(air_loop_hvac.model)
  spm_oa_pretreat.setMinimumSetpointTemperature(-99.0)
  spm_oa_pretreat.setMaximumSetpointTemperature(99.0)
  spm_oa_pretreat.setMinimumSetpointHumidityRatio(0.00001)
  spm_oa_pretreat.setMaximumSetpointHumidityRatio(1.0)
  # Reference setpoint node and
  # Mixed air stream node are outlet
  # node of the OA system
  mixed_air_node = oa_system.mixedAirModelObject.get.to_Node.get
  spm_oa_pretreat.setReferenceSetpointNode(mixed_air_node)
  spm_oa_pretreat.setMixedAirStreamNode(mixed_air_node)
  # Outdoor air node is
  # the outboard OA node of teh OA system
  spm_oa_pretreat.setOutdoorAirStreamNode(oa_system.outboardOANode.get)
  # Return air node is the inlet
  # node of the OA system
  return_air_node = oa_system.returnAirModelObject.get.to_Node.get
  spm_oa_pretreat.setReturnAirStreamNode(return_air_node)
  # Attach to the outlet of the ERV
  erv_outlet = erv.primaryAirOutletModelObject.get.to_Node.get
  spm_oa_pretreat.addToNode(erv_outlet)

  # Apply the prototype Heat Exchanger power assumptions.
  heat_exchanger_air_to_air_sensible_and_latent_apply_prototype_nominal_electric_power(erv)

  # Determine if the system is a DOAS based on
  # whether there is 100% OA in heating and cooling sizing.
  is_doas = false
  sizing_system = air_loop_hvac.sizingSystem
  if sizing_system.allOutdoorAirinCooling && sizing_system.allOutdoorAirinHeating
    is_doas = true
  end

  # Set the bypass control type
  # If DOAS system, BypassWhenWithinEconomizerLimits
  # to disable ERV during economizing.
  # Otherwise, BypassWhenOAFlowGreaterThanMinimum
  # to disable ERV during economizing and when OA
  # is also greater than minimum.
  bypass_ctrl_type = if is_doas
                       'BypassWhenWithinEconomizerLimits'
                     else
                       'BypassWhenOAFlowGreaterThanMinimum'
                     end
  oa_system.getControllerOutdoorAir.setHeatRecoveryBypassControlType(bypass_ctrl_type)

  return true
end

#air_loop_hvac_apply_multizone_vav_outdoor_air_sizing(air_loop_hvac) ⇒ Object

NECB does not change damper positions

return [Boolean] returns true if successful, false if not



13
14
15
16
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 13

def air_loop_hvac_apply_multizone_vav_outdoor_air_sizing(air_loop_hvac)
  # Do not change anything.
  return true
end

#air_loop_hvac_apply_single_zone_controls(air_loop_hvac, climate_zone) ⇒ Boolean

NECB has no single zone air loop control requirements

Returns:

  • (Boolean)

    returns true if successful, false if not



475
476
477
478
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 475

def air_loop_hvac_apply_single_zone_controls(air_loop_hvac, climate_zone)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: No special economizer controls were modeled.")
  return true
end

#air_loop_hvac_apply_vav_damper_action(air_loop_hvac) ⇒ Boolean

TODO:

see if this impacts the sizing run.

Set the VAV damper control to single maximum or dual maximum control depending on the standard.

Returns:

  • (Boolean)

    Returns true if successful, false if not



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 427

def air_loop_hvac_apply_vav_damper_action(air_loop_hvac)
  damper_action = 'Single Maximum'

  # Interpret this as an EnergyPlus input
  damper_action_eplus = nil
  if damper_action == 'Single Maximum'
    damper_action_eplus = 'Normal'
  elsif damper_action == 'Dual Maximum'
    # EnergyPlus 8.7 changed the meaning of 'Reverse'.
    # For versions of OpenStudio using E+ 8.6 or lower
    damper_action_eplus = if air_loop_hvac.model.version < OpenStudio::VersionString.new('2.0.5')
                            'Reverse'
                            # For versions of OpenStudio using E+ 8.7 or higher
                          else
                            'ReverseWithLimits'
                          end
  end

  # Set the control for any VAV reheat terminals
  # on this airloop.
  control_type_set = false
  air_loop_hvac.demandComponents.each do |equip|
    if equip.to_AirTerminalSingleDuctVAVReheat.is_initialized
      term = equip.to_AirTerminalSingleDuctVAVReheat.get
      # Dual maximum only applies to terminals with HW reheat coils
      if damper_action == 'Dual Maximum'
        if term.reheatCoil.to_CoilHeatingWater.is_initialized
          term.setDamperHeatingAction(damper_action_eplus)
          control_type_set = true
        end
      else
        term.setDamperHeatingAction(damper_action_eplus)
        control_type_set = true
        term.setMaximumFlowFractionDuringReheat(0.5)
      end
    end
  end

  if control_type_set
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: VAV damper action was set to #{damper_action} control.")
  end

  return true
end

#air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac, climate_zone) ⇒ Boolean

TODO:

Add exception logic for systems that serve multifamily, parking garage, warehouse

Determine if demand control ventilation (DCV) is required for this air loop.

Returns:

  • (Boolean)

    Returns true if required, false if not.



416
417
418
419
420
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 416

def air_loop_hvac_demand_control_ventilation_required?(air_loop_hvac, climate_zone)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{template} #{climate_zone}:  #{air_loop_hvac.name}: DCV is not required for any system.")
  dcv_required = false
  return dcv_required
end

#air_loop_hvac_economizer_required?(air_loop_hvac) ⇒ Boolean

Determine whether or not this system is required to have an economizer.

Returns:

  • (Boolean)

    returns true if an economizer is required, false if not



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 22

def air_loop_hvac_economizer_required?(air_loop_hvac)
  economizer_required = false

  # need a better way to determine if an economizer is needed.
  return economizer_required if ((air_loop_hvac.name.to_s.include? 'Outpatient F1' ) ||
                                 (air_loop_hvac.sizingSystem.typeofLoadtoSizeOn.to_s == "VentilationRequirement"))

  # A big number of btu per hr as the minimum requirement
  infinity_btu_per_hr = 999_999_999_999
  minimum_capacity_btu_per_hr = infinity_btu_per_hr

  # Determine if the airloop serves any computer rooms
  # / data centers, which changes the economizer.
  is_dc = false
  if air_loop_hvac_data_center_area_served(air_loop_hvac) > 0
    is_dc = true
  end

  # Determine the minimum capacity that requires an economizer
  minimum_capacity_btu_per_hr = 68_243 # NECB requires economizer for cooling cap > 20 kW

  # puts air_loop_hvac.name.to_s
  # Design Supply Air Flow Rate: This method below reads the value from the sql file.
  dsafr_m3_per_s = air_loop_hvac.autosizedDesignSupplyAirFlowRate
  min_dsafr_l_per_s = 1500
  unless dsafr_m3_per_s.empty?
    dsafr_l_per_s = dsafr_m3_per_s.get * 1000
    if dsafr_l_per_s > min_dsafr_l_per_s
      economizer_required = true
      puts "economizer_required = true for #{air_loop_hvac.name} because dsafr_l_per_s(#{dsafr_l_per_s}) > 1500"
      if is_dc
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the 'Design Supply Air Flow Rate' of #{dsafr_l_per_s} L/s exceeds the minimum air flow rate of #{min_dsafr_l_per_s} L/s for data centers.")
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the 'Design Supply Air Flow Rate' of #{dsafr_l_per_s} L/s exceeds the minimum air flow rate of #{min_dsafr_l_per_s} L/s.")
      end
    end
  end
  # Check whether the system requires an economizer by comparing
  # the system capacity to the minimum capacity.
  total_cooling_capacity_w = air_loop_hvac_total_cooling_capacity(air_loop_hvac)
  total_cooling_capacity_btu_per_hr = OpenStudio.convert(total_cooling_capacity_w, 'W', 'Btu/hr').get
  if total_cooling_capacity_btu_per_hr >= minimum_capacity_btu_per_hr
    if is_dc
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr for data centers.")
    else
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} requires an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr.")
    end
    puts "economizer_required = true for #{air_loop_hvac.name} because total_cooling_capacity_btu_per_hr(#{total_cooling_capacity_btu_per_hr}) >= #{minimum_capacity_btu_per_hr}"
    economizer_required = true
  else
    if is_dc
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} does not require an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr is less than the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr for data centers.")
    else
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "#{air_loop_hvac.name} does not require an economizer because the total cooling capacity of #{total_cooling_capacity_btu_per_hr.round} Btu/hr is less than the minimum capacity of #{minimum_capacity_btu_per_hr.round} Btu/hr.")
    end
  end

  return economizer_required
end

#air_loop_hvac_enable_unoccupied_fan_shutoff(air_loop_hvac, min_occ_pct = 0.05) ⇒ Boolean

Shut off the system during unoccupied periods. During these times, systems will cycle on briefly if temperature drifts below setpoint. For systems with fan-powered terminals, the whole system (not just the terminal fans) will cycle on. Terminal-only night cycling is not used because the terminals cannot provide cooling, so terminal-only night cycling leads to excessive unmet cooling hours during unoccupied periods. If the system already has a schedule other than Always-On, no change will be made. If the system has an Always-On schedule assigned, a new schedule will be created. In this case, occupied is defined as the total percent occupancy for the loop for all zones served.

the system will be considered unoccupied.

Parameters:

  • min_occ_pct (Double) (defaults to: 0.05)

    the fractional value below which

Returns:

  • (Boolean)

    true if successful, false if not



1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1064

def air_loop_hvac_enable_unoccupied_fan_shutoff(air_loop_hvac, min_occ_pct = 0.05)
  # Set the system to night cycle
  air_loop_hvac.setNightCycleControlType('CycleOnAny')

  # Check if already using a schedule other than always on
  avail_sch = air_loop_hvac.availabilitySchedule
  unless avail_sch == air_loop_hvac.model.alwaysOnDiscreteSchedule
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Availability schedule is already set to #{avail_sch.name}.  Will assume this includes unoccupied shut down; no changes will be made.")
    return true
  end

  # Get the airloop occupancy schedule
  loop_occ_sch = air_loop_hvac_get_occupancy_schedule(air_loop_hvac, occupied_percentage_threshold: min_occ_pct)
  flh = OpenstudioStandards::Schedules.schedule_get_equivalent_full_load_hours(loop_occ_sch)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}: Annual occupied hours = #{flh.round} hr/yr, assuming a #{min_occ_pct} occupancy threshold.  This schedule will be used as the HVAC operation schedule.")

  # Set HVAC availability schedule to follow occupancy
  air_loop_hvac.setAvailabilitySchedule(loop_occ_sch)
  air_loop_hvac.supplyComponents.each do |comp|
    if comp.to_AirLoopHVACUnitaryHeatPumpAirToAir.is_initialized
      comp.to_AirLoopHVACUnitaryHeatPumpAirToAir.get.setSupplyAirFanOperatingModeSchedule(loop_occ_sch)
    elsif comp.to_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed.is_initialized
      comp.to_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed.get.setAvailabilitySchedule(loop_occ_sch)
    end
  end

  return true
end

#air_loop_hvac_energy_recovery_ventilator_required?(air_loop_hvac, climate_zone) ⇒ Boolean

TODO:

Add exception logic for systems serving parking garage, warehouse, or multifamily

Check if ERV is required on this airloop.

Returns:

  • (Boolean)

    Returns true if required, false if not.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 110

def air_loop_hvac_energy_recovery_ventilator_required?(air_loop_hvac, climate_zone)
  # ERV Not Applicable for AHUs that serve
  # parking garage, warehouse, or multifamily
  # if space_types_served_names.include?('PNNL_Asset_Rating_Apartment_Space_Type') ||
  # space_types_served_names.include?('PNNL_Asset_Rating_LowRiseApartment_Space_Type') ||
  # space_types_served_names.include?('PNNL_Asset_Rating_ParkingGarage_Space_Type') ||
  # space_types_served_names.include?('PNNL_Asset_Rating_Warehouse_Space_Type')
  # OpenStudio::logFree(OpenStudio::Info, "openstudio.standards.AirLoopHVAC", "For #{self.name}, ERV not applicable because it because it serves parking garage, warehouse, or multifamily.")
  # return false
  # end

  erv_required = nil
  # ERV not applicable for medical AHUs (AHU1 in Outpatient), per AIA 2001 - 7.31.D2.
  if air_loop_hvac.name.to_s.include? 'Outpatient F1'
    erv_required = false
    return erv_required
  end

  # ERV not applicable for medical AHUs, per AIA 2001 - 7.31.D2.
  if air_loop_hvac.name.to_s.include? 'VAV_ER'
    erv_required = false
    return erv_required
  elsif air_loop_hvac.name.to_s.include? 'VAV_OR'
    erv_required = false
    return erv_required
  end

  # ERV Not Applicable for AHUs that have DCV
  # or that have no OA intake.
  controller_oa = nil
  controller_mv = nil
  oa_system = nil
  if air_loop_hvac.airLoopHVACOutdoorAirSystem.is_initialized
    oa_system = air_loop_hvac.airLoopHVACOutdoorAirSystem.get
    controller_oa = oa_system.getControllerOutdoorAir
    controller_mv = controller_oa.controllerMechanicalVentilation
    if controller_mv.demandControlledVentilation == true
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV not applicable because DCV enabled.")
      return false
    end
  else
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV not applicable because it has no OA intake.")
    return false
  end

  # Get the AHU design supply air flow rate
  dsn_flow_m3_per_s = nil
  if air_loop_hvac.designSupplyAirFlowRate.is_initialized
    dsn_flow_m3_per_s = air_loop_hvac.designSupplyAirFlowRate.get
  elsif air_loop_hvac.autosizedDesignSupplyAirFlowRate.is_initialized
    dsn_flow_m3_per_s = air_loop_hvac.autosizedDesignSupplyAirFlowRate.get
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name} design supply air flow rate is not available, cannot apply efficiency standard.")
    return false
  end
  dsn_flow_cfm = OpenStudio.convert(dsn_flow_m3_per_s, 'm^3/s', 'cfm').get

  # Get the minimum OA flow rate
  min_oa_flow_m3_per_s = nil
  if controller_oa.minimumOutdoorAirFlowRate.is_initialized
    min_oa_flow_m3_per_s = controller_oa.minimumOutdoorAirFlowRate.get
  elsif controller_oa.autosizedMinimumOutdoorAirFlowRate.is_initialized
    min_oa_flow_m3_per_s = controller_oa.autosizedMinimumOutdoorAirFlowRate.get
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.AirLoopHVAC', "For #{controller_oa.name}: minimum OA flow rate is not available, cannot apply efficiency standard.")
    return false
  end
  min_oa_flow_cfm = OpenStudio.convert(min_oa_flow_m3_per_s, 'm^3/s', 'cfm').get

  # Calculate the percent OA at design airflow
  pct_oa = min_oa_flow_m3_per_s / dsn_flow_m3_per_s

  # The NECB2011 requirement is that systems with an exhaust heat content > 150 kW require an HRV
  # The calculation for this is done below, to modify erv_required
  # erv_cfm set to nil here as placeholder, will lead to erv_required = false
  erv_cfm = nil

  # Determine if an ERV is required
  # erv_required = nil
  if erv_cfm.nil?
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV not required based on #{(pct_oa * 100).round}% OA flow, design supply air flow of #{dsn_flow_cfm.round}cfm, and climate zone #{climate_zone}.")
    erv_required = false
  elsif dsn_flow_cfm < erv_cfm
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV not required based on #{(pct_oa * 100).round}% OA flow, design supply air flow of #{dsn_flow_cfm.round}cfm, and climate zone #{climate_zone}. Does not exceed minimum flow requirement of #{erv_cfm}cfm.")
    erv_required = false
  else
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV required based on #{(pct_oa * 100).round}% OA flow, design supply air flow of #{dsn_flow_cfm.round}cfm, and climate zone #{climate_zone}. Exceeds minimum flow requirement of #{erv_cfm}cfm.")
    erv_required = true
  end

  # This code modifies erv_required for NECB2011
  # Calculation of exhaust heat content and check whether it is > 150 kW

  # get all zones in the model
  zones = air_loop_hvac.thermalZones

  # initialize counters
  sum_zone_oa = 0.0
  sum_zone_oa_times_heat_design_t = 0.0

  # zone loop
  zones.each do |zone|
    # get design heat temperature for each zone; this is equivalent to design exhaust temperature
    heat_design_t = 21.0
    zone_thermostat = zone.thermostat.get
    if zone_thermostat.to_ThermostatSetpointDualSetpoint.is_initialized
      dual_thermostat = zone_thermostat.to_ThermostatSetpointDualSetpoint.get
      if dual_thermostat.heatingSetpointTemperatureSchedule.is_initialized
        htg_temp_sch = dual_thermostat.heatingSetpointTemperatureSchedule.get
        htg_temp_sch_ruleset = htg_temp_sch.to_ScheduleRuleset.get
        winter_dd_sch = htg_temp_sch_ruleset.winterDesignDaySchedule
        heat_design_t = winter_dd_sch.values.max
      end
    end
    # initialize counter
    zone_oa = 0.0
    # outdoor defined at space level; get OA flow for all spaces within zone
    spaces = zone.spaces

    # space loop
    spaces.each do |space|
      unless space.designSpecificationOutdoorAir.empty? # if empty, don't do anything
        outdoor_air = space.designSpecificationOutdoorAir.get
        # in bTAP, outdoor air specified as outdoor air per
        oa_flow_per_floor_area = outdoor_air.outdoorAirFlowperFloorArea
        oa_flow = oa_flow_per_floor_area * space.floorArea * zone.multiplier # oa flow for the space
        zone_oa += oa_flow # add up oa flow for all spaces to get zone air flow
      end
      # space loop
    end
    sum_zone_oa += zone_oa # sum of all zone oa flows to get system oa flow
    sum_zone_oa_times_heat_design_t += (zone_oa * heat_design_t) # calculated to get oa flow weighted average of design exhaust temperature
    # zone loop
  end

  # Calculate average exhaust temperature (oa flow weighted average)
  avg_exhaust_temp = sum_zone_oa_times_heat_design_t / sum_zone_oa

  # for debugging/testing
  #      puts "average exhaust temp = #{avg_exhaust_temp}"
  #      puts "sum_zone_oa = #{sum_zone_oa}"

  # Get January winter design temperature
  # get model weather file name
  weather_file_path = air_loop_hvac.model.weatherFile.get.path.get.to_s
  stat_file_path = weather_file_path.gsub('.epw', '.stat')
  stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)

  # get winter(heating) design temp stored in array
  # Note that the NECB2011 specifies using the 2.5% january design temperature
  # The outdoor temperature used here is the 0.4% heating design temperature of the coldest month, available in stat file
  outdoor_temp = stat_file.heating_design_info[1]

  #      for debugging/testing
  #      puts "outdoor design temp = #{outdoor_temp}"

  # Calculate exhaust heat content
  exhaust_heat_content = 0.00123 * sum_zone_oa * 1000.0 * (avg_exhaust_temp - outdoor_temp)

  # for debugging/testing
  #      puts "exhaust heat content = #{exhaust_heat_content}"

  # Modify erv_required based on exhaust heat content
  if exhaust_heat_content > 150.0
    erv_required = true
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV required based on exhaust heat content.")
  else
    erv_required = false
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{air_loop_hvac.name}, ERV not required based on exhaust heat content.")
  end

  return erv_required
end

#air_loop_hvac_motorized_oa_damper_limits(air_loop_hvac, climate_zone) ⇒ Array<Double>

Determine the air flow and number of story limits for whether motorized OA damper is required. If both nil, never required

Returns:

  • (Array<Double>)

    [minimum_oa_flow_cfm, maximum_stories].



493
494
495
496
497
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 493

def air_loop_hvac_motorized_oa_damper_limits(air_loop_hvac, climate_zone)
  minimum_oa_flow_cfm = 0
  maximum_stories = 0
  return [minimum_oa_flow_cfm, maximum_stories]
end

#air_loop_hvac_static_pressure_reset_required?(air_loop_hvac, has_ddc) ⇒ Boolean

NECB doesn’t require static pressure reset.

return [Boolean] returns true if static pressure reset is required, false if not

Returns:

  • (Boolean)


483
484
485
486
487
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 483

def air_loop_hvac_static_pressure_reset_required?(air_loop_hvac, has_ddc)
  # static pressure reset not required
  sp_reset_required = false
  return sp_reset_required
end

#air_terminal_single_duct_vav_reheat_set_heating_cap(air_terminal_single_duct_vav_reheat) ⇒ Boolean

Sets the capacity of the reheat coil based on the minimum flow fraction, and the maximum flow rate.

Parameters:

  • air_terminal_single_duct_vav_reheat (OpenStudio::Model::AirTerminalSingleDuctVAVReheat)

    the air terminal object

Returns:

  • (Boolean)

    returns true if successful, false if not



2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2497

def air_terminal_single_duct_vav_reheat_set_heating_cap(air_terminal_single_duct_vav_reheat)
  flow_rate_fraction = 0.0
  if air_terminal_single_duct_vav_reheat.constantMinimumAirFlowFraction.is_initialized
    flow_rate_fraction = air_terminal_single_duct_vav_reheat.constantMinimumAirFlowFraction.get
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.AirTerminalSingleDuctVAVReheat', \
    "Minimum flow fraction is not defined for terminal device #{air_terminal_single_duct_vav_reheat.name}")
    return false
  end
  cap = 1.2 * 1000.0 * flow_rate_fraction * air_terminal_single_duct_vav_reheat.autosizedMaximumAirFlowRate.to_f * (43.0 - 13.0)
  if air_terminal_single_duct_vav_reheat.reheatCoil.to_CoilHeatingElectric.is_initialized
    reheat_coil = air_terminal_single_duct_vav_reheat.reheatCoil.to_CoilHeatingElectric.get
    reheat_coil.setNominalCapacity(cap)
  elsif air_terminal_single_duct_vav_reheat.reheatCoil.to_CoilHeatingWater.is_initialized
    reheat_coil = air_terminal_single_duct_vav_reheat.reheatCoil.to_CoilHeatingWater.get
    reheat_coil.setPerformanceInputMethod('NominalCapacity')
    reheat_coil.setRatedCapacity(cap)
  end
  air_terminal_single_duct_vav_reheat.setMaximumReheatAirTemperature(43.0)
  return true
end

#apply_auto_zoning(model:, sizing_run_dir: Dir.pwd, lights_type: 'NECB_Default', lights_scale: 1.0) ⇒ Object

Top level method that merges spaces into zones where possible. This requires a sizing run. This follows the spirit of the EE4 modelling manual found here www.nrcan.gc.ca/energy/software-tools/7457 where the A zone includes those areas in the building that meet three criteria:

  • Served by the same HVAC system

  • Similar operation and function

  • Similar heating/cooling loads

Some expections are dwelling units wet zone and wild zones. These spaces will have special considerations when autozoning a building.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 54

def apply_auto_zoning(model:, sizing_run_dir: Dir.pwd, lights_type: 'NECB_Default', lights_scale: 1.0)
  raise('validation of model failed.') unless validate_initial_model(model)

  # Check to see if model is using another vintage of spacetypes. If so overwrite the @standards for the object with the
  # other spacetype data. This is required for correct system mapping.
  template = determine_spacetype_vintage(model)
  unless template == self.class.name
    # Frankenstein the standards data wrt spacetype data.
    @standards_data['space_types'] = Standard.build(template).standards_data['space_types']
  end

  # The first thing we need to do is get a sizing run to determine the heating loads of all the spaces. The default
  # btap geometry has a one to one relationship of zones to spaces.. So we simply create the thermal zones for all the spaces.
  # to do this we need to create thermals zone for each space.

  # Remove any Thermal zones assigned before
  model.getThermalZones.each(&:remove)
  # create new thermal zones one to one with spaces.
  model_create_thermal_zones(model)
  # do a sizing run.
  if model_run_sizing_run(model, "#{sizing_run_dir}/autozone") == false
    raise('autorun sizing run failed!')
  end

  # collect sizing information on each space.
  store_space_sizing_loads(model)
  # Remove any Thermal zones assigned again to start fresh.
  model.getThermalZones.each(&:remove)
  auto_zone_dwelling_units(model)
  auto_zone_wet_spaces(model: model, lights_type: lights_type, lights_scale: lights_scale)
  auto_zone_all_other_spaces(model)
  auto_zone_wild_spaces(model: model, lights_type: lights_type, lights_scale: lights_scale)
  # This will color the spaces and zones.
  random = Random.new(1234)
  # Set ideal hvac in case we want to not implement the hvac yet and still run osm right after this function.
  # model.getThermalZones.each { |zone| zone.setUseIdealAirLoads(true) }
  model.getThermalZones.sort.each { |item| item.setRenderingColor(set_random_rendering_color(item, random)) }
  model.getSpaceTypes.sort.each { |item| item.setRenderingColor(set_random_rendering_color(item, random)) }
end

#apply_building_default_constructionset(model) ⇒ Object



486
487
488
489
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 486

def apply_building_default_constructionset(model)
  bldg_def_const_set = model_add_construction_set_from_osm(model: model)
  model.getBuilding.setDefaultConstructionSet(bldg_def_const_set)
end

#apply_default_constructionsets_to_spacetypes(climate_zone, model) ⇒ Object



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 491

def apply_default_constructionsets_to_spacetypes(climate_zone, model)
  model.getSpaceTypes.sort.each do |space_type|
    # Get the standards building type
    stds_building_type = nil
    if space_type.standardsBuildingType.is_initialized
      stds_building_type = space_type.standardsBuildingType.get
    else
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Space type called '#{space_type.name}' has no standards building type.")
    end

    # Get the standards space type
    stds_spc_type = nil
    if space_type.standardsSpaceType.is_initialized
      stds_spc_type = space_type.standardsSpaceType.get
    else
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Space type called '#{space_type.name}' has no standards space type.")
    end

    # If the standards space type is Attic,
    # the building type should be blank.
    if stds_spc_type == 'Attic'
      stds_building_type = ''
    end

    # Attempt to make a construction set for this space type
    # and assign it if it can be created.
    spc_type_const_set = model_add_construction_set_from_osm(model: model)
    if spc_type_const_set.is_initialized
      space_type.setDefaultConstructionSet(spc_type_const_set.get)
    end
  end
end

#apply_economizers(climate_zone, model) ⇒ Object



1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1551

def apply_economizers(climate_zone, model)
  # NECB2011 prescribes ability to provide 100% OA (5.2.2.7-5.2.2.9)
  econ_max_100_pct_oa_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  econ_max_100_pct_oa_sch.setName('Economizer Max OA Fraction 100 pct')
  econ_max_100_pct_oa_sch.defaultDaySchedule.setName('Economizer Max OA Fraction 100 pct Default')
  econ_max_100_pct_oa_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1.0)

  # Check each airloop
  model.getAirLoopHVACs.sort.each do |air_loop|
    if air_loop_hvac_economizer_required?(air_loop) == true
      # If an economizer is required, determine the economizer type
      # in the prototype buildings, which depends on climate zone.
      economizer_type = nil

      # NECB 5.2.2.8 states that economizer can be controlled based on difference betweeen
      # return air temperature and outside air temperature OR return air enthalpy
      # and outside air enthalphy; latter chosen to be consistent with MNECB and CAN-QUEST implementation
      economizer_type = 'DifferentialEnthalpy'
      # Set the economizer type
      # Get the OA system and OA controller
      oa_sys = air_loop.airLoopHVACOutdoorAirSystem
      if oa_sys.is_initialized
        oa_sys = oa_sys.get
      else
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.prototype.Model', "#{air_loop.name} is required to have an economizer, but it has no OA system.")
        next
      end
      oa_control = oa_sys.getControllerOutdoorAir
      oa_control.setEconomizerControlType(economizer_type)
    end
  end
end

#apply_envelope(model:, ext_wall_cond: nil, ext_floor_cond: nil, ext_roof_cond: nil, ground_wall_cond: nil, ground_floor_cond: nil, ground_roof_cond: nil, door_construction_cond: nil, fixed_window_cond: nil, glass_door_cond: nil, overhead_door_cond: nil, skylight_cond: nil, glass_door_solar_trans: nil, fixed_wind_solar_trans: nil, skylight_solar_trans: nil, infiltration_scale: nil, necb_hdd: true) ⇒ Object



681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 681

def apply_envelope(model:,
                   ext_wall_cond: nil,
                   ext_floor_cond: nil,
                   ext_roof_cond: nil,
                   ground_wall_cond: nil,
                   ground_floor_cond: nil,
                   ground_roof_cond: nil,
                   door_construction_cond: nil,
                   fixed_window_cond: nil,
                   glass_door_cond: nil,
                   overhead_door_cond: nil,
                   skylight_cond: nil,
                   glass_door_solar_trans: nil,
                   fixed_wind_solar_trans: nil,
                   skylight_solar_trans: nil,
                   infiltration_scale: nil,
                   necb_hdd: true)
  raise('validation of model failed.') unless validate_initial_model(model)

  model_apply_infiltration_standard(model)
  ecm = ECMS.new
  ecm.scale_infiltration_loads(model: model, scale: infiltration_scale)
  model.getInsideSurfaceConvectionAlgorithm.setAlgorithm('TARP')
  model.getOutsideSurfaceConvectionAlgorithm.setAlgorithm('TARP')
  model_add_constructions(model)
  apply_standard_construction_properties(model: model,
                                         ext_wall_cond: ext_wall_cond,
                                         ext_floor_cond: ext_floor_cond,
                                         ext_roof_cond: ext_roof_cond,
                                         ground_wall_cond: ground_wall_cond,
                                         ground_floor_cond: ground_floor_cond,
                                         ground_roof_cond: ground_roof_cond,
                                         door_construction_cond: door_construction_cond,
                                         fixed_window_cond: fixed_window_cond,
                                         glass_door_cond: glass_door_cond,
                                         overhead_door_cond: overhead_door_cond,
                                         skylight_cond: skylight_cond,
                                         glass_door_solar_trans: glass_door_solar_trans,
                                         fixed_wind_solar_trans: fixed_wind_solar_trans,
                                         skylight_solar_trans: skylight_solar_trans,
                                         necb_hdd: necb_hdd)
  model_create_thermal_zones(model, @space_multiplier_map)
end

#apply_fdwr_srr_daylighting(model:, fdwr_set: -1.0,, srr_set: -1.0,, necb_hdd: true) ⇒ Object

Thermal zones need to be set to determine conditioned spaces when applying fdwr and srr limits.

# fdwr_set/srr_set settings:
# 0-1:  Remove all windows/skylights and add windows/skylights to match this fdwr/srr
# -1:  Remove all windows/skylights and add windows/skylights to match max fdwr/srr from NECB
# -2:  Do not apply any fdwr/srr changes, leave windows/skylights alone (also works for fdwr/srr > 1)
# -3:  Use old method which reduces existing window/skylight size (if necessary) to meet maximum NECB fdwr/srr
# limit
# <-3.1:  Remove all the windows/skylights
# > 1:  Do nothing


952
953
954
955
956
957
958
959
960
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 952

def apply_fdwr_srr_daylighting(model:, fdwr_set: -1.0, srr_set: -1.0, necb_hdd: true)
  fdwr_set = -1.0 if (fdwr_set == 'NECB_default') || fdwr_set.nil?
  srr_set = -1.0 if (srr_set == 'NECB_default') || srr_set.nil?
  fdwr_set = fdwr_set.to_f
  srr_set = srr_set.to_f
  apply_standard_window_to_wall_ratio(model: model, fdwr_set: fdwr_set, necb_hdd: necb_hdd)
  apply_standard_skylight_to_roof_ratio(model: model, srr_set: srr_set)
  # model_add_daylighting_controls(model) # to be removed after refactor.
end

#apply_kiva_foundation(model) ⇒ Object

apply the Kiva foundation model to floors and walls with ground boundary condition created by: Kamel Haddad ([email protected])



727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 727

def apply_kiva_foundation(model)
  # define a Kiva model for the whole bldg that's used for the first floor in contact with ground in each zone
  bldg_kiva_model = OpenStudio::Model::FoundationKiva.new(model)
  bldg_kiva_model.setName("Bldg Kiva Foundation")
  bldg_kiva_model.setWallHeightAboveGrade(0.0)
  bldg_kiva_model.setWallDepthBelowSlab(0.0)
  model.getThermalZones.sort.each do |zone|
    zone_kiva_models = [bldg_kiva_model]
    zone_grd_flr_counter = 0
    zone.spaces.sort.each do |space|
      # store space floors and walls in contact with ground and exterior walls
      space_ground_floors = []
      space_ground_walls = []
      space_ext_walls = []
      space_ground_floors += space.surfaces.select {|surf| surf.surfaceType.downcase == 'floor' && surf.isGroundSurface }
      space_ground_walls += space.surfaces.select {|surf| surf.surfaceType.downcase == 'wall' && surf.isGroundSurface }
      space_ext_walls += space.surfaces.select {|surf| surf.surfaceType.downcase == 'wall' && surf.outsideBoundaryCondition.downcase == 'outdoors'}
      # loop through space floors in contact with ground and assing a Kiva model for each
      space_ground_floors.each do |gfloor|
        zone_grd_flr_counter += 1
        if zone_grd_flr_counter > 1
          # a new Kiva model is needed for each additional floor in contact with the ground in the zone
          kiva_model = OpenStudio::Model::FoundationKiva.new(model)
          kiva_model.setName("#{gfloor.name.to_s} Kiva Foundation")
          kiva_model.setWallHeightAboveGrade(0.0)
          kiva_model.setWallDepthBelowSlab(0.0)
          zone_kiva_models << kiva_model
        end
        # Kiva model only works with standard materials. Replace constructions massless materials with standard ones.
        replace_massless_material_with_std_material(model,gfloor)
        gfloor.setOutsideBoundaryCondition('Foundation')
        gfloor.setAdjacentFoundation(zone_kiva_models.last)
        # Set the exposed perimeter for space floors in contact with the ground.
        floor_exp_per = 0.0
        if !space_ground_walls.empty?
          floor_exp_per += get_surface_exp_per(gfloor,space_ground_walls)
        elsif !space_ext_walls.empty?
          floor_exp_per += get_surface_exp_per(gfloor,space_ext_walls)
        end
        gfloor.createSurfacePropertyExposedFoundationPerimeter('TotalExposedPerimeter',floor_exp_per)
        # specify a foundation boundary condition for space walls in contact with the ground and in
        # contact with the space floor in contact with ground 'gfloor'
        space_ground_walls.each do |gwall|
          if surfaces_are_in_contact?(gfloor,gwall)
            replace_massless_material_with_std_material(model,gwall)
            gwall.setOutsideBoundaryCondition('Foundation')
            gwall.setAdjacentFoundation(zone_kiva_models.last)
          end
        end
      end
    end
  end
  kiva_settings = model.getFoundationKivaSettings if !model.getFoundationKivas.empty?
end

#apply_limit_fdwr(model:, fdwr_lim:) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 25

def apply_limit_fdwr(model:, fdwr_lim:)
  # Loop through all spaces in the model, and
  # per the PNNL PRM Reference Manual, find the areas
  # of each space conditioning category (res, nonres, semi-heated)
  # separately.  Include space multipliers.
  nr_wall_m2 = 0.001 # Avoids divide by zero errors later
  nr_wind_m2 = 0
  res_wall_m2 = 0.001
  res_wind_m2 = 0
  sh_wall_m2 = 0.001
  sh_wind_m2 = 0
  total_wall_m2 = 0.001
  total_subsurface_m2 = 0.0
  # Store the space conditioning category for later use
  space_cats = {}
  model.getSpaces.sort.each do |space|
    # Loop through all surfaces in this space
    wall_area_m2 = 0
    wind_area_m2 = 0
    space.surfaces.sort.each do |surface|
      # Skip non-outdoor surfaces
      next unless surface.outsideBoundaryCondition == 'Outdoors'
      # Skip non-walls
      next unless surface.surfaceType.casecmp('wall').zero?

      # This wall's gross area (including window area)
      wall_area_m2 += surface.grossArea * space.multiplier
      # Subsurfaces in this surface
      surface.subSurfaces.sort.each do |ss|
        wind_area_m2 += ss.netArea * space.multiplier
      end
    end

    # Determine the space category
    # @todo This should really use the heating/cooling loads
    # from the proposed building.  However, in an attempt
    # to avoid another sizing run just for this purpose,
    # conditioned status is based on heating/cooling
    # setpoints.  If heated-only, will be assumed Semiheated.
    # The full-bore method is on the next line in case needed.
    # cat = thermal_zone_conditioning_category(space, template, climate_zone)
    cooled = OpenstudioStandards::Space.space_cooled?(space)
    heated = OpenstudioStandards::Space.space_heated?(space)
    cat = 'Unconditioned'
    # Unconditioned
    if !heated && !cooled
      cat = 'Unconditioned'
      # Heated-Only
    elsif heated && !cooled
      cat = 'Semiheated'
      # Heated and Cooled
    else
      res = OpenstudioStandards::ThermalZone.thermal_zone_residential?(space.thermalZone.get)
      cat = if res
              'ResConditioned'
            else
              'NonResConditioned'
            end
    end
    space_cats[space] = cat
    # NECB2011 keep track of totals for NECB regardless of conditioned or not.
    total_wall_m2 += wall_area_m2
    total_subsurface_m2 += wind_area_m2 # this contains doors as well.

    # Add to the correct category
    case cat
    when 'Unconditioned'
      next # Skip unconditioned spaces
    when 'NonResConditioned'
      nr_wall_m2 += wall_area_m2
      nr_wind_m2 += wind_area_m2
    when 'ResConditioned'
      res_wall_m2 += wall_area_m2
      res_wind_m2 += wind_area_m2
    when 'Semiheated'
      sh_wall_m2 += wall_area_m2
      sh_wind_m2 += wind_area_m2
    end
  end

  # Calculate the WWR of each category
  wwr_nr = ((nr_wind_m2 / nr_wall_m2) * 100.0).round(1)
  wwr_res = ((res_wind_m2 / res_wall_m2) * 100).round(1)
  wwr_sh = ((sh_wind_m2 / sh_wall_m2) * 100).round(1)
  fdwr = ((total_subsurface_m2 / total_wall_m2) * 100).round(1) # used by NECB2011

  # Convert to IP and report
  nr_wind_ft2 = OpenStudio.convert(nr_wind_m2, 'm^2', 'ft^2').get
  nr_wall_ft2 = OpenStudio.convert(nr_wall_m2, 'm^2', 'ft^2').get

  res_wind_ft2 = OpenStudio.convert(res_wind_m2, 'm^2', 'ft^2').get
  res_wall_ft2 = OpenStudio.convert(res_wall_m2, 'm^2', 'ft^2').get

  sh_wind_ft2 = OpenStudio.convert(sh_wind_m2, 'm^2', 'ft^2').get
  sh_wall_ft2 = OpenStudio.convert(sh_wall_m2, 'm^2', 'ft^2').get

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR NonRes = #{wwr_nr.round}%; window = #{nr_wind_ft2.round} ft2, wall = #{nr_wall_ft2.round} ft2.")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR Res = #{wwr_res.round}%; window = #{res_wind_ft2.round} ft2, wall = #{res_wall_ft2.round} ft2.")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "WWR Semiheated = #{wwr_sh.round}%; window = #{sh_wind_ft2.round} ft2, wall = #{sh_wall_ft2.round} ft2.")

  # WWR limit
  wwr_lim = 40.0

  # Check against WWR limit
  red_nr = wwr_nr > wwr_lim
  red_res = wwr_res > wwr_lim
  red_sh = wwr_sh > wwr_lim

  # puts "Current FDWR is #{fdwr}, must be less than #{fdwr_lim}."
  # puts "Current subsurf area is #{total_subsurface_m2} and gross surface area is #{total_wall_m2}"
  # Stop here unless windows / doors need reducing
  return true unless fdwr > fdwr_lim

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Reducing the size of all windows (by raising sill height) to reduce window area down to the limit of #{wwr_lim.round}%.")
  # Determine the factors by which to reduce the window / door area
  mult = fdwr_lim / fdwr
  # Reduce the window area if any of the categories necessary
  model.getSpaces.sort.each do |space|
    # Loop through all surfaces in this space
    space.surfaces.sort.each do |surface|
      # Skip non-outdoor surfaces
      next unless surface.outsideBoundaryCondition == 'Outdoors'
      # Skip non-walls
      next unless surface.surfaceType == 'Wall'

      # Subsurfaces in this surface
      surface.subSurfaces.sort.each do |ss|
        # Reduce the size of the window
        red = 1.0 - mult
        OpenstudioStandards::Geometry.sub_surface_reduce_area_by_percent_by_raising_sill(ss, red)
      end
    end
  end
  return true
end

#apply_loads(model:, lights_type: 'NECB_Default', lights_scale: 1.0, validate: true, occupancy_loads_scale: nil, electrical_loads_scale: nil, oa_scale: nil) ⇒ Object



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 637

def apply_loads(model:,
                lights_type: 'NECB_Default',
                lights_scale: 1.0,
                validate: true,
                occupancy_loads_scale: nil,
                electrical_loads_scale: nil,
                oa_scale: nil)
  # Create ECM object.
  ecm = ECMS.new
  lights_scale = convert_arg_to_f(variable: lights_scale, default: 1.0)
  if validate
    raise('validation of model failed.') unless validate_initial_model(model)
    raise('validation of spacetypes failed.') unless validate_and_upate_space_types(model)
  end
  # this sets/stores the template version loads that the model uses.
  model.getBuilding.setStandardsTemplate(self.class.name)
  set_occ_sensor_spacetypes(model, @space_type_map)
  model_add_loads(model, lights_type, lights_scale)
  ecm.scale_occupancy_loads(model: model, scale: occupancy_loads_scale)
  ecm.scale_electrical_loads(model: model, scale: electrical_loads_scale)
  ecm.scale_oa_loads(model: model, scale: oa_scale)
end

#apply_loop_pump_power(model:, sizing_run_dir:) ⇒ Object



1098
1099
1100
1101
1102
1103
1104
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1098

def apply_loop_pump_power(model:, sizing_run_dir:)
  # Remove duplicate materials and constructions
  # Note For NECB2015 This is the 2nd time this method is bieng run.
  # First time it ran in the super() within model_apply_standard() method
  # model = return BTAP::FileIO::remove_duplicate_materials_and_constructions(model)
  return model
end

#apply_max_fdwr_nrcan(model:, fdwr_lim:) ⇒ Object

This method applies the maximum fenestration and door to wall ratio to a building as per NECB 2011 8.4.4.3 and 3.2.1.4 (or equivalent in other versions of the NECB). It first checks for al exterior walls adjacent to conditioned spaces. It distinguishes between plenums and other conditioned spaces. It uses both to calculate the maximum window area to be applied to the building but attempts to put these windows only on non-plenum conditioned spaces (if possible).



651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 651

def apply_max_fdwr_nrcan(model:, fdwr_lim:)
  # First determine which vertical (between 89 and 91 degrees from horizontal) walls are adjacent to conditioned
  # spaces.
  exp_surf_info = find_exposed_conditioned_vertical_surfaces(model)
  # If there are none (or very few) then throw a warning.
  if exp_surf_info['total_exp_wall_area_m2'] < 0.1
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.Model', 'This building has no exposed walls adjacent to heated spaces.')
    return false
  end

  construct_set = model.getBuilding.defaultConstructionSet.get
  fixed_window_construct_set = construct_set.defaultExteriorSubSurfaceConstructions.get.fixedWindowConstruction.get

  # IF FDWR is greater than 1 then something is wrong raise an error.  If it is less than 0.001 assume all the windows
  # should go.
  if fdwr_lim > 1
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', 'This building requires a larger window area than there is wall area.')
    return false
  elsif fdwr_lim < 0.001
    exp_surf_info['exp_nonplenum_walls'].sort.each do |exp_surf|
      exp_surf.subSurfaces.sort.each(&:remove)
    end
    return true
  end
  # Get the required window area.
  win_area = fdwr_lim * exp_surf_info['total_exp_wall_area_m2']
  # Try to put the windows on non-plenum walls if possible.  So determine if you can fit the required window area
  # on the non-plenum wall area.
  if win_area <= exp_surf_info['exp_nonplenum_wall_area_m2']
    # If you can fit the windows on the non-plenum wall area then recalculate the window ratio so that is is only for
    # the non-plenum walls.
    nonplenum_fdwr = win_area / exp_surf_info['exp_nonplenum_wall_area_m2']
    exp_surf_info['exp_nonplenum_walls'].sort.each do |exp_surf|
      # Remove any subsurfaces, add the window, set the name to be whatever the surface name is plus the subsurface
      # type (which will be 'fixedwindow')
      exp_surf.subSurfaces.sort.each(&:remove)
      exp_surf.setWindowToWallRatio(nonplenum_fdwr)
      exp_surf.subSurfaces.sort.each do |sub_surf|
        sub_surf.setSubSurfaceType('FixedWindow')
        sub_surf.setConstruction(fixed_window_construct_set)
        new_name = exp_surf.name.to_s + '_' + sub_surf.subSurfaceType.to_s
        sub_surf.setName(new_name)
      end
    end
  else
    # There was not enough non-plenum wall area so add the windows to both the plenum and non-plenum walls.  This is
    # done separately because the 'find_exposed_conditioned_vertical_surfaces' method returns the plenum and
    # non-plenum walls separately.
    exp_surf_info['exp_nonplenum_walls'].sort.each do |exp_surf|
      # Remove any subsurfaces, add the window, set the name to be whatever the surface name is plus the subsurface
      # type (which will be 'fixedwindow')
      exp_surf.subSurfaces.sort.each(&:remove)
      exp_surf.setWindowToWallRatio(fdwr_lim)
      exp_surf.subSurfaces.sort.each do |sub_surf|
        sub_surf.setSubSurfaceType('FixedWindow')
        sub_surf.setConstruction(fixed_window_construct_set)
        new_name = exp_surf.name.to_s + '_' + sub_surf.subSurfaceType.to_s
        sub_surf.setName(new_name)
      end
    end
    exp_surf_info['exp_plenum_walls'].sort.each do |exp_surf|
      # Remove any subsurfaces, add the window, set the name to be whatever the surface name is plus the subsurface
      # type (which will be 'fixedwindow')
      exp_surf.subSurfaces.sort.each(&:remove)
      exp_surf.setWindowToWallRatio(fdwr_lim)
      exp_surf.subSurfaces.sort.each do |sub_surf|
        sub_surf.setSubSurfaceType('FixedWindow')
        sub_surf.setConstruction(fixed_window_construct_set)
        new_name = exp_surf.name.to_s + '_' + sub_surf.subSurfaceType.to_s
        sub_surf.setName(new_name)
      end
    end
  end
  return true
end

#apply_max_srr_nrcan(model:, srr_lim:) ⇒ Object

This method is similar to the ‘apply_max_fdwr’ method above but applies the maximum skylight to roof area ratio to a building as per NECB 2011 8.4.4.3 and 3.2.1.4 (or equivalent in other versions of the NECB). It first checks for all exterior roofs adjacent to conditioned spaces. It distinguishes between plenums and other conditioned spaces. It uses only the non-plenum roof area to calculate the maximum skylight area to be applied to the building.



731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 731

def apply_max_srr_nrcan(model:, srr_lim:)
  # First determine which roof surfaces are adjacent to heated spaces (both plenum and non-plenum).
  exp_surf_info = find_exposed_conditioned_roof_surfaces(model)
  # If the non-plenum roof area is very small raise a warning.  It may be perfectly fine but it is probably a good
  # idea to warn the user.
  if exp_surf_info['exp_nonplenum_roof_area_m2'] < 0.1
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'This building has no exposed ceilings adjacent to spaces that are not attics or plenums.  No skylights will be added.')
    return false
  end

  # If the SRR is greater than one something is seriously wrong so raise an error.  If it is less than 0.001 assume
  # all the skylights should go.
  if srr_lim > 1
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', 'This building requires a larger skylight area than there is roof area.')
    return false
  elsif srr_lim < 0.001
    exp_surf_info['exp_nonplenum_roofs'].sort.each do |exp_surf|
      exp_surf.subSurfaces.sort.each(&:remove)
    end
    return true
  end

  construct_set = model.getBuilding.defaultConstructionSet.get
  skylight_construct_set = construct_set.defaultExteriorSubSurfaceConstructions.get.skylightConstruction.get

  # Go through all of exposed roofs adjacent to heated, non-plenum spaces, remove any existing subsurfaces, and add
  # a skylight in the centroid of the surface, with the same shape of the surface, only scaled to be the area
  # determined by the SRR.  The name of the skylight will be the surface name with the subsurface type attached
  # ('skylight' in this case).  Note that this method will only work if the surface does not fold into itself (like an
  # L or a V).
  exp_surf_info['exp_nonplenum_roofs'].sort.each do |roof|
    # sub_surface_create_centered_subsurface_from_scaled_surface(roof, srr_lim, model)
    sub_surface_create_scaled_subsurfaces_from_surface(surface: roof, area_fraction: srr_lim, construction: skylight_construct_set)
  end
  return true
end

#apply_standard_construction_properties(model:, runner: nil, ext_wall_cond: nil, ext_floor_cond: nil, ext_roof_cond: nil, ground_wall_cond: nil, ground_floor_cond: nil, ground_roof_cond: nil, fixed_window_cond: nil, fixed_wind_solar_trans: nil, fixed_wind_vis_trans: nil, operable_wind_solar_trans: nil, operable_window_cond: nil, operable_wind_vis_trans: nil, glass_door_cond: nil, glass_door_solar_trans: nil, glass_door_vis_trans: nil, door_construction_cond: nil, overhead_door_cond: nil, skylight_cond: nil, skylight_solar_trans: nil, skylight_vis_trans: nil, tubular_daylight_dome_cond: nil, tubular_daylight_dome_solar_trans: nil, tubular_daylight_dome_vis_trans: nil, tubular_daylight_diffuser_cond: nil, tubular_daylight_diffuser_solar_trans: nil, tubular_daylight_diffuser_vis_trans: nil, necb_hdd: true) ⇒ Boolean

Go through the default construction sets and hard-assigned constructions. Clone the existing constructions and set their intended surface type and standards construction type per the PRM. For some standards, this will involve making modifications. For others, it will not.

90.1-2007, 90.1-2010, 90.1-2013

Returns:

  • (Boolean)

    returns true if successful, false if not



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 300

def apply_standard_construction_properties(model:,
                                           runner: nil,
                                           # ext surfaces
                                           ext_wall_cond: nil,
                                           ext_floor_cond: nil,
                                           ext_roof_cond: nil,
                                           # ground surfaces
                                           ground_wall_cond: nil,
                                           ground_floor_cond: nil,
                                           ground_roof_cond: nil,
                                           # fixed Windows
                                           fixed_window_cond: nil,
                                           fixed_wind_solar_trans: nil,
                                           fixed_wind_vis_trans: nil,
                                           # operable windows
                                           operable_wind_solar_trans: nil,
                                           operable_window_cond: nil,
                                           operable_wind_vis_trans: nil,
                                           # glass doors
                                           glass_door_cond: nil,
                                           glass_door_solar_trans: nil,
                                           glass_door_vis_trans: nil,
                                           # opaque doors
                                           door_construction_cond: nil,
                                           overhead_door_cond: nil,
                                           # skylights
                                           skylight_cond: nil,
                                           skylight_solar_trans: nil,
                                           skylight_vis_trans: nil,
                                           # tubular daylight dome
                                           tubular_daylight_dome_cond: nil,
                                           tubular_daylight_dome_solar_trans: nil,
                                           tubular_daylight_dome_vis_trans: nil,
                                           # tubular daylight diffuser
                                           tubular_daylight_diffuser_cond: nil,
                                           tubular_daylight_diffuser_solar_trans: nil,
                                           tubular_daylight_diffuser_vis_trans: nil,
                                           necb_hdd: true)

  model.getDefaultConstructionSets.sort.each do |default_surface_construction_set|
    BTAP.runner_register('Info', 'apply_standard_construction_properties', runner)
    if model.weatherFile.empty? || model.weatherFile.get.path.empty? || !File.exist?(model.weatherFile.get.path.get.to_s)

      BTAP.runner_register('Error', 'Weather file is not defined. Please ensure the weather file is defined and exists.', runner)
      return false
    end

    # hdd required in scope for eval function.
    hdd = get_necb_hdd18(model: model, necb_hdd: necb_hdd)
    # Lambdas are preferred over methods in methods for small utility methods.
    correct_cond = lambda do |conductivity, surface_type|
      return conductivity.nil? || conductivity.to_f <= 0.0 || conductivity == 'NECB_Default' ? eval(model_find_objects(@standards_data['surface_thermal_transmittance'], surface_type)[0]['formula']) : conductivity.to_f
    end

    # Converts trans and vis to nil if requesting default.. or casts the string to a float.
    correct_vis_trans = lambda do |value|
      return value.nil? || value.to_f <= 0.0 || value == 'NECB_Default' ? nil : value.to_f
    end

    BTAP::Resources::Envelope::ConstructionSets.customize_default_surface_construction_set!(model: model,
                                                                                            name: "#{default_surface_construction_set.name.get} at hdd = #{get_necb_hdd18(model: model, necb_hdd: necb_hdd)}",
                                                                                            default_surface_construction_set: default_surface_construction_set,
                                                                                            # ext surfaces
                                                                                            ext_wall_cond: correct_cond.call(ext_wall_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Wall'),
                                                                                            ext_floor_cond: correct_cond.call(ext_floor_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Floor'),
                                                                                            ext_roof_cond: correct_cond.call(ext_roof_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'RoofCeiling'),
                                                                                            # ground surfaces
                                                                                            ground_wall_cond: correct_cond.call(ground_wall_cond, 'boundary_condition' => 'Ground', 'surface' => 'Wall'),
                                                                                            ground_floor_cond: correct_cond.call(ground_floor_cond, 'boundary_condition' => 'Ground', 'surface' => 'Floor'),
                                                                                            ground_roof_cond: correct_cond.call(ground_roof_cond, 'boundary_condition' => 'Ground', 'surface' => 'RoofCeiling'),
                                                                                            # fixed Windows
                                                                                            fixed_window_cond: correct_cond.call(fixed_window_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Window'),
                                                                                            fixed_wind_solar_trans: correct_vis_trans.call(fixed_wind_solar_trans),
                                                                                            fixed_wind_vis_trans: correct_vis_trans.call(fixed_wind_vis_trans),
                                                                                            # operable windows
                                                                                            operable_wind_solar_trans: correct_vis_trans.call(operable_wind_solar_trans),
                                                                                            operable_window_cond: correct_cond.call(fixed_window_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Window'),
                                                                                            operable_wind_vis_trans: correct_vis_trans.call(operable_wind_vis_trans),
                                                                                            # glass doors
                                                                                            glass_door_cond: correct_cond.call(glass_door_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Window'),
                                                                                            glass_door_solar_trans: correct_vis_trans.call(glass_door_solar_trans),
                                                                                            glass_door_vis_trans: correct_vis_trans.call(glass_door_vis_trans),
                                                                                            # opaque doors
                                                                                            door_construction_cond: correct_cond.call(door_construction_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Door'),
                                                                                            overhead_door_cond: correct_cond.call(overhead_door_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Door'),
                                                                                            # skylights
                                                                                            skylight_cond: correct_cond.call(skylight_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Window'),
                                                                                            skylight_solar_trans: correct_vis_trans.call(skylight_solar_trans),
                                                                                            skylight_vis_trans: correct_vis_trans.call(skylight_vis_trans),
                                                                                            # tubular daylight dome
                                                                                            tubular_daylight_dome_cond: correct_cond.call(skylight_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Window'),
                                                                                            tubular_daylight_dome_solar_trans: correct_vis_trans.call(tubular_daylight_dome_solar_trans),
                                                                                            tubular_daylight_dome_vis_trans: correct_vis_trans.call(tubular_daylight_dome_vis_trans),
                                                                                            # tubular daylight diffuser
                                                                                            tubular_daylight_diffuser_cond: correct_cond.call(skylight_cond, 'boundary_condition' => 'Outdoors', 'surface' => 'Window'),
                                                                                            tubular_daylight_diffuser_solar_trans: correct_vis_trans.call(tubular_daylight_diffuser_solar_trans),
                                                                                            tubular_daylight_diffuser_vis_trans: correct_vis_trans.call(tubular_daylight_diffuser_vis_trans))
  end
  # sets all surfaces to use default constructions sets except adiabatic, where it does a hard assignment of the interior wall construction type.
  model.getPlanarSurfaces.sort.each(&:resetConstruction)
  # if the default construction set is defined..try to assign the interior wall to the adiabatic surfaces
  BTAP::Resources::Envelope.assign_interior_surface_construction_to_adiabatic_surfaces(model, nil)
  BTAP.runner_register('Info', ' apply_standard_construction_properties was sucessful.', runner)
end

#apply_standard_efficiencies(model:, sizing_run_dir:, dcv_type: 'NECB_Default', necb_reference_hp: false) ⇒ Object

Parameters:

  • necb_reference_hp (Boolean) (defaults to: false)

    if true, NECB reference model rules for heat pumps will be used.



1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1029

def apply_standard_efficiencies(model:, sizing_run_dir:, dcv_type: 'NECB_Default', necb_reference_hp: false)
  raise('validation of model failed.') unless validate_initial_model(model)

  climate_zone = 'NECB HDD Method'
  raise("sizing run 1 failed! check #{sizing_run_dir}") if model_run_sizing_run(model, "#{sizing_run_dir}/plant_loops") == false

  # This is needed for NECB2011 as a workaround for sizing the reheat boxes.
  model.getAirTerminalSingleDuctVAVReheats.each { |iobj| air_terminal_single_duct_vav_reheat_set_heating_cap(iobj) }
  # Apply the prototype HVAC assumptions
  model_apply_prototype_hvac_assumptions(model, nil, climate_zone)
  # Apply the HVAC efficiency standard
  sql_db_vars_map = {}
  model_apply_hvac_efficiency_standard(model, climate_zone, sql_db_vars_map: sql_db_vars_map, necb_ref_hp: necb_reference_hp)

  model_enable_demand_controlled_ventilation(model, dcv_type)
  return sql_db_vars_map
end

#apply_standard_lights(set_lights: true, space_type:, space_type_properties:, lights_type:, lights_scale:) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/openstudio-standards/standards/necb/NECB2011/lighting.rb', line 2

def apply_standard_lights(set_lights: true,
                          space_type:,
                          space_type_properties:,
                          lights_type:,
                          lights_scale:)

  ##### Remove leading or trailing whitespace in case users add them in inputs
  if lights_scale.instance_of?(String)
    lights_scale = lights_scale.strip
  end

  if lights_type.nil? || lights_type == 'none'
    lights_type = 'NECB_Default'
  end
  if lights_scale.nil? || lights_scale == 'none' || lights_scale == 'NECB_Default'
    lights_scale = 1.0
  end

  ##### Convert a string to a float
  if lights_scale.instance_of?(String)
    lights_scale = lights_scale.to_f
  end

  lights_have_info = false
  lighting_per_area = space_type_properties['lighting_per_area'].to_f
  lighting_per_person = space_type_properties['lighting_per_person'].to_f
  lights_frac_to_return_air = space_type_properties['lighting_fraction_to_return_air'].to_f
  lights_frac_radiant = space_type_properties['lighting_fraction_radiant'].to_f
  lights_frac_visible = space_type_properties['lighting_fraction_visible'].to_f
  lights_frac_replaceable = space_type_properties['lighting_fraction_replaceable'].to_f
  lights_have_info = true if !lighting_per_area.zero? || !lighting_per_person.zero?

  ##### NOTE: Reference for LED lighting's return air, radiant, and visible fraction values is: page 142, NREL (2014), "Proven Energy-Saving Technologies for Commercial Properties", available at https://www.nrel.gov/docs/fy15osti/63807.pdf
  if lights_type == 'LED'
    led_lights_have_info = false
    led_spacetype_data = @standards_data['tables']['led_lighting_data']['table']
    standards_building_type = space_type.standardsBuildingType.is_initialized ? space_type.standardsBuildingType.get : nil
    standards_space_type = space_type.standardsSpaceType.is_initialized ? space_type.standardsSpaceType.get : nil
    led_space_type_properties = led_spacetype_data.detect { |s| (s['building_type'] == standards_building_type) && (s['space_type'] == standards_space_type) }
    if led_space_type_properties.nil?
      raise("#{standards_building_type} for #{standards_space_type} was not found please verify the led lighting database names match the space type names.")
    end

    lighting_per_area_led_lighting = led_space_type_properties['lighting_per_area'].to_f
    lights_frac_to_return_air_led_lighting = led_space_type_properties['lighting_fraction_to_return_air'].to_f
    lights_frac_radiant_led_lighting = led_space_type_properties['lighting_fraction_radiant'].to_f
    lights_frac_visible_led_lighting = led_space_type_properties['lighting_fraction_visible'].to_f
    led_lights_have_info = true unless lighting_per_area_led_lighting.zero?

  end

  return unless set_lights && lights_have_info

  # Remove all but the first instance
  instances = space_type.lights.sort
  if instances.size.zero?
    definition = OpenStudio::Model::LightsDefinition.new(space_type.model)
    if lights_type == 'NECB_Default'
      definition.setName("#{space_type.name} Lights Definition")
    elsif lights_type == 'LED'
      definition.setName("#{space_type.name} Lights Definition - LED lighting")
    end
    # puts definition.name().to_s
    instance = OpenStudio::Model::Lights.new(definition)
    instance.setName("#{space_type.name} Lights")
    # puts instance.name.to_s
    instance.setSpaceType(space_type)
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no lights, one has been created.")
    instances << instance
  elsif instances.size > 1
    instances.each_with_index do |inst, i|
      next if i.zero?

      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "Removed #{inst.name} from #{space_type.name}.")
      inst.remove
    end
  end

  # Modify the definition of the instance
  space_type.lights.sort.each do |inst|
    definition = inst.lightsDefinition
    unless lighting_per_area.zero?
      if lights_type == 'NECB_Default'
        set_lighting_per_area(space_type: space_type,
                              definition: definition,
                              lighting_per_area: lighting_per_area,
                              lights_scale: lights_scale)
      elsif lights_type == 'LED'
        set_lighting_per_area_led_lighting(space_type: space_type,
                                           definition: definition,
                                           lighting_per_area_led_lighting: lighting_per_area_led_lighting,
                                           lights_scale: lights_scale)
      end
    end
    unless lighting_per_person.zero?
      definition.setWattsperPerson(OpenStudio.convert(lighting_per_person.to_f, 'W/person', 'W/person').get)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set lighting to #{lighting_per_person} W/person.")
    end
    unless lights_frac_to_return_air.zero?
      if lights_type == 'NECB_Default'
        definition.setReturnAirFraction(lights_frac_to_return_air)
      elsif lights_type == 'LED'
        definition.setReturnAirFraction(lights_frac_to_return_air_led_lighting)
      end
    end
    unless lights_frac_radiant.zero?
      if lights_type == 'NECB_Default'
        definition.setFractionRadiant(lights_frac_radiant)
      elsif lights_type == 'LED'
        definition.setFractionRadiant(lights_frac_radiant_led_lighting)
      end
    end
    unless lights_frac_visible.zero?
      if lights_type == 'NECB_Default'
        definition.setFractionVisible(lights_frac_visible)
      elsif lights_type == 'LED'
        definition.setFractionVisible(lights_frac_visible_led_lighting)
      end
    end
    # unless lights_frac_replaceable.zero?
    #  definition.setFractionReplaceable(lights_frac_replaceable)
    # end
  end

  # If additional lights are specified, add those too
  additional_lighting_per_area = space_type_properties['additional_lighting_per_area'].to_f

  # If there are none then exit method
  return if additional_lighting_per_area.zero?

  # Create the lighting definition
  additional_lights_def = OpenStudio::Model::LightsDefinition.new(space_type.model)
  additional_lights_def.setName("#{space_type.name} Additional Lights Definition")
  additional_lights_def.setWattsperSpaceFloorArea(OpenStudio.convert(additional_lighting_per_area.to_f, 'W/ft^2', 'W/m^2').get)
  additional_lights_def.setReturnAirFraction(lights_frac_to_return_air)
  additional_lights_def.setFractionRadiant(lights_frac_radiant)
  additional_lights_def.setFractionVisible(lights_frac_visible)

  # Create the lighting instance and hook it up to the space type
  additional_lights = OpenStudio::Model::Lights.new(additional_lights_def)
  additional_lights.setName("#{space_type.name} Additional Lights")
  additional_lights.setSpaceType(space_type)
end

#apply_standard_skylight_to_roof_ratio(model:, srr_set: -1.0)) ⇒ Object

Reduces the SRR to the values specified by the PRM. SRR reduction will be done by shrinking vertices toward the centroid.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 164

def apply_standard_skylight_to_roof_ratio(model:, srr_set: -1.0)
  # If srr_set is between 1.0 and 1.2 set it to the maximum allowed by the NECB.  If srr_set is between 0.0 and 1.0
  # apply whatever was passed.  If srr_set >= 1.2 then set the existing srr of the building to be the necb maximum
  # only if the the srr exceeds this maximum (otherwise leave it to be whatever was modeled).

  # srr_set settings:
  # 0-1:  Remove all skylights and add skylights to match this srr
  # -1:  Remove all skylights and add skylights to match max srr from NECB
  # -2:  Do not apply any srr changes, leave skylights alone (also works for srr > 1)
  # -3:  Use old method which reduces existing skylight size (if necessary) to meet maximum NECB skylight limit
  # <-3.1:  Remove all skylights
  # > 1:  Do nothing

  return if srr_set.to_f > 1.0
  return apply_max_srr_nrcan(model: model, srr_lim: srr_set.to_f) if srr_set.to_f >= 0.0 && srr_set.to_f <= 1.0
  # Get the maximum NECB srr
  return apply_max_srr_nrcan(model: model, srr_lim: get_standards_constant('skylight_to_roof_ratio_max_value').to_f) if srr_set.to_f >= -1.1 && srr_set.to_f <= -0.9
  return if srr_set.to_f >= -2.1 && srr_set.to_f <= -1.9
  return apply_max_srr_nrcan(model: model, srr_lim: srr_set.to_f) if srr_set.to_f < -3.1
  return unless srr_set.to_f >= -3.1 && srr_set.to_f <= -2.9

  # SRR limit
  srr_lim = get_standards_constant('skylight_to_roof_ratio_max_value') * 100.0

  # Loop through all spaces in the model, and
  # per the PNNL PRM Reference Manual, find the areas
  # of each space conditioning category (res, nonres, semi-heated)
  # separately.  Include space multipliers.
  nr_wall_m2 = 0.001 # Avoids divide by zero errors later
  nr_sky_m2 = 0
  res_wall_m2 = 0.001
  res_sky_m2 = 0
  sh_wall_m2 = 0.001
  sh_sky_m2 = 0
  total_roof_m2 = 0.001
  total_subsurface_m2 = 0
  model.getSpaces.sort.each do |space|
    # Loop through all surfaces in this space
    wall_area_m2 = 0
    sky_area_m2 = 0
    space.surfaces.sort.each do |surface|
      # Skip non-outdoor surfaces
      next unless surface.outsideBoundaryCondition == 'Outdoors'
      # Skip non-walls
      next unless surface.surfaceType == 'RoofCeiling'

      # This wall's gross area (including skylight area)
      wall_area_m2 += surface.grossArea * space.multiplier
      # Subsurfaces in this surface
      surface.subSurfaces.sort.each do |ss|
        sky_area_m2 += ss.netArea * space.multiplier
      end
    end

    # Determine the space category
    cat = 'NonRes'
    if OpenstudioStandards::Space.space_residential?(space)
      cat = 'Res'
    end
    # if space.is_semiheated
    # cat = 'Semiheated'
    # end

    # Add to the correct category
    case cat
    when 'NonRes'
      nr_wall_m2 += wall_area_m2
      nr_sky_m2 += sky_area_m2
    when 'Res'
      res_wall_m2 += wall_area_m2
      res_sky_m2 += sky_area_m2
    when 'Semiheated'
      sh_wall_m2 += wall_area_m2
      sh_sky_m2 += sky_area_m2
    end
    total_roof_m2 += wall_area_m2
    total_subsurface_m2 += sky_area_m2
  end

  # Calculate the SRR of each category
  srr_nr = ((nr_sky_m2 / nr_wall_m2) * 100).round(1)
  srr_res = ((res_sky_m2 / res_wall_m2) * 100).round(1)
  srr_sh = ((sh_sky_m2 / sh_wall_m2) * 100).round(1)
  srr = ((total_subsurface_m2 / total_roof_m2) * 100.0).round(1)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The skylight to roof ratios (SRRs) are: NonRes: #{srr_nr.round}%, Res: #{srr_res.round}%.")

  # Check against SRR limit
  red_nr = srr_nr > srr_lim
  red_res = srr_res > srr_lim
  red_sh = srr_sh > srr_lim

  # Stop here unless windows need reducing
  return true unless srr > srr_lim

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Reducing the size of all windows (by raising sill height) to reduce window area down to the limit of #{srr_lim.round}%.")
  # Determine the factors by which to reduce the window / door area
  mult = srr_lim / srr

  # Reduce the subsurface areas
  model.getSpaces.sort.each do |space|
    # Loop through all surfaces in this space
    space.surfaces.sort.each do |surface|
      # Skip non-outdoor surfaces
      next unless surface.outsideBoundaryCondition == 'Outdoors'
      # Skip non-walls
      next unless surface.surfaceType == 'RoofCeiling'

      # Subsurfaces in this surface
      surface.subSurfaces.sort.each do |ss|
        # Reduce the size of the subsurface
        red = 1.0 - mult
        OpenstudioStandards::Geometry.sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red)
      end
    end
  end

  return true
end

#apply_standard_window_to_wall_ratio(model:, fdwr_set: -1.0,, necb_hdd: true) ⇒ Object

Reduces the WWR to the values specified by the NECB NECB 3.2.1.4



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 4

def apply_standard_window_to_wall_ratio(model:, fdwr_set: -1.0, necb_hdd: true)
  # NECB FDWR limit
  hdd = get_necb_hdd18(model: model, necb_hdd: necb_hdd)

  # Get the maximum NECB fdwr
  # fdwr_set settings:
  # 0-1:  Remove all windows and add windows to match this fdwr
  # -1:  Remove all windows and add windows to match max fdwr from NECB
  # -2:  Do not apply any fdwr changes, leave windows alone (also works for fdwr > 1)
  # -3:  Use old method which reduces existing window size (if necessary) to meet maximum NECB fdwr limit
  # <-3.1:  Remove all windows and doors
  # > 1:  Do nothing

  return if fdwr_set.to_f > 1.0
  return apply_max_fdwr_nrcan(model: model, fdwr_lim: fdwr_set.to_f) if fdwr_set.to_f >= 0.0 && fdwr_set.to_f <= 1.0
  return apply_max_fdwr_nrcan(model: model, fdwr_lim: max_fwdr(hdd).round(3)) if fdwr_set.to_f >= -1.1 && fdwr_set.to_f <= -0.9
  return if fdwr_set.to_f >= -2.1 && fdwr_set.to_f <= -1.9
  return apply_limit_fdwr(model: model, fdwr_lim: (max_fwdr(hdd) * 100.0).round(1)) if fdwr_set.to_f >= -3.1 && fdwr_set.to_f <= -2.9
  return apply_max_fdwr_nrcan(model: model, fdwr_lim: fdwr_set.to_f) if fdwr_set.to_f < -3.1
end

#apply_systems(model:, primary_heating_fuel:, sizing_run_dir:, shw_scale:, baseline_system_zones_map_option:) ⇒ Object

Organizes Zones and assigns them to appropriate systems according to NECB 2011-17 systems spacetype rules in Sec 8. requires requires fuel type to be assigned for each system aspect. Defaults to gas hydronic.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 96

def apply_systems(model:,
                  primary_heating_fuel:,
                  sizing_run_dir:,
                  shw_scale:,
                  baseline_system_zones_map_option:)
  raise('validation of model failed.') unless validate_initial_model(model)

  # Check to see if model is using another vintage of spacetypes. If so overwrite the @standards for the object with the
  # other spacetype data. This is required for correct system mapping.
  template = determine_spacetype_vintage(model)
  unless template == self.class.name
    # Frankenstein the standards data wrt spacetype data.
    @standards_data['space_types'] = Standard.build(template).standards_data['space_types']
  end

  # do a sizing run.
  if model_run_sizing_run(model, "#{sizing_run_dir}/autozone_systems") == false
    raise('autorun sizing run failed!')
  end

  # collect sizing information on each space.
  store_space_sizing_loads(model)


  # remove idealair from zones if any.
  model.getZoneHVACIdealLoadsAirSystems.each(&:remove)
  @hw_loop = create_hw_loop_if_required(self.fuel_type_set.baseboard_type,
                                        self.fuel_type_set.boiler_fueltype,
                                        self.fuel_type_set.mau_heating_coil_type,
                                        model)
  # Rule that all dwelling units have their own zone and system.
  auto_system_dwelling_units(model: model,
                             necb_reference_hp: self.fuel_type_set.necb_reference_hp,
                             necb_reference_hp_supp_fuel: self.fuel_type_set.necb_reference_hp_supp_fuel,
                             baseboard_type: self.fuel_type_set.baseboard_type,
                             boiler_fueltype: self.fuel_type_set.boiler_fueltype,
                             chiller_type: self.fuel_type_set.chiller_type,
                             fan_type: self.fuel_type_set.fan_type,
                             heating_coil_type_sys3: self.fuel_type_set.heating_coil_type_sys3,
                             heating_coil_type_sys4: self.fuel_type_set.heating_coil_type_sys4,
                             hw_loop: @hw_loop,
                             heating_coil_type_sys6: self.fuel_type_set.heating_coil_type_sys6,
                             mau_cooling_type: self.fuel_type_set.mau_cooling_type,
                             mau_heating_coil_type: self.fuel_type_set.mau_heating_coil_type,
                             mau_type: self.fuel_type_set.mau_type,
                             baseline_system_zones_map_option: baseline_system_zones_map_option)

  # Assign a single system 4 for all wet spaces.. and assign the control zone to the one with the largest load.
  auto_system_wet_spaces(baseboard_type: self.fuel_type_set.baseboard_type,
                         necb_reference_hp: self.fuel_type_set.necb_reference_hp,
                         necb_reference_hp_supp_fuel: self.fuel_type_set.necb_reference_hp_supp_fuel,
                         boiler_fueltype: self.fuel_type_set.boiler_fueltype,
                         heating_coil_type_sys4: self.fuel_type_set.heating_coil_type_sys4,
                         model: model)

  # Assign a single system 4 for all storage spaces.. and assign the control zone to the one with the largest load.
  auto_system_storage_spaces(baseboard_type: self.fuel_type_set.baseboard_type,
                             necb_reference_hp: self.fuel_type_set.necb_reference_hp,
                             necb_reference_hp_supp_fuel: self.fuel_type_set.necb_reference_hp_supp_fuel,
                             boiler_fueltype: self.fuel_type_set.boiler_fueltype,
                             heating_coil_type_sys4: self.fuel_type_set.heating_coil_type_sys4,
                             model: model)

  # Assign the wild spaces to a single system 4 system with a control zone with the largest load.
  auto_system_wild_spaces(baseboard_type: self.fuel_type_set.baseboard_type,
                          necb_reference_hp: self.fuel_type_set.necb_reference_hp,
                          necb_reference_hp_supp_fuel: self.fuel_type_set.necb_reference_hp_supp_fuel,
                          heating_coil_type_sys4: self.fuel_type_set.heating_coil_type_sys4,
                          model: model)
  # do the regular assignment for the rest and group where possible.
  auto_system_all_other_spaces(model: model,
                               necb_reference_hp: self.fuel_type_set.necb_reference_hp,
                               necb_reference_hp_supp_fuel: self.fuel_type_set.necb_reference_hp_supp_fuel,
                               baseboard_type: self.fuel_type_set.baseboard_type,
                               boiler_fueltype: self.fuel_type_set.boiler_fueltype,
                               chiller_type: self.fuel_type_set.chiller_type,
                               fan_type: self.fuel_type_set.fan_type,
                               heating_coil_type_sys3: self.fuel_type_set.heating_coil_type_sys3,
                               heating_coil_type_sys4: self.fuel_type_set.heating_coil_type_sys4,
                               hw_loop: @hw_loop,
                               heating_coil_type_sys6: self.fuel_type_set.heating_coil_type_sys6,
                               mau_cooling_type: self.fuel_type_set.mau_cooling_type,
                               mau_heating_coil_type: self.fuel_type_set.mau_heating_coil_type,
                               mau_type: self.fuel_type_set.mau_type
  )
  model_add_swh(model: model,
                swh_fueltype: self.fuel_type_set.swh_fueltype,
                shw_scale: shw_scale)
  model_apply_sizing_parameters(model)
  # set a larger tolerance for unmet hours from default 0.2 to 1.0C
  model.getOutputControlReportingTolerances.setToleranceforTimeHeatingSetpointNotMet(1.0)
  model.getOutputControlReportingTolerances.setToleranceforTimeCoolingSetpointNotMet(1.0)
end

#apply_systems_and_efficiencies(model:, sizing_run_dir:, primary_heating_fuel:, dcv_type: 'NECB_Default', ecm_system_name: 'NECB_Default', ecm_system_zones_map_option: 'NECB_Default', erv_package: 'NECB_Default', boiler_eff: nil, furnace_eff: nil, unitary_cop: nil, shw_eff: nil, daylighting_type: 'NECB_Default', nv_type: nil, nv_opening_fraction: nil, nv_temp_out_min: nil, nv_delta_temp_in_out: nil, pv_ground_type:, pv_ground_total_area_pv_panels_m2:, pv_ground_tilt_angle:, pv_ground_azimuth_angle:, pv_ground_module_description:, chiller_type: 'NECB_Default', shw_scale:, airloop_economizer_type: nil, baseline_system_zones_map_option:) ⇒ Object



518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 518

def apply_systems_and_efficiencies(model:,
                                   sizing_run_dir:,
                                   primary_heating_fuel:,
                                   dcv_type: 'NECB_Default',
                                   ecm_system_name: 'NECB_Default',
                                   ecm_system_zones_map_option: 'NECB_Default',
                                   erv_package: 'NECB_Default',
                                   boiler_eff: nil,
                                   furnace_eff: nil,
                                   unitary_cop: nil,
                                   shw_eff: nil,
                                   daylighting_type: 'NECB_Default',
                                   nv_type: nil,
                                   nv_opening_fraction: nil,
                                   nv_temp_out_min: nil,
                                   nv_delta_temp_in_out: nil,
                                   pv_ground_type:,
                                   pv_ground_total_area_pv_panels_m2:,
                                   pv_ground_tilt_angle:,
                                   pv_ground_azimuth_angle:,
                                   pv_ground_module_description:,
                                   chiller_type: 'NECB_Default',
                                   shw_scale:,
                                   airloop_economizer_type: nil,
                                   baseline_system_zones_map_option:)

  # Create ECM object.
  ecm = ECMS.new

  # -------- Systems Layout-----------

  # Create Default Systems.
  apply_systems(model: model,
                primary_heating_fuel: primary_heating_fuel,
                sizing_run_dir: sizing_run_dir,
                shw_scale: shw_scale,
                baseline_system_zones_map_option: baseline_system_zones_map_option)

  # Apply new ECM system. Overwrite standard as required.
  ecm.apply_system_ecm(model: model,
                       ecm_system_name: ecm_system_name,
                       template_standard: self,
                       primary_heating_fuel: self.fuel_type_set.ecm_fueltype,
                       ecm_system_zones_map_option: ecm_system_zones_map_option)

  # -------- Performace, Efficiencies, Controls and Sensors ------------
  #
  # Set code standard equipment characteristics.
  sql_db_vars_map = apply_standard_efficiencies(model: model,
                                                sizing_run_dir: sizing_run_dir,
                                                necb_reference_hp: self.fuel_type_set.necb_reference_hp)
  # Apply System
  ecm.apply_system_efficiencies_ecm(model: model, ecm_system_name: ecm_system_name, template_standard: self)
  # Apply ECM ERV charecteristics as required. Part 2 of above ECM.
  ecm.apply_erv_ecm_efficiency(model: model, erv_package: erv_package)
  # Apply DCV as required
  model_enable_demand_controlled_ventilation(model, dcv_type)
  # Apply Boiler Efficiency
  ecm.modify_boiler_efficiency(model: model, boiler_eff: boiler_eff)
  # Apply Furnace Efficiency
  ecm.modify_furnace_efficiency(model: model, furnace_eff: furnace_eff)
  # Apply Unitary curves
  ecm.modify_unitary_cop(model: model, unitary_cop: unitary_cop, sizing_done: false, sql_db_vars_map: sql_db_vars_map)
  # Apply SHW Efficiency
  ecm.modify_shw_efficiency(model: model, shw_eff: shw_eff)
  # Apply daylight controls.
  model_add_daylighting_controls(model: model, daylighting_type: daylighting_type)
  # Apply Chiller efficiency
  ecm.modify_chiller_efficiency(model: model, chiller_type: chiller_type)
  # Apply airloop economizer
  ecm.add_airloop_economizer(model: model, airloop_economizer_type: airloop_economizer_type)
  # Perform a second sizing run if needed
  if (!unitary_cop.nil? && unitary_cop != 'NECB_Default') || !model.getPlantLoops.empty?
    if model_run_sizing_run(model, "#{sizing_run_dir}/SR2") == false
      raise('sizing run 2 failed!')
    end
  end
  # apply unitary cop
  ecm.modify_unitary_cop(model: model, unitary_cop: unitary_cop, sizing_done: true, sql_db_vars_map: sql_db_vars_map)
  # set capacities of district heating and cooling equipment for ground-source heat pump ecm
  district_heat = false
  if model.version < OpenStudio::VersionString.new('3.7.0')
    district_heat = !model.getDistrictHeatings.empty?
  else
    district_heat = !model.getDistrictHeatingWaters.empty?
  end
  ecm.set_ghx_loop_district_cap(model) if (district_heat && !model.getDistrictCoolings.empty?)

  # -------Pump sizing required by some vintages----------------
  # Apply Pump power as required.
  apply_loop_pump_power(model: model, sizing_run_dir: sizing_run_dir)

  # -------Natural ventilation----------------
  # Apply natural ventilation using simplified method.
  if nv_type == 'add_nv'
    ecm.apply_nv(model: model,
                 nv_type: nv_type,
                 nv_opening_fraction: nv_opening_fraction,
                 nv_temp_out_min: nv_temp_out_min,
                 nv_delta_temp_in_out: nv_delta_temp_in_out)
  end

  # -------Ground-mounted PV panels----------------
  # Apply ground-mounted PV panels as required.
  if pv_ground_type == 'add_pv_ground'
    ecm.apply_pv_ground(model: model,
                        pv_ground_type: pv_ground_type,
                        pv_ground_total_area_pv_panels_m2: pv_ground_total_area_pv_panels_m2,
                        pv_ground_tilt_angle: pv_ground_tilt_angle,
                        pv_ground_azimuth_angle: pv_ground_azimuth_angle,
                        pv_ground_module_description: pv_ground_module_description)
  end

  # Rename air loop and plant loop nodes to accommodate coming OpenStudio version
  rename_air_loop_nodes(model)
  rename_plant_loop_nodes(model)

end

#apply_thermal_bridging(model: nil, tbd_option: 'none', tbd_interpolate: false, wall_U: nil, floor_U: nil, roof_U: nil) ⇒ Boolean

(Optionally) uprates, then derates, envelope surface constructions due to MAJOR thermal bridges (e.g. roof parapets, corners, fenestration perimeters). See lib/openstudio-standards/btap/bridging.rb, which relies on the Thermal Bridging & Derating (TBD) gem.

Parameters:

  • model (OpenStudio::Model::Model) (defaults to: nil)

    an OpenStudio model

  • tbd_option (String) (defaults to: 'none')

    BTAP/TBD option

  • tbd_interpolate (Boolean) (defaults to: false)

    true if TBD interpolates between costed Uo

  • wall_U (Double) (defaults to: nil)

    wall conductance in W/m2.K (nil by default)

  • floor_U (Double) (defaults to: nil)

    floor conductance in W/m2.K (nil by default)

  • roof_U (Double) (defaults to: nil)

    roof conductance in W/m2.K (nil by default)

Returns:

  • (Boolean)

    true if successful, e.g. no errors, compliant if uprated



987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 987

def apply_thermal_bridging(model: nil,
                           tbd_option: 'none',
                           tbd_interpolate: false,
                           wall_U: nil,
                           floor_U: nil,
                           roof_U: nil)
  return false unless model.is_a?(OpenStudio::Model::Model)
  return false unless tbd_option.respond_to?(:to_s)

  tbd_option = tbd_option.to_s
  # 4x options:
  #  - 'none' (TBD is ignored)
  #  - derate using 'bad' PSI factors (BTAP-costed)
  #  - derate using 'good' PSI factors (BTAP-costed)
  #  - 'uprate' (then derate), i.e. iterative process (BTAP-costed)
  ok = tbd_option == 'bad' || tbd_option == 'good' || tbd_option == 'uprate'
  return true  if tbd_option == 'none'
  return false unless ok

  argh = {} # BTAP/TBD arguments
  ok = tbd_interpolate == true || tbd_interpolate == false
  argh[:interpolate] = tbd_interpolate if ok
  argh[:interpolate] = false       unless ok

  argh[:walls ] = { uo: wall_U  }
  argh[:floors] = { uo: floor_U }
  argh[:roofs ] = { uo: roof_U  }

  if tbd_option == 'uprate'
    argh[:walls  ][:ut] = wall_U
    argh[:floors ][:ut] = floor_U
    argh[:roofs  ][:ut] = roof_U
  elsif tbd_option == 'good'
    argh[:quality] = :good
  end    # default == :bad

  @tbd = BTAP::Bridging.new(model, argh)

  true
end

#apply_weather_data(model:, epw_file:, custom_weather_folder: nil) ⇒ Object



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 660

def apply_weather_data(model:, epw_file:, custom_weather_folder: nil)
  # Create full path to weather file
  weather_files = File.absolute_path(File.join(__FILE__, '..', '..', '..', '..', '..' , '..', "data/weather"))
  weather_file = File.join(weather_files, epw_file)
  # Check if the weather file exists.  If it does continue as normal, otherwise try to dowload it from the
  # canmet-energy/btap_weather repository
  unless File.exist?(weather_file)
    # Check if btap_batch transferred the weather file
    weather_transfer = check_datapoint_weather_folder(epw_file: epw_file, weather_folder: weather_files, custom_weather_folder: custom_weather_folder)
    # If btap_batch didn't transfer the weather file, download it.
    get_weather_file_from_repo(epw_file: epw_file) unless weather_transfer
  end

  # Fix EMS references. Temporary workaround for OS issue #2598
  model_temp_fix_ems_references(model)
  model.getThermostatSetpointDualSetpoints(&:remove)
  model.getYearDescription.setDayofWeekforStartDay('Sunday')
  weather_file_path = OpenstudioStandards::Weather.get_standards_weather_file_path(epw_file)
  OpenstudioStandards::Weather.model_set_building_location(model, weather_file_path: weather_file_path)
end

#are_space_loads_similar?(space_1:, space_2:, surface_percent_difference_tolerance: 0.01, angular_percent_difference_tolerance: 0.001, heating_load_percent_difference_tolerance: 15.0) ⇒ Boolean

This method will determine if the loads on a space are similar. (Exposure, space type, space loads, and schedules, etc)

Returns:

  • (Boolean)


541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 541

def are_space_loads_similar?(space_1:,
                             space_2:,
                             surface_percent_difference_tolerance: 0.01,
                             angular_percent_difference_tolerance: 0.001,
                             heating_load_percent_difference_tolerance: 15.0)
  # Do they have the same space type?
  return false unless space_1.multiplier == space_2.multiplier
  # Ensure that they both have defined spacetypes
  return false if space_1.spaceType.empty?
  return false if space_2.spaceType.empty?
  # ensure that they have the same spacetype.
  return false unless space_1.spaceType.get == space_2.spaceType.get

  # Perform surface comparision. If ranges are within percent_difference_tolerance.. they can be considered the same.
  space_1_floor_area = space_1.floorArea
  space_2_floor_area = space_2.floorArea
  space_1_surface_report = space_surface_report(space_1)
  space_2_surface_report = space_surface_report(space_2)
  # Spaces should have the same number of surface orientations.
  return false unless space_1_surface_report.size == space_2_surface_report.size
  # spaces should have similar loads
  return false unless percentage_difference(stored_space_heating_load(space_1), stored_space_heating_load(space_2)) <= heating_load_percent_difference_tolerance

  # Each surface should match
  space_1_surface_report.each do |space_1_surface|
    surface_match = space_2_surface_report.detect do |space_2_surface|
      space_1_surface[:surface_type] == space_2_surface[:surface_type] &&
        space_1_surface[:boundary_condition] == space_2_surface[:boundary_condition] &&
        percentage_difference(space_1_surface[:tilt], space_2_surface[:tilt]) <= angular_percent_difference_tolerance &&
        percentage_difference(space_1_surface[:azimuth], space_2_surface[:azimuth]) <= angular_percent_difference_tolerance &&
        percentage_difference(space_1_surface[:surface_area_to_floor_ratio],
                              space_2_surface[:surface_area_to_floor_ratio]) <= surface_percent_difference_tolerance &&
        percentage_difference(space_1_surface[:glazed_subsurface_area_to_floor_ratio],
                              space_2_surface[:glazed_subsurface_area_to_floor_ratio]) <= surface_percent_difference_tolerance &&
        percentage_difference(space_1_surface[:opaque_subsurface_area_to_floor_ratio],
                              space_2_surface[:opaque_subsurface_area_to_floor_ratio]) <= surface_percent_difference_tolerance
    end
    return false if surface_match.nil?
  end
  return true
end

#are_zone_loads_similar?(zone_1:, zone_2:) ⇒ Boolean

This method will determine if the loads on a zone are similar. (Exposure, space type, space loads, and schedules, etc)

Returns:

  • (Boolean)


524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 524

def are_zone_loads_similar?(zone_1:, zone_2:)
  # make sure they have the same number of spaces.
  truthes = []
  return false if zone_1.spaces.size != zone_2.spaces.size

  zone_1.spaces.each do |space_1|
    zone_2.spaces.each do |space_2|
      if are_space_loads_similar?(space_1: space_1, space_2: space_2)
        truthes << true
      end
    end
  end
  # truthes sizes should be the same as the # of spaces if all spaces are similar.
  return truthes.size == zone_1.spaces.size
end

#assign_base_sys_name(airloop, sys_abbr:, sys_oa:, sys_name_pars:) ⇒ Object

Method to set the base system name based on the following syntax: |sys_abbr|sys_oa|sc>?|sh>?|ssf>?|zh>?|zc>?|srf>?| “sys_abbr” designates the NECB system type (“sys_1, sys_2, … sys_6”) “sys_oa”: “mixed” or “doas” “sys_name_pars” is a hash for the remaining system name parts for heat recovery, heating, cooling, supply fan, zone heating, zone cooling, and return fan



2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2186

def assign_base_sys_name(airloop, sys_abbr:, sys_oa:, sys_name_pars:)
  sys_name = "#{sys_abbr}|#{sys_oa}|"
  sys_name_pars.each do |key, value|
    case key.downcase
    when 'sys_hr'
      case value.downcase
      when 'none'
        sys_name += 'shr>none'
      end

    when 'sys_htg'
      case value.downcase
      when 'none'
        sys_name += 'sh>none'
      when 'electric'
        sys_name += 'sh>c-e'
      when 'hot water'
        sys_name += 'sh>c-hw'
      when 'gas'
        sys_name += 'sh>c-g'
      when 'dx'
        sys_name += 'sh>ashp'
      when 'ccashp'
        sys_name += 'sh>ccashp'
      when 'ashp'
        sys_name += 'sh>ashp'
      end

    when 'sys_clg'
      case value.downcase
      when 'none'
        sys_name += 'sc>none'
      when 'chilled water'
        sys_name += 'sc>c-chw'
      when 'dx'
        if sys_name_pars['sys_htg'] == 'dx'
          sys_name += 'sc>ashp'
        else
          sys_name += 'sc>dx'
        end
      when 'ccashp'
        sys_name += 'sc>ccashp'
      when 'ashp'
        sys_name += 'sc>ashp'
      end

    when 'sys_sf'
      case value.downcase
      when 'none'
        sys_name += 'ssf>none'
      when 'cv'
        sys_name += 'ssf>cv'
      when 'vv'
        sys_name += 'ssf>vv'
      end

    when 'zone_htg'
      case value.downcase
      when 'none'
        sys_name += 'zh>none'
      when 'electric'
        sys_name += 'zh>b-e'
      when 'hot water'
        sys_name += 'zh>b-hw'
      when 'tpfc'
        sys_name += 'zh>fpfc'
      when 'fpfc'
        sys_name += 'zh>tpfc'
      when 'pthp'
        sys_name += 'zh>pthp'
      end

    when 'zone_clg'
      case value.downcase
      when 'none'
        sys_name += 'zc>none'
      when 'tpfc'
        sys_name += 'zc>tpfc'
      when 'fpfc'
        sys_name += 'zc>fpfc'
      when 'ptac'
        sys_name += 'zc>ptac'
      when 'pthp'
        sys_name += 'zc>pthp'
      end

    when 'sys_rf'
      case value.downcase
      when 'none'
        sys_name += 'srf>none'
      when 'cv'
        sys_name += 'srf>cv'
      when 'vv'
        sys_name += 'srf>vv'
      end
    end
    sys_name += '|'
  end

  airloop.setName(sys_name)
end

#assign_contruction_to_adiabatic_surfaces(model) ⇒ Object



539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 539

def assign_contruction_to_adiabatic_surfaces(model)
  cp02_carpet_pad = OpenStudio::Model::MasslessOpaqueMaterial.new(model)
  cp02_carpet_pad.setName('CP02 CARPET PAD')
  cp02_carpet_pad.setRoughness('VeryRough')
  cp02_carpet_pad.setThermalResistance(0.21648)
  cp02_carpet_pad.setThermalAbsorptance(0.9)
  cp02_carpet_pad.setSolarAbsorptance(0.7)
  cp02_carpet_pad.setVisibleAbsorptance(0.8)

  normalweight_concrete_floor = OpenStudio::Model::StandardOpaqueMaterial.new(model)
  normalweight_concrete_floor.setName('100mm Normalweight concrete floor')
  normalweight_concrete_floor.setRoughness('MediumSmooth')
  normalweight_concrete_floor.setThickness(0.1016)
  normalweight_concrete_floor.setConductivity(2.31)
  normalweight_concrete_floor.setDensity(2322)
  normalweight_concrete_floor.setSpecificHeat(832)

  nonres_floor_insulation = OpenStudio::Model::MasslessOpaqueMaterial.new(model)
  nonres_floor_insulation.setName('Nonres_Floor_Insulation')
  nonres_floor_insulation.setRoughness('MediumSmooth')
  nonres_floor_insulation.setThermalResistance(2.88291975297193)
  nonres_floor_insulation.setThermalAbsorptance(0.9)
  nonres_floor_insulation.setSolarAbsorptance(0.7)
  nonres_floor_insulation.setVisibleAbsorptance(0.7)

  floor_adiabatic_construction = OpenStudio::Model::Construction.new(model)
  floor_adiabatic_construction.setName('Floor Adiabatic construction')
  floor_layers = OpenStudio::Model::MaterialVector.new
  floor_layers << cp02_carpet_pad
  floor_layers << normalweight_concrete_floor
  floor_layers << nonres_floor_insulation
  floor_adiabatic_construction.setLayers(floor_layers)

  g01_13mm_gypsum_board = OpenStudio::Model::StandardOpaqueMaterial.new(model)
  g01_13mm_gypsum_board.setName('G01 13mm gypsum board')
  g01_13mm_gypsum_board.setRoughness('Smooth')
  g01_13mm_gypsum_board.setThickness(0.0127)
  g01_13mm_gypsum_board.setConductivity(0.1600)
  g01_13mm_gypsum_board.setDensity(800)
  g01_13mm_gypsum_board.setSpecificHeat(1090)
  g01_13mm_gypsum_board.setThermalAbsorptance(0.9)
  g01_13mm_gypsum_board.setSolarAbsorptance(0.7)
  g01_13mm_gypsum_board.setVisibleAbsorptance(0.5)

  wall_adiabatic_construction = OpenStudio::Model::Construction.new(model)
  wall_adiabatic_construction.setName('Wall Adiabatic construction')
  wall_layers = OpenStudio::Model::MaterialVector.new
  wall_layers << g01_13mm_gypsum_board
  wall_layers << g01_13mm_gypsum_board
  wall_adiabatic_construction.setLayers(wall_layers)

  m10_200mm_concrete_block_basement_wall = OpenStudio::Model::StandardOpaqueMaterial.new(model)
  m10_200mm_concrete_block_basement_wall.setName('M10 200mm concrete block basement wall')
  m10_200mm_concrete_block_basement_wall.setRoughness('MediumRough')
  m10_200mm_concrete_block_basement_wall.setThickness(0.2032)
  m10_200mm_concrete_block_basement_wall.setConductivity(1.326)
  m10_200mm_concrete_block_basement_wall.setDensity(1842)
  m10_200mm_concrete_block_basement_wall.setSpecificHeat(912)

  basement_wall_construction = OpenStudio::Model::Construction.new(model)
  basement_wall_construction.setName('Basement Wall construction')
  basement_wall_layers = OpenStudio::Model::MaterialVector.new
  basement_wall_layers << m10_200mm_concrete_block_basement_wall
  basement_wall_construction.setLayers(basement_wall_layers)

  basement_floor_construction = OpenStudio::Model::Construction.new(model)
  basement_floor_construction.setName('Basement Floor construction')
  basement_floor_layers = OpenStudio::Model::MaterialVector.new
  basement_floor_layers << m10_200mm_concrete_block_basement_wall
  basement_floor_layers << cp02_carpet_pad
  basement_floor_construction.setLayers(basement_floor_layers)

  model.getSurfaces.sort.each do |surface|
    if surface.outsideBoundaryCondition.to_s == 'Adiabatic'
      if surface.surfaceType.to_s == 'Wall'
        surface.setConstruction(wall_adiabatic_construction)
      else
        surface.setConstruction(floor_adiabatic_construction)
      end
    elsif surface.outsideBoundaryCondition.to_s == 'OtherSideCoefficients'
      # Ground
      if surface.surfaceType.to_s == 'Wall'
        surface.setOutsideBoundaryCondition('Ground')
        surface.setConstruction(basement_wall_construction)
      else
        surface.setOutsideBoundaryCondition('Ground')
        surface.setConstruction(basement_floor_construction)
      end
    end
  end
end

#auto_size_shw_capacity(model:, u: 0.45, height_to_radius: 2, shw_scale: 'NECB_Default') ⇒ Object

This calculates the volume and capacity of one mixed tank that is assumed to service all shw in the building u is the tank insulation in W/(m^2*K), height_to_radius is the ratio of tank radius to tank height and is dimensionless



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 230

def auto_size_shw_capacity(model:, u: 0.45, height_to_radius: 2, shw_scale: 'NECB_Default')
  peak_flow_rate = 0
  shw_space_types = []
  space_peak_flows = []
  water_use = 0
  weekly_peak_flow = {
    'Default|Wkdy' => Array.new(24, 0),
    'Sat' => Array.new(24, 0),
    'Sun|Hol' => Array.new(24, 0)
  }
  peak_day_sched = nil
  peak_hour_sched = 0
  peak_flow_sched = 0
  next_hour_day = nil
  next_hour_hour = 0
  next_hour_flow = 0
  total_peak_flow_rate = 0
  shw_spaces = []
  shw_sched_names = []

  ##### Modify shw_scale if required
  if shw_scale.instance_of?(String)
    shw_scale = shw_scale.strip # remove leading or trailing whitespace in case users add them in shw_scale
  end
  if shw_scale == 'NECB_Default' or shw_scale.nil? or shw_scale == 'none' or shw_scale == false
    shw_scale = 1.0
  elsif shw_scale.instance_of?(String) # Convert a string to a float
    shw_scale = shw_scale.to_f
  end

  # First go through all the spaces in the building and determine and determine their shw requirements
  space_types_table = @standards_data['space_types']
  model.getSpaces.sort.each do |space|
    space_peak_flow = 0
    data = nil
    space_type_name = space.spaceType.get.standardsSpaceType.get.to_s
    tank_temperature = 60
    # find the specific space_type properties from standard.json
    space_types_table.each do |space_type|
      if (space_type['building_type'] + ' ' + space_type_name) == (space_type['building_type'] + ' ' + space_type['space_type'])
        break if space_type['necb_hvac_system_selection_type'] == '- undefined -'
        # If there is no service hot water load.. Don't bother adding anything.
        break if (space_type['service_water_heating_peak_flow_per_area'].to_f == 0.0 && space_type['service_water_heating_peak_flow_rate'].to_f == 0.0) || space_type['service_water_heating_schedule'].nil?

        # If there is a service hot water load collect the space information
        data = space_type
        break
      end
    end
    # If there is no service hot water load.. Don't bother adding anything.
    # Skip space types with no data
    next if data.nil?
    space_area = OpenStudio.convert(space.floorArea, 'm^2', 'ft^2').get # ft2
    # Calculate the peak shw flow rate for the space.  Peak flow from JSON file is in US Gal/hr/ft^2
    # space_peak_flow_ind is the peak flow rate for the space while space_peak_flow is the peak flow
    # rate for the space multiplied by the space (ultimately thermal zone) multiplier.  space_peak_flow is used for
    # much of the rest as it reflects how much water is used.  space_peak_flow_ind is recorded and used later on
    # when defining water use equipment.  When when water use equipment is assigned to spaces then the water use
    # by the equipment is multiplied by the space multiplier.  Note that there is a separate water use equipment
    # multiplier as well which is different than the space (ultimately thermal zone) multiplier.
    space_peak_flow_ind = data['service_water_heating_peak_flow_per_area'].to_f * space_area * shw_scale
    space_peak_flow = space_peak_flow_ind * space.multiplier
    #      space_peak_flows << space_peak_flow
    # Add the peak shw flow rate for the space to the total for the entire building
    total_peak_flow_rate += space_peak_flow
    # Get the tank temperature for the space.  This should always be 60 C but I added this part in case something changes in the future.
    if data['service_water_heating_target_temperature'].nil? || (data['service_water_heating_target_temperature'] <= 16)
      tank_temperature = 60
    else
      tank_temperature = data['service_water_heating_target_temperature']
    end
    # Get the shw schedule
    #      shw_sched_names << data['service_water_heating_schedule']
    # 'shw_peakflow_ind_SI' is the shw peak flow rate of the individual space (without the space multiplier)
    space_info = {
      'shw_spaces' => space,
      'shw_peakflow_SI' => OpenStudio.convert(space_peak_flow, 'gal/hr', 'm^3/s').get,
      'shw_peakflow_ind_SI' => OpenStudio.convert(space_peak_flow_ind, 'gal/hr', 'm^3/s').get,
      'shw_temp_SI' => tank_temperature,
      'shw_sched' => data['service_water_heating_schedule']
    }
    shw_spaces << space_info

    # The following gets the water use schedule for space and applies it to the peak flow rate for the space.  This
    # creates an array containing the hourly shw consumption for the space for each day type (Weekday/default, Saturday,
    # Sunday/Holiday).  The hourly shw consumption for each space is added to the array ultimately producing an array
    # containing the hourly shw demand for the entire building.  This is used to determine the peak shw demand
    # hour and rate for the building.  This is different than the overall peak shw demand for the building in that it
    # takes into account the shw schedule.  The peak shw demand hour and rate should always be less than the overall peak
    # shw demand.

    # Cycle through the hash accumulating the shw rates for each day type.
    weekly_peak_flow.sort.each do |day_peak_sched|
      day_sched = []
      # Create the search criteria and retrieve the schedule for the current space and current day type.
      search_criteria = {
        'template' => template,
        'name' => data['service_water_heating_schedule'],
        'day_types' => day_peak_sched[0]
      }
      schedules_table = @standards_data['schedules']

      day_sched = model_find_object(schedules_table, search_criteria)
      # Make sure the schedule is not empty and contains 24 hours.
      if day_sched.empty? || day_sched['values'].size != 24
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.model_add_swh', "The water use schedule called #{data['service_water_heating_schedule']} for #{space_type_name} is corrupted or could not be found.  Please check that the schedules.json file is available and that the schedule names are spelled correctly")
        return false
      end
      # For each hour of the current day type multiply the shw schedule fractional multiplier (representing the fraction of the total shw
      # rate used in that hour) times the overall peak shw rate for the current space.  Add the resulting values to the
      # array tracking hourly shw demand for the building.  Also, determine what the highest hourly demand is for the
      # building.
      day_peak_sched[1].sort.each_with_index do |hour_flow, hour_index|
        weekly_peak_flow[day_peak_sched[0]][hour_index] += day_sched['values'][hour_index] * space_peak_flow
        if weekly_peak_flow[day_peak_sched[0]][hour_index] > peak_flow_sched
          peak_flow_sched = weekly_peak_flow[day_peak_sched[0]][hour_index]
        end
      end
    end
  end
  if shw_spaces.empty?
    space_info = {
      'shw_spaces' => nil,
      'shw_peakflow_SI' => 0,
      'shw_peakflow_ind_SI' => 0,
      'shw_temp_SI' => 60,
      'shw_sched' => []
    }
    shw_spaces << space_info
    tank_param = {
      'tank_volume_SI' => 0,
      'tank_capacity_SI' => 0,
      'max_temp_SI' => 60,
      'loop_peak_flow_rate_SI' => 0,
      'parasitic_loss' => 0,
      'spaces_w_dhw' => shw_spaces
    }
    return tank_param
  end
  next_day_test = nil
  next_hour_test = 0
  # The following loop goes through each hour in the array tracking hourly shw demand to find which hours contain the
  # peak hourly shw demand (this is in case the peak hourly shw demand occurs more than once).  It then determines what
  # the hourly shw demand is for the following hour.  It is meant to determine, of the peak hourly shw times, which has
  # the highest shw demand the following hour.  This is used to determine shw capacity and volume.
  weekly_peak_flow.sort.each do |day_peak_sched|
    day_peak_sched[1].sort.each_with_index do |hour_flow, hour_index|
      if hour_flow == peak_flow_sched
        if hour_index == 23
          next_hour_test = 0
          case day_peak_sched[0]
          when 'Default|Wkdy'
            next_day_test = 'Sat'
          when 'Sat'
            next_day_test = 'Sun|Hol'
          when 'Sun|Hol'
            next_day_test = 'Default|Wkdy'
          end
        else
          next_hour_test = hour_index + 1
          next_day_test = day_peak_sched[0]
        end
        if next_hour_flow < weekly_peak_flow[next_day_test][next_hour_test]
          next_hour_flow = weekly_peak_flow[next_day_test][next_hour_test]
          next_hour_hour = next_hour_test
          next_hour_day = next_day_test
          peak_day_sched = day_peak_sched[0]
          peak_hour_sched = hour_index
        end
      end
    end
  end
  # The shw tank is sized so that it can fulfill the hour with the highest shw needs.  Since the flow is in US Gal/hr
  # No conversion is necessary.
  tank_volume = peak_flow_sched
  # Interperite the fractional shw schedules as being the fraction of the hour that the maximum shw rate is used and determine
  # what this fraction is for the entire building.
  peak_time_fraction = 1 - (peak_flow_sched / total_peak_flow_rate)
  # Assume the shw tank needs some minimum amount of time to recover (avoids requiring a ridiculously high capacity).
  # If the recovery time is to short then the tank needs to hold enough water to service the peak shw hour and the one
  # after.  Then give the tank the entire hour to heat up again.  Note again that since peak flows are per hour, and
  # we are only looking at an hour, no conversion is necessary.
  if peak_time_fraction <= 0.2
    tank_volume += next_hour_flow
    peak_time_fraction = 1
  end
  tank_volume_SI = OpenStudio.convert(tank_volume, 'gal', 'm^3').get
  # Determine the tank capacity as the heat output required to heat up the entire volume of the tank in time remaining
  # in the hour after the peak shw draw is stopped (assume water is provided to the building at 15C ).
  max_temp = -273
  shw_spaces.each do |shw_space|
    if shw_space['shw_temp_SI'] > max_temp
      max_temp = shw_space['shw_temp_SI']
    end
  end
  tank_capacity_SI = tank_volume_SI * 1000 * 4180 * (max_temp - 15) / (3600 * peak_time_fraction)
  tank_radius = (tank_volume_SI / (height_to_radius * Math::PI))**(1.0 / 3)
  tank_area = 2 * (1 + height_to_radius) * Math::PI * (tank_radius**2)
  room_temp = OpenStudio.convert(70, 'F', 'C').get
  parasitic_loss = u * tank_area * (max_temp - room_temp)
  tank_param = {
    'tank_volume_SI' => tank_volume_SI,
    'tank_capacity_SI' => tank_capacity_SI,
    'max_temp_SI' => max_temp,
    'loop_peak_flow_rate_SI' => OpenStudio.convert(total_peak_flow_rate, 'gal/hr', 'm^3/s').get,
    'parasitic_loss' => parasitic_loss,
    'spaces_w_dhw' => shw_spaces
  }
  return tank_param
end

#auto_size_shw_pump_head(model, default: true, pipe_vel: 1.75, kin_visc_SI: 0.000004736, density_SI: 983, pipe_rough_m: 0.0000015) ⇒ Object

Autosize the pump head by calculating the piping longest piping length and deriving required head from that. If default is set to true then it returns a default pump head of 179532 Pa which is based on the OpenStudio 2.4.1 defaults for a constant speed pump. The method first assumes that the tank and pump are located in the space closest to the center of the bottom of the building. It then assumes that water is delivered to the bottom center of every space that has a demand for shw. It calculates the x, y, and z components of the vector between the shw space and the spaces with demand for shw. The distance of the piping run is calculated by adding the x, y, and z components of the vector (rather than the magnitude of the vector). For the purposes of calculating pressure loss along the pipe bends, and other minor losses are accounted by doubling the calculated length of the pipe. The default kinematic viscosity of water is assumed to be that at 60 C (in m^2/s). The default density of water is assumed to be 983 kg/m^3 as per hypertextbook.com/facts/2007/AllenMa.shtml accessed 2018-07-27. The pipe is assumed to be made out of PVC and have a roughness height of 1.5*10^-6 m as per: www.pipeflow.com/pipe-pressure-drop-calculations/pipe-roughness accessed on 2018-07-25. The default maximum velocity is from the table from ‘The Engineering Toolbox’ link: www.engineeringtoolbox.com/flow-velocity-water-pipes-d_385.html Chris Kirney 2018-07-27.



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 456

def auto_size_shw_pump_head(model, default: true, pipe_vel: 1.75, kin_visc_SI: 0.000004736, density_SI: 983, pipe_rough_m: 0.0000015)
  return 179532 if default

  mech_room, cond_spaces = find_mech_room(model)
  return 179532 if mech_room.nil? || cond_spaces.nil?

  space_coord_dists = []
  total_peak_flow = 0
  hl_Pas = []
  # Now go through each space with a shw load and determine the x, y, and z components of a vector from the centroid
  # of the floor of the space containing the shw_tank and the centroid of the floor of the given space
  cond_spaces.each do |cond_space|
    # Find the specific space_type properties from standard.json
    spaceType_name = cond_space['space'].spaceType.get.nameString
    sp_type = spaceType_name[15..-1]
    # Including regular expressions in the following match for cases where extra characters, which do not belong, are
    # added to either the space type in the model or the space type reference file.
    sp_type_info = @standards_data['tables']['space_types']['table'].detect do |data|
      (Regexp.new(data['space_type'].to_s.upcase).match(sp_type.upcase) || Regexp.new(sp_type.upcase).match(data['space_type'].to_s.upcase) || (data['space_type'].to_s.upcase == sp_type.upcase)) &&
        (data['building_type'].to_s == 'Space Function')
    end

    # If the space type could not be found let the use know and go on to the next space.
    if sp_type_info.nil?
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.model_add_swh', "The space type called #{sp_type} could not be found.  Please check that the schedules.json file is available and that the space types are spelled correctly")
      next
    end
    next if sp_type_info['service_water_heating_peak_flow_per_area'].to_f == 0.0 && sp_type_info['service_water_heating_peak_flow_rate'].to_f == 0.0 || sp_type_info['service_water_heating_schedule'].nil?

    space_area = OpenStudio.convert(cond_space['space'].floorArea, 'm^2', 'ft^2').get # ft2
    # Calculate the peak shw flow rate for the space
    space_peak_flow = (sp_type_info['service_water_heating_peak_flow_per_area'].to_f * space_area) * cond_space['space'].multiplier
    space_peak_flow_SI = OpenStudio.convert(space_peak_flow, 'gal/hr', 'm^3/s').get
    # Determine the total shw peak flow for the building.
    total_peak_flow += space_peak_flow_SI
    # I use centroid for the floor as the location of the source or point of use for the shw system.
    if space_peak_flow_SI > 0
      space_coord_dist = []
      cond_space['space_centroid'].each_with_index do |dist, coord|
        space_coord_dist << (dist - mech_room['space_centroid'][coord]).abs
      end
      space_coord_dists << space_coord_dist
    end
  end
  # The piping run length from the shw tank to a given space is assumed to be the sum of the coordinates of the vector
  # described above.  The longest piping run becomes the one used for sizing.  Note that I double the length of this
  # piping run below when calculating head loss.
  space_coord_dists.sort.each do |space_coord_dist|
    sizing_pipe_length = space_coord_dist[0] + space_coord_dist[1] + space_coord_dist[2]
    # The shw pump is sized by assuming that the sum of the peak shw volume flow rates for each space has to be fed
    # through the longest piping run.  So for the sizing calculations below, the flow rate is the sum of the peak volume
    # flow rates for the entire building.  The length of the piping run is twice the calculated longest piping run
    # described above.
    # Step 1:  Calculate the Reynold's number.  Note kinematic viscosity is set for water at 60 C and pipe diameter is
    #          set to 3/4".  These can be changed by passing different values to the method.  I got the kinematic
    #          viscosity from www.engineeringtoolbox.com/water-dynamic-kinematic-viscosity-d_596.html accessed 2018-07-05.
    #          I got the pipe roughness from www.pipeflow.com/pipe-pressure-drop-calculations/pipe-roughness accessed on
    #          2018-07-25.  I assume 3/4" pipe because that is what Mike Lubun says is used in most cases (unless it
    #          it is for process water but we assume that is not the case).
    # Determine the bulk velocity of the shw through the pipe.
    # find pipe diameter for the peak flow
    pipe_dia_m = (4.0 * total_peak_flow / (Math::PI * pipe_vel))**0.5
    # Get the Reynolds number.
    re_pipe = (pipe_vel * pipe_dia_m) / kin_visc_SI
    # Step 2:  Figure out what the Darcy-Weisbach friction factor is.
    relative_rough = pipe_rough_m / pipe_dia_m
    f = friction_factor(re_pipe, relative_rough)
    # Step 3:  Calculate the major head loss
    #          Note that you may be thinking that I forgot to divide the last term by 2 in the equation below.  I didn't.
    #          I multiplied the piping length by 2 because I did not take pipe bends etc. into account and I calculate the
    #          maximum piping run in a really approximate way.  Thus I multiply the piping run by 2.  If you can think
    #          of something better please replace what I have.
    # hl is taken from https://neutrium.net/fluid_flow/pressure-loss-in-pipe accessed 2018-07-26 (I added the height
    # component).  Note that while I allow all of the other physical values to be set I assume that you are building on
    # earth hence g is hard coded to 9.81 m/s^2.
    hl_Pa = (f * (sizing_pipe_length / pipe_dia_m) * (pipe_vel**2) * density_SI) + density_SI * space_coord_dist[2] * 9.81
    if hl_Pa < 1
      hl_Pa = 1
    end
    hl_Pas << hl_Pa
  end
  # If no spaces with shw were found return the default pump head.
  return 179532 if hl_Pas.empty?

  return hl_Pas.max_by { |hl| hl }
end

#auto_system_all_other_spaces(baseboard_type:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', boiler_fueltype:, chiller_type:, fan_type:, heating_coil_type_sys3:, heating_coil_type_sys4:, heating_coil_type_sys6:, hw_loop:, mau_cooling_type:, mau_heating_coil_type:, mau_type:, model:) ⇒ Object

This method will deal with all non wet, non-wild, and non-dwelling units thermal zones.



982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 982

def auto_system_all_other_spaces(baseboard_type:,
                                 necb_reference_hp:false,
                                 necb_reference_hp_supp_fuel:'DefaultFuel',
                                 boiler_fueltype:,
                                 chiller_type:,
                                 fan_type:,
                                 heating_coil_type_sys3:,
                                 heating_coil_type_sys4:,
                                 heating_coil_type_sys6:,
                                 hw_loop:,
                                 mau_cooling_type:,
                                 mau_heating_coil_type:,
                                 mau_type:,
                                 model:)

  zones = []
  other_spaces = model.getSpaces.select do |space|
    !is_a_necb_dwelling_unit?(space) &&
      !is_an_necb_wildcard_space?(space) &&
      !is_an_necb_storage_space?(space)
  end
  other_spaces.each do |space|
    zones << space.thermalZone.get
  end
  zones.uniq!

  # since dwelling units are all zoned 1:1 to space:zone we simply add the zone to the appropriate btap system.
  create_necb_system(baseboard_type: baseboard_type,
                     boiler_fueltype: boiler_fueltype,
                     chiller_type: chiller_type,
                     fan_type: fan_type,
                     heating_coil_type_sys3: heating_coil_type_sys3,
                     heating_coil_type_sys4: heating_coil_type_sys4,
                     heating_coil_type_sys6: heating_coil_type_sys6,
                     hw_loop: @hw_loop,
                     mau_cooling_type: mau_cooling_type,
                     mau_heating_coil_type: mau_heating_coil_type,
                     mau_type: mau_type,
                     model: model,
                     zones: zones,
                     necb_reference_hp: necb_reference_hp,
                     necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel)
end

#auto_system_dwelling_units(baseboard_type:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', boiler_fueltype:, chiller_type:, fan_type:, heating_coil_type_sys3:, heating_coil_type_sys4:, heating_coil_type_sys6:, hw_loop:, mau_cooling_type:, mau_heating_coil_type:, mau_type:, model:, baseline_system_zones_map_option:) ⇒ Object

This method will ensure that all dwelling units are assigned to a system 1 or 3. There is an option to have a shared AHU or not.



1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1029

def auto_system_dwelling_units(baseboard_type:,
                               necb_reference_hp:false,
                               necb_reference_hp_supp_fuel:'DefaultFuel',
                               boiler_fueltype:,
                               chiller_type:,
                               fan_type:,
                               heating_coil_type_sys3:,
                               heating_coil_type_sys4:,
                               heating_coil_type_sys6:,
                               hw_loop:,
                               mau_cooling_type:,
                               mau_heating_coil_type:,
                               mau_type:,
                               model:,
                               baseline_system_zones_map_option:)
  system_zones_hash = {}
  # Determine if dwelling units have a shared AHU.  If user entered building stories > 4 then set to true.
  if baseline_system_zones_map_option == 'one_sys_per_dwelling_unit'
    dwelling_shared_ahu = false
  elsif baseline_system_zones_map_option == 'one_sys_per_bldg' || baseline_system_zones_map_option == 'NECB_Default' || baseline_system_zones_map_option == 'none' || baseline_system_zones_map_option == nil || necb_reference_hp
    dwelling_shared_ahu = true
  end
  # store dwelling zones into array
  zones = []
  model.getSpaces.select { |space| is_a_necb_dwelling_unit?(space) }.each do |space|
    zones << space.thermalZone.get
  end
  zones.uniq!

  # sort system 1 or 3 used for each dwelling unit as per T8.4.4.8.A NECB 2011-17
  zones.each do |zone|
    system_zones_hash[get_necb_thermal_zone_system_selection(zone)] = [] if system_zones_hash[get_necb_thermal_zone_system_selection(zone)].nil?
    system_zones_hash[get_necb_thermal_zone_system_selection(zone)] << zone
  end

  # go through each system and zones pairs to
  system_zones_hash.each_pair do |system, sys_zones|
    case system
    when 1
      if dwelling_shared_ahu
        add_sys1_unitary_ac_baseboard_heating(model: model,
                                              necb_reference_hp: necb_reference_hp,
                                              necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                              zones: sys_zones,
                                              mau_type: mau_type,
                                              mau_heating_coil_type: mau_heating_coil_type,
                                              baseboard_type: baseboard_type,
                                              hw_loop: @hw_loop,
                                              multispeed: false)
      else
        # Create a separate air loop for each unit.
        sys_zones.each do |zone|
          add_sys1_unitary_ac_baseboard_heating(model: model,
                                                necb_reference_hp: necb_reference_hp,
                                                necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                zones: [zone],
                                                mau_type: mau_type,
                                                mau_heating_coil_type: mau_heating_coil_type,
                                                baseboard_type: baseboard_type,
                                                hw_loop: @hw_loop,
                                                multispeed: false)
        end
      end

    when 3
      if dwelling_shared_ahu
        add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
                                                                              necb_reference_hp: necb_reference_hp,
                                                                              necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                                              zones: sys_zones,
                                                                              heating_coil_type: heating_coil_type_sys3,
                                                                              baseboard_type: baseboard_type,
                                                                              hw_loop: @hw_loop,
                                                                              multispeed: false)
      else
        # Create a separate air loop for each unit.
        sys_zones.each do |zone|
          add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
                                                                                necb_reference_hp: necb_reference_hp,
                                                                                necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                                                zones: [zone],
                                                                                heating_coil_type: heating_coil_type_sys3,
                                                                                baseboard_type: baseboard_type,
                                                                                hw_loop: @hw_loop,
                                                                                multispeed: false)
        end
      end
    end
  end
end

#auto_system_storage_spaces(baseboard_type:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', boiler_fueltype:, heating_coil_type_sys4:, model:) ⇒ Object

All wet spaces will be on their own system 4 AHU.



1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1151

def auto_system_storage_spaces(baseboard_type:,
                               necb_reference_hp:false,
                               necb_reference_hp_supp_fuel:'DefaultFuel',
                               boiler_fueltype:,
                               heating_coil_type_sys4:,
                               model:)
  # Determine what zones are storage zones.
  tz = []
  storage_spaces = model.getSpaces.select { |space| is_an_necb_storage_space?(space) }
  storage_spaces.each { |space| tz << space.thermalZone.get }
  tz.uniq!

  return if tz.empty?

  # create a system 4 for the  zones.
  add_sys4_single_zone_make_up_air_unit_with_baseboard_heating(model: model,
                                                               necb_reference_hp: necb_reference_hp,
                                                               necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                               zones: tz,
                                                               heating_coil_type: heating_coil_type_sys4,
                                                               baseboard_type: baseboard_type,
                                                               hw_loop: @hw_loop)
  #      add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
  #                                                                            zones: tz,
  #                                                                            heating_coil_type: heating_coil_type_sys4,
  #                                                                            baseboard_type: baseboard_type,
  #                                                                            hw_loop: @hw_loop,
  #                                                                            multispeed: true)
end

#auto_system_wet_spaces(baseboard_type:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', boiler_fueltype:, heating_coil_type_sys4:, model:) ⇒ Object

All wet spaces will be on their own system 4 AHU.



1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1121

def auto_system_wet_spaces(baseboard_type:,
                           necb_reference_hp:false,
                           necb_reference_hp_supp_fuel:'DefaultFuel',
                           boiler_fueltype:,
                           heating_coil_type_sys4:,
                           model:)
  # Determine what zones are wet zones.
  wet_tz = []
  wet_spaces = model.getSpaces.select { |space| is_an_necb_wet_space?(space) }
  wet_spaces.each { |space| wet_tz << space.thermalZone.get }
  wet_tz.uniq!
  # create a system 4 for the wet zones.
  return if wet_tz.empty?

  add_sys4_single_zone_make_up_air_unit_with_baseboard_heating(model: model,
                                                               necb_reference_hp: necb_reference_hp,
                                                               necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                               zones: wet_tz,
                                                               heating_coil_type: heating_coil_type_sys4,
                                                               baseboard_type: baseboard_type,
                                                               hw_loop: @hw_loop)
  #      add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
  #                                                                            zones: wet_tz,
  #                                                                            heating_coil_type: heating_coil_type_sys4,
  #                                                                            baseboard_type: baseboard_type,
  #                                                                            hw_loop: @hw_loop,
  #                                                                            multispeed: false)
end

#auto_system_wild_spaces(baseboard_type:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'Defaultfuel', heating_coil_type_sys4:, model:) ⇒ Object

All wild spaces will be on a single system 4 ahu with the largests heating load zone being the control zone.



1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1182

def auto_system_wild_spaces(baseboard_type:,
                            necb_reference_hp:false,
                            necb_reference_hp_supp_fuel:'Defaultfuel',
                            heating_coil_type_sys4:,
                            model:)

  zones = []
  wild_spaces = model.getSpaces.select { |space| !is_an_necb_wet_space?(space) && is_an_necb_wildcard_space?(space) }
  wild_spaces.each { |space| zones << space.thermalZone.get }
  zones.uniq!

  return if zones.empty?

  # create a system 4 for the wild zones.
  add_sys4_single_zone_make_up_air_unit_with_baseboard_heating(model: model,
                                                               necb_reference_hp: necb_reference_hp,
                                                               necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                               zones: zones,
                                                               heating_coil_type: heating_coil_type_sys4,
                                                               baseboard_type: baseboard_type,
                                                               hw_loop: @hw_loop)
  #      add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
  #                                                                            zones: zones,
  #                                                                            heating_coil_type: heating_coil_type_sys4,
  #                                                                            baseboard_type: baseboard_type,
  #                                                                            hw_loop: @hw_loop,
  #                                                                            multispeed: true)
end

#auto_zone_all_other_spaces(model) ⇒ Object

This method will find all the spaces that are not wet, wild or dwelling units and zone them. It will try to determine if the spaces are similar based on exposure and load and blend those spaces into the same zone. It will not merge spaces from different floors, since this will impact Chris Kirneys costing algorithms.



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 338

def auto_zone_all_other_spaces(model)
  other_tz_array = []
  # iterate through all non wildcard spaces.
  model.getSpaces.select { |space| !is_a_necb_dwelling_unit?(space) && !is_an_necb_wildcard_space?(space) }.each do |space|
    # skip if already assigned to a thermal zone.
    next unless space.thermalZone.empty?

    # create new zone for this space based on the space name.
    zone = OpenStudio::Model::ThermalZone.new(model)
    tz_name = "ALL_ST=#{space.spaceType.get.standardsSpaceType.get}_FL=#{space.buildingStory.get.name}_SCH=#{determine_dominant_schedule([space])}"
    zone.setName(tz_name)
    # sets space mulitplier unless it is nil or 1.
    unless space_multiplier_map[space.name.to_s].nil? || (space_multiplier_map[space.name.to_s] == 1)
      zone.setMultiplier(space_multiplier_map[space.name.to_s])
    end
    # Assign space to the new zone.
    space.setThermalZone(zone)

    # Add a thermostat
    space_type_name = space.spaceType.get.name.get
    thermostat_name = space_type_name + ' Thermostat'
    thermostat = model.getThermostatSetpointDualSetpointByName(thermostat_name)
    if thermostat.empty?
      # The thermostat name for the spacetype should exist.
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Thermostat #{thermostat_name} not found for space name: #{space.name}")
    else
      thermostat_clone = thermostat.get.clone(model).to_ThermostatSetpointDualSetpoint.get
      zone.setThermostatSetpointDualSetpoint(thermostat_clone)
      # Set Ideal loads to thermal zone for sizing for NECB needs. We need this for sizing.
      ideal_loads = OpenStudio::Model::ZoneHVACIdealLoadsAirSystem.new(model)
      ideal_loads.addToThermalZone(zone)
    end
    # Go through other spaces and if you find something with similar loads on the same floor, add it to the zone.
    model.getSpaces.select { |curr_space| !is_a_necb_dwelling_unit?(curr_space) && !is_an_necb_wildcard_space?(curr_space) }.each do |space_target|
      if space_target.thermalZone.empty?
        if are_space_loads_similar?(space_1: space, space_2: space_target) && space.buildingStory.get == space_target.buildingStory.get # added since chris needs zones to not span floors for costing.
          space_target.setThermalZone(zone)
        end
      end
    end
    other_tz_array << zone
  end
  return other_tz_array
end

#auto_zone_dwelling_units(model) ⇒ Object

Dwelling unit spaces need to have their own HVAC system. Thankfully NECB defines what spacetypes are considering dwelling units and have been defined as spaces that are openstudio-standards/standards/necb/NECB2011/data/necb_hvac_system_selection_type.json as spaces that are Residential/Accomodation and Sleeping area’ this is determine by the is_a_necb_dwelling_unit? method. The thermostat is set by the space-type schedule. This will return an array of TZ.



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 253

def auto_zone_dwelling_units(model)
  dwelling_tz_array = []
  # ----Dwelling units----------- will always have their own system per unit, so they should have their own thermal zone.
  model.getSpaces.select { |space| is_a_necb_dwelling_unit?(space) }.each do |space|
    zone = OpenStudio::Model::ThermalZone.new(model)
    zone.setName("DU_BT=#{space.spaceType.get.standardsBuildingType.get}_ST=#{space.spaceType.get.standardsSpaceType.get}_FL=#{space.buildingStory.get.name}_SCH#{determine_dominant_schedule([space])}")
    unless space_multiplier_map[space.name.to_s].nil? || (space_multiplier_map[space.name.to_s] == 1)
      zone.setMultiplier(space_multiplier_map[space.name.to_s])
    end
    space.setThermalZone(zone)

    # Add a thermostat based on the space type.
    space_type_name = space.spaceType.get.name.get
    thermostat_name = space_type_name + ' Thermostat'
    thermostat = model.getThermostatSetpointDualSetpointByName(thermostat_name)
    if thermostat.empty?
      # The thermostat name for the spacetype should exist.
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Thermostat #{thermostat_name} not found for space name: #{space.name}")
    else
      thermostat_clone = thermostat.get.clone(model).to_ThermostatSetpointDualSetpoint.get
      zone.setThermostatSetpointDualSetpoint(thermostat_clone)
      # Set Ideal loads to thermal zone for sizing for NECB needs. We need this for sizing.
      OpenStudio::Model::ZoneHVACIdealLoadsAirSystem.new(model).addToThermalZone(zone)
    end
    dwelling_tz_array << zone
  end
  return dwelling_tz_array
end

#auto_zone_wet_spaces(model:, lights_type: 'NECB_Default', lights_scale: 1.0) ⇒ Object

Something that the code is silent on are smelly humid areas that should not be on the same system as the rest of the

building.. These are the 'wet' spaces and have been defined as locker and washroom areas.. These will be put under

their own single system 4 system. These will be set to the dominant floor schedule.



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 286

def auto_zone_wet_spaces(model:, lights_type: 'NECB_Default', lights_scale: 1.0)
  wet_zone_array = []
  model.getSpaces.select { |space| is_an_necb_wet_space?(space) }.each do |space|
    # if this space was already assigned to something skip it.
    next unless space.thermalZone.empty?

    # get space to dominant schedule
    dominant_schedule = determine_dominant_schedule(space.model.getSpaces)
    # create new TZ and set space to the zone.
    zone = OpenStudio::Model::ThermalZone.new(model)
    space.setThermalZone(zone)
    tz_name = "WET_ST=#{space.spaceType.get.standardsSpaceType.get}_FL=#{space.buildingStory.get.name}_SCH#{dominant_schedule}"
    zone.setName(tz_name)
    # Set multiplier from the original tz multiplier.
    unless space_multiplier_map[space.name.to_s].nil? || (space_multiplier_map[space.name.to_s] == 1)
      zone.setMultiplier(space_multiplier_map[space.name.to_s])
    end

    # this method will determine if the right schedule was used for this wet & wild space if not.. it will reset the space
    # to use the correct schedule version of the wet and wild space type.
    adjust_wildcard_spacetype_schedule(space: space, schedule: dominant_schedule, lights_type: lights_type, lights_scale: lights_scale)

    # Find spacetype thermostat and assign it to the zone.
    thermostat_name = space.spaceType.get.name.get + ' Thermostat'
    thermostat = model.getThermostatSetpointDualSetpointByName(thermostat_name)
    if thermostat.empty?
      # The thermostat name for the spacetype should exist.
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Thermostat #{thermostat_name} not found for space name: #{space.name}-")
    else
      thermostat_clone = thermostat.get.clone(model).to_ThermostatSetpointDualSetpoint.get
      zone.setThermostatSetpointDualSetpoint(thermostat_clone)
      # Set Ideal loads to thermal zone for sizing for NECB needs. We need this for sizing.
      ideal_loads = OpenStudio::Model::ZoneHVACIdealLoadsAirSystem.new(model)
      ideal_loads.addToThermalZone(zone)
    end
    # Go through other spaces to see if there are similar spaces with similar loads on the same floor that can be grouped.
    model.getSpaces.select { |s| is_an_necb_wet_space?(s) }.each do |space_target|
      if space_target.thermalZone.empty?
        if are_space_loads_similar?(space_1: space, space_2: space_target) && space.buildingStory.get == space_target.buildingStory.get # added since chris needs zones to not span floors for costing.
          adjust_wildcard_spacetype_schedule(space: space_target, schedule: dominant_schedule, lights_type: lights_type, lights_scale: lights_scale)
          space_target.setThermalZone(zone)
        end
      end
    end
    wet_zone_array << zone
  end
  return wet_zone_array
end

#auto_zone_wild_spaces(model:, lights_type: 'NECB_Default', lights_scale: 1.0) ⇒ Object

This will take all the wildcard spaces and merge them to be supported by a system 4. The control zone will be the zone that has the largest heating load per area.



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 385

def auto_zone_wild_spaces(model:, lights_type: 'NECB_Default', lights_scale: 1.0)
  other_tz_array = []
  # iterate through wildcard spaces.
  model.getSpaces.select { |space| is_an_necb_wildcard_space?(space) && !is_an_necb_wet_space?(space) }.each do |space|
    # skip if already assigned to a thermal zone.
    next unless space.thermalZone.empty?

    # create new zone for this space based on the space name.
    zone = OpenStudio::Model::ThermalZone.new(model)
    tz_name = "WILD_ST=#{space.spaceType.get.standardsSpaceType.get}_FL=#{space.buildingStory.get.name}_SCH=#{determine_dominant_schedule(space.model.getSpaces)}"
    zone.setName(tz_name)
    # sets space mulitplier unless it is nil or 1.
    unless space_multiplier_map[space.name.to_s].nil? || (space_multiplier_map[space.name.to_s] == 1)
      zone.setMultiplier(space_multiplier_map[space.name.to_s])
    end
    # Assign space to the new zone.
    space.setThermalZone(zone)

    # lets keep the wild schedules to be the same as what dominate the floor.
    dominant_floor_schedule = determine_dominant_schedule(space.model.getSpaces)

    adjust_wildcard_spacetype_schedule(space: space,
                                       schedule: dominant_floor_schedule,
                                       lights_type: lights_type,
                                       lights_scale: lights_scale)

    # Add a thermostat
    space_type_name = space.spaceType.get.name.get
    thermostat_name = space_type_name + ' Thermostat'
    thermostat = model.getThermostatSetpointDualSetpointByName(thermostat_name)
    if thermostat.empty?
      # The thermostat name for the spacetype should exist.
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Thermostat #{thermostat_name} not found for space name: #{space.name}")
    else
      thermostat_clone = thermostat.get.clone(model).to_ThermostatSetpointDualSetpoint.get
      zone.setThermostatSetpointDualSetpoint(thermostat_clone)
      # Set Ideal loads to thermal zone for sizing for NECB needs. We need this for sizing.
      ideal_loads = OpenStudio::Model::ZoneHVACIdealLoadsAirSystem.new(model)
      ideal_loads.addToThermalZone(zone)
    end
    # Go through other spaces and if you find something with similar loads on the same floor, add it to the zone.
    model.getSpaces.select { |curr_space| is_an_necb_wildcard_space?(curr_space) && !is_an_necb_wet_space?(curr_space) }.each do |space_target|
      if space_target.thermalZone.empty?
        if are_space_loads_similar?(space_1: space, space_2: space_target) &&
           (space.buildingStory.get == space_target.buildingStory.get) # added since chris needs zones to not span floors for costing.
          space_target.setThermalZone(zone)
        end
      end
    end
    other_tz_array << zone
  end
  return other_tz_array

  wild_zone_array = []
  # Get a list of all the wild spaces.
  model.getSpaces.select { |space| is_an_necb_wildcard_space?(space) && !is_an_necb_wet_space?(space) }.each do |space|
    # if this space was already assigned to something skip it.
    next unless space.thermalZone.empty?

    # find adjacent spaces to the current space.
    adj_spaces = OpenstudioStandards::Geometry.space_get_adjacent_spaces_with_shared_wall_areas(space, true)
    adj_spaces = adj_spaces.map { |key, value| key }

    # find unassigned adjacent wild spaces that have not been assigned that have the same multiplier these will be
    # lumped together in the same zone.
    wild_adjacent_spaces = adj_spaces.select do |adj_space|
      is_an_necb_wildcard_space?(adj_space) &&
        !is_an_necb_wet_space?(adj_space) &&
        adj_space.thermalZone.empty? &&
        (space_multiplier_map[space.name.to_s] == space_multiplier_map[adj_space.name.to_s])
    end
    # put them all together.
    wild_adjacent_spaces << space

    # Get adjacent candidate foster zones. Must not be a wildcard space and must not be linked to another space incase
    # it is part of a mirrored space.
    other_adjacent_spaces = adj_spaces.select do |adj_space|
      (is_an_necb_wildcard_space?(adj_space) == false) &&
        (adj_space.thermalZone.get.spaces.size == 1) &&
        (space_multiplier_map[space.name.to_s] == space_multiplier_map[adj_space.name.to_s])
    end

    # If there are adjacent spaces that fit the above criteria.
    # We will need to set each space to the dominant floor schedule by setting the spaces spacetypes to that
    # schedule version and eventually set it to a system 4
    unless other_adjacent_spaces.empty?
      # assign the space(s) to the adjacent thermal zone.
      schedule_type = determine_dominant_schedule(space.buildingStory.get.spaces)
      zone = other_adjacent_spaces.first.thermalZone.get
      wild_adjacent_spaces.each do |curr_space|
        adjust_wildcard_spacetype_schedule(curr_space, schedule_type, @lights_type, @lights_scale, @space_height)
        curr_space.setThermalZone(zone)
      end
    end

    # create new TZ and set space to the zone.
    zone = OpenStudio::Model::ThermalZone.new(model)
    space.setThermalZone(zone)
    zone.setName("Wild-ZN:BT=#{space.spaceType.get.standardsBuildingType.get}:ST=#{space.spaceType.get.standardsSpaceType.get}:FL=#{space.buildingStory.get.name}:")
    # Set multiplier from the original tz multiplier.
    unless space_multiplier_map[space.name.to_s].nil? || (space_multiplier_map[space.name.to_s] == 1)
      zone.setMultiplier(space_multiplier_map[space.name.to_s])
    end

    # Set space to dominant

    dominant_floor_schedule = determine_dominant_schedule(space.buildingStory.get.spaces)
    # this method will determine if the right schedule was used for this wet & wild space if not.. it will reset the space
    # to use the correct schedule version of the wet and wild space type.
    adjust_wildcard_spacetype_schedule(space: space, schedule: dominant_floor_schedule, lights_type: @lights_type, lights_scale: @lights_scale)
    # Find spacetype thermostat and assign it to the zone.
    thermostat_name = space.spaceType.get.name.get + ' Thermostat'
    thermostat = model.getThermostatSetpointDualSetpointByName(thermostat_name)
    if thermostat.empty?
      # The thermostat name for the spacetype should exist.
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Thermostat #{thermostat_name} not found for space name: #{space.name}")
    else
      thermostat_clone = thermostat.get.clone(model).to_ThermostatSetpointDualSetpoint.get
      zone.setThermostatSetpointDualSetpoint(thermostat_clone)
      # Set Ideal loads to thermal zone for sizing for NECB needs. We need this for sizing.
      ideal_loads = OpenStudio::Model::ZoneHVACIdealLoadsAirSystem.new(model)
      ideal_loads.addToThermalZone(zone)
    end
    # Go through other spaces to see if there are similar spaces with similar loads on the same floor that can be grouped.
    model.getSpaces.select { |s| is_an_necb_wildcard_space?(s) && !is_an_necb_wet_space?(s) }.each do |space_target|
      if space_target.thermalZone.empty?
        if are_space_loads_similar?(space_1: space, space_2: space_target) &&
           space.buildingStory.get == space_target.buildingStory.get # added since chris needs zones to not span floors for costing.
          adjust_wildcard_spacetype_schedule(space: space_target, schedule: dominant_floor_schedule, lights_type: @lights_type, lights_scale: @lights_scale)
          space_target.setThermalZone(zone)
        end
      end
    end
    wild_zone_array << zone
  end
  return wild_zone_array
end

#boiler_hot_water_apply_efficiency_and_curves(boiler_hot_water) ⇒ Boolean

Applies the standard efficiency ratings and typical performance curves to this object.

Parameters:

  • boiler_hot_water (OpenStudio::Model::BoilerHotWater)

    the object to modify

Returns:

  • (Boolean)

    true if successful, false if not



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 533

def boiler_hot_water_apply_efficiency_and_curves(boiler_hot_water)
  successfully_set_all_properties = false

  # Define the criteria to find the boiler properties
  # in the hvac standards data set.
  search_criteria = boiler_hot_water_find_search_criteria(boiler_hot_water)
  fuel_type = search_criteria['fuel_type']
  fluid_type = search_criteria['fluid_type']

  # Get the capacity
  capacity_w = boiler_hot_water_find_capacity(boiler_hot_water)

  # Check if secondary and/or modulating boiler required
  # If boiler names include 'Primary Boiler' or 'Secondary Boiler' then NECB rules are applied
  boiler_capacity = capacity_w
  if boiler_hot_water.name.to_s.include?('Primary Boiler') || boiler_hot_water.name.to_s.include?('Secondary Boiler')
    if capacity_w / 1000.0 >= 352.0
      if boiler_hot_water.name.to_s.include?('Primary Boiler')
        boiler_capacity = capacity_w
        boiler_hot_water.setBoilerFlowMode('LeavingSetpointModulated')
        boiler_hot_water.setMinimumPartLoadRatio(0.25)
      elsif boiler_hot_water.name.to_s.include?('Secondary Boiler')
        boiler_capacity = 0.001
      end
    elsif ((capacity_w / 1000.0) >= 176.0) && ((capacity_w / 1000.0) < 352.0)
      boiler_capacity = capacity_w / 2
    elsif (capacity_w / 1000.0) <= 176.0
      if boiler_hot_water.name.to_s.include?('Primary Boiler')
        if capacity_w <= 1.0
          boiler_capacity = 1.0
        else
          boiler_capacity = capacity_w
        end
      elsif boiler_hot_water.name.to_s.include?('Secondary Boiler')
        boiler_capacity = 0.001
      end
    end
  end
  boiler_hot_water.setNominalCapacity(boiler_capacity)

  # Convert capacity to Btu/hr
  capacity_btu_per_hr = OpenStudio.convert(boiler_capacity, 'W', 'Btu/hr').get
  capacity_kbtu_per_hr = OpenStudio.convert(boiler_capacity, 'W', 'kBtu/hr').get

  # Get the boiler properties
  boiler_table = @standards_data['boilers']
  blr_props = model_find_object(boiler_table, search_criteria, capacity_btu_per_hr)
  unless blr_props
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.BoilerHotWater', "For #{boiler_hot_water.name}, cannot find boiler properties, cannot apply efficiency standard.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  # Make the EFFFPLR curve
  eff_fplr = model_add_curve(boiler_hot_water.model, blr_props['efffplr'])
  if eff_fplr
    boiler_hot_water.setNormalizedBoilerEfficiencyCurve(eff_fplr)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.BoilerHotWater', "For #{boiler_hot_water.name}, cannot find eff_fplr curve, will not be set.")
    successfully_set_all_properties = false
  end

  # Get the minimum efficiency standards
  thermal_eff = nil

  # If specified as AFUE
  unless blr_props['minimum_annual_fuel_utilization_efficiency'].nil?
    min_afue = blr_props['minimum_annual_fuel_utilization_efficiency']
    thermal_eff = afue_to_thermal_eff(min_afue)
    new_comp_name = "#{boiler_hot_water.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{min_afue} AFUE"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.BoilerHotWater', "For #{template}: #{boiler_hot_water.name}: #{fuel_type} #{fluid_type} Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; AFUE = #{min_afue}")
  end

  # If specified as thermal efficiency
  unless blr_props['minimum_thermal_efficiency'].nil?
    thermal_eff = blr_props['minimum_thermal_efficiency']
    new_comp_name = "#{boiler_hot_water.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{thermal_eff} Thermal Eff"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.BoilerHotWater', "For #{template}: #{boiler_hot_water.name}: #{fuel_type} #{fluid_type} Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; Thermal Efficiency = #{thermal_eff}")
  end

  # If specified as combustion efficiency
  unless blr_props['minimum_combustion_efficiency'].nil?
    min_comb_eff = blr_props['minimum_combustion_efficiency']
    thermal_eff = combustion_eff_to_thermal_eff(min_comb_eff)
    new_comp_name = "#{boiler_hot_water.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{min_comb_eff} Combustion Eff"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.BoilerHotWater', "For #{template}: #{boiler_hot_water.name}: #{fuel_type} #{fluid_type} Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; Combustion Efficiency = #{min_comb_eff}")
  end

  # Set the name
  boiler_hot_water.setName(new_comp_name)

  # Set the efficiency values
  unless thermal_eff.nil?
    boiler_hot_water.setNominalThermalEfficiency(thermal_eff)
  end

  return successfully_set_all_properties
end

#boiler_hot_water_find_search_criteria(boiler_hot_water) ⇒ Hash

find search criteria

Parameters:

  • boiler_hot_water (OpenStudio::Model::BoilerHotWater)

    hot water boiler object

Returns:

  • (Hash)

    used for standards_lookup_table(model)



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 503

def boiler_hot_water_find_search_criteria(boiler_hot_water)
  # Define the criteria to find the boiler properties
  # in the hvac standards data set.
  search_criteria = {}
  search_criteria['template'] = template
  # Get fuel type
  fuel_type = nil
  case boiler_hot_water.fuelType
  when 'NaturalGas'
    fuel_type = 'Gas'
  when 'Electricity'
    fuel_type = 'Electric'
  when 'FuelOilNo1', 'FuelOilNo2'
    fuel_type = 'Oil'
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.BoilerHotWater', "For #{boiler_hot_water.name}, a fuel type of #{fuel_type} is not yet supported.  Assuming 'Gas.'")
    fuel_type = 'Gas'
  end

  search_criteria['fuel_type'] = fuel_type
  # Get the fluid type
  fluid_type = 'Hot Water'
  search_criteria['fluid_type'] = fluid_type
  return search_criteria
end

#check_boolean_value(value, varname) ⇒ Object

Raises:

  • (ArgumentError)


1904
1905
1906
1907
1908
1909
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1904

def check_boolean_value(value, varname)
  return true if value =~ /^(true|t|yes|y|1)$/i
  return false if value.empty? || value =~ /^(false|f|no|n|0)$/i

  raise ArgumentError, "invalid value for #{varname}: #{value}"
end

#check_datapoint_weather_folder(epw_file:, weather_folder:, custom_weather_folder: nil) ⇒ Object

This method checks if a zip file containing weather data is stored in an external directory. If it is, then it checks if the name of the zip file (without extension) matches the name of the desired epw file (without extension). If it does then it copies the zip file to the openstudio-standards weather directory and expands the file. Presumably the zip file contains the .ddy, .epw, and .stat files needed by the rest of BTAP. If no appropriate weather zip files are present in the external directory then the method returns false. Arguments: epw_file (string): The name of the .epw file that BTAP will use. weather_folder (string): The path to the openstudio-standards weather folder. custom_weather_folder (string): The path to the external folder presumably containing the weather data zip file.



2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2342

def check_datapoint_weather_folder(epw_file:, weather_folder:, custom_weather_folder: nil)
  # Check if the external weather directory exists and return false if there isn't one
  return false if custom_weather_folder.nil?
  # Check if there are any zip files in the external weather directory and return false if there isn't any.
  zip_search_term = File.join(custom_weather_folder.to_s, '*.zip')
  zip_files = Dir[zip_search_term]
  return false if zip_files.empty?
  # Look for a zip file in the external directory named after the .epw file.  If there isn't one return false.
  weather_loc = epw_file[0..-5]
  weather_zip = weather_loc + '.zip'
  zip_find = zip_files.select{ |check_file | File.basename(check_file).to_s.downcase == weather_zip.to_s.downcase }
  return false if zip_find.empty?
  # Copy the zip file we want from the external weather directory to the openstudio-standards weather directory and
  # extract the weather data in the file.
  puts "Copying: #{zip_find[0]} from the btap_cli weather folder to the openstudio-standards weather folder: #{weather_folder}"
  FileUtils.cp(zip_find[0], weather_folder)
  dest_zip = File.join(weather_folder, weather_zip)
  # Return true if everything goes well.
  return extract_weather_data(zipped_file: dest_zip, weather_dir: weather_folder)
end

#chiller_electric_eir_apply_efficiency_and_curves(chiller_electric_eir, clg_tower_objs) ⇒ Boolean

Applies the standard efficiency ratings and typical performance curves to this object.

Returns:

  • (Boolean)

    true if successful, false if not



635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 635

def chiller_electric_eir_apply_efficiency_and_curves(chiller_electric_eir, clg_tower_objs)
  # Define the criteria to find the chiller properties
  # in the hvac standards data set.
  search_criteria = chiller_electric_eir_find_search_criteria(chiller_electric_eir)
  cooling_type = search_criteria['cooling_type']
  condenser_type = search_criteria['condenser_type']
  compressor_type = search_criteria['compressor_type']

  # Get the chiller capacity
  capacity_w = chiller_electric_eir_find_capacity(chiller_electric_eir)

  # All chillers must be modulating down to 25% of their capacity
  chiller_electric_eir.setChillerFlowMode('LeavingSetpointModulated')
  chiller_electric_eir.setMinimumPartLoadRatio(0.25)
  chiller_electric_eir.setMinimumUnloadingRatio(0.25)
  chiller_capacity = capacity_w
  # If the chiller name includes 'Primary' or 'Secondary' then apply NECB rules
  if (chiller_electric_eir.name.to_s.include? 'Primary') || (chiller_electric_eir.name.to_s.include? 'Secondary')
    if (capacity_w / 1000.0) < 2100.0
      if chiller_electric_eir.name.to_s.include? 'Primary Chiller'
        chiller_capacity = capacity_w
      elsif chiller_electric_eir.name.to_s.include? 'Secondary Chiller'
        chiller_capacity = 0.001
      end
    else
      chiller_capacity = capacity_w / 2.0
    end
  end
  chiller_electric_eir.setReferenceCapacity(chiller_capacity)

  # Convert capacity to tons
  capacity_tons = OpenStudio.convert(chiller_capacity, 'W', 'ton').get

  # Get chiller compressor type if needed
  chiller_types = ['reciprocating','scroll','rotary screw','centrifugal']
  chiller_name_has_type = chiller_types.any? {|type| chiller_electric_eir.name.to_s.downcase.include? type}
  unless chiller_name_has_type
    chlr_type_search_criteria = {}
    chlr_type_search_criteria['cooling_type'] = cooling_type
    chlr_types_table = @standards_data['chiller_types']
    chlr_type_props = model_find_object(chlr_types_table, chlr_type_search_criteria, capacity_tons)
    unless chlr_type_props
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ChillerElectricEIR', "For #{chiller_electric_eir.name}, cannot find chiller type information")
      successfully_set_all_properties = false
      return successfully_set_all_properties
    end
    compressor_type = chlr_type_props['compressor_type']
    chiller_electric_eir.setName(chiller_electric_eir.name.to_s + ' ' + compressor_type)
  end
  # Get the chiller properties
  search_criteria['compressor_type'] = compressor_type
  chlr_table = @standards_data['chillers']
  chlr_props = model_find_object(chlr_table, search_criteria, capacity_tons, Date.today)
  unless chlr_props
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ChillerElectricEIR', "For #{chiller_electric_eir.name}, cannot find chiller properties, cannot apply standard efficiencies or curves.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  # Make the CAPFT curve
  cool_cap_ft = model_add_curve(chiller_electric_eir.model, chlr_props['capft'])
  if cool_cap_ft
    chiller_electric_eir.setCoolingCapacityFunctionOfTemperature(cool_cap_ft)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ChillerElectricEIR', "For #{chiller_electric_eir.name}, cannot find cool_cap_ft curve, will not be set.")
    successfully_set_all_properties = false
  end

  # Make the EIRFT curve
  cool_eir_ft = model_add_curve(chiller_electric_eir.model, chlr_props['eirft'])
  if cool_eir_ft
    chiller_electric_eir.setElectricInputToCoolingOutputRatioFunctionOfTemperature(cool_eir_ft)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ChillerElectricEIR', "For #{chiller_electric_eir.name}, cannot find cool_eir_ft curve, will not be set.")
    successfully_set_all_properties = false
  end

  # Make the EIRFPLR curve
  # which may be either a CurveBicubic or a CurveQuadratic based on chiller type
  cool_plf_fplr = model_add_curve(chiller_electric_eir.model, chlr_props['eirfplr'])
  if cool_plf_fplr
    chiller_electric_eir.setElectricInputToCoolingOutputRatioFunctionOfPLR(cool_plf_fplr)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ChillerElectricEIR', "For #{chiller_electric_eir.name}, cannot find cool_plf_fplr curve, will not be set.")
    successfully_set_all_properties = false
  end

  # Set the efficiency value
  kw_per_ton = nil
  cop = nil
  if chlr_props['minimum_full_load_efficiency']
    kw_per_ton = chlr_props['minimum_full_load_efficiency']
    cop = kw_per_ton_to_cop(kw_per_ton)
    chiller_electric_eir.setReferenceCOP(cop)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ChillerElectricEIR', "For #{chiller_electric_eir.name}, cannot find minimum full load efficiency, will not be set.")
    successfully_set_all_properties = false
  end

  # Set cooling tower properties now that the new COP of the chiller is set
  if chiller_electric_eir.name.to_s.include? 'Primary Chiller'
    # Single speed tower model assumes 25% extra for compressor power
    tower_cap = capacity_w * (1.0 + 1.0 / chiller_electric_eir.referenceCOP)
    if (tower_cap / 1000.0) < 1750
      clg_tower_objs[0].setNumberofCells(1)
    else
      clg_tower_objs[0].setNumberofCells((tower_cap / (1000 * 1750) + 0.5).round)
    end
    clg_tower_objs[0].setFanPoweratDesignAirFlowRate(0.015 * tower_cap)
  end

  # Append the name with size and kw/ton
  chiller_electric_eir.setName("#{chiller_electric_eir.name} #{capacity_tons.round}tons #{kw_per_ton.round(1)}kW/ton")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.ChillerElectricEIR', "For #{template}: #{chiller_electric_eir.name}: #{cooling_type} #{condenser_type} #{compressor_type} Capacity = #{capacity_tons.round}tons; COP = #{cop.round(1)} (#{kw_per_ton.round(1)}kW/ton)")

  return successfully_set_all_properties
end

#clean_and_scale_model(model:, rotation_degrees: nil, scale_x: nil, scale_y: nil, scale_z: nil) ⇒ Object

This method cleans the model of any existing HVAC systems and applies any desired ratation or scaling to the model.



478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 478

def clean_and_scale_model(model:, rotation_degrees: nil, scale_x: nil, scale_y: nil, scale_z: nil)
  # clean model..
  BTAP::Resources::Envelope::remove_all_envelope_information(model)
  model = remove_all_hvac(model)
  model.getThermalZones.sort.each { |zone| zone.setUseIdealAirLoads(true) }
  model.getZoneHVACPackagedTerminalAirConditioners.each(&:remove)
  model.getCoilCoolingDXSingleSpeeds.each(&:remove)
  model.getZoneHVACBaseboardConvectiveWaters.each(&:remove)
  model.getAirLoopHVACZoneMixers.each(&:remove)
  model.getAirLoopHVACZoneSplitters.each(&:remove)
  model.getAirTerminalSingleDuctConstantVolumeNoReheats.each(&:remove)
  model.getWaterUseEquipmentDefinitions.each(&:remove)
  model.getWaterUseEquipments.each(&:remove)
  model.getWaterUseConnectionss.each(&:remove)
  model.getPumpConstantSpeeds.each(&:remove)
  model.getPumpVariableSpeeds.each(&:remove)
  model.getBoilerHotWaters.each(&:remove)
  model.getBoilerSteams.each(&:remove)
  model.getPlantLoops.each(&:remove)
  model.getSchedules.each(&:remove)
  model.getThermalZones.sort.each { |zone| zone.thermostat(&:remove) }
  model.getSpaces.sort.each { |space| space.designSpecificationOutdoorAir(&:remove) }
  model.getThermostatSetpointDualSetpoints.each(&:remove)

  scale_x = 1.0
  scale_y = 1.0
  scale_z = 1.0
  # Rotate to model if requested
  rotation_degrees = convert_arg_to_f(variable: rotation_degrees, default: 0.0)
  BTAP::Geometry.rotate_building(model: model, degrees: rotation_degrees) unless rotation_degrees == 0.0

  # Scale model if requested
  scale_x = convert_arg_to_f(variable: scale_x, default: 1.0)
  scale_y = convert_arg_to_f(variable: scale_y, default: 1.0)
  scale_z = convert_arg_to_f(variable: scale_z, default: 1.0)
  return unless scale_x != 1.0 || scale_y != 1.0 || scale_z != 1.0

  BTAP::Geometry.scale_model(model, scale_x, scale_y, scale_z)
end

#coil_cooling_dx_multi_speed_apply_efficiency_and_curves(coil_cooling_dx_multi_speed, sql_db_vars_map) ⇒ Boolean

Applies the standard efficiency ratings and typical performance curves to this object.

Returns:

  • (Boolean)

    true if successful, false if not



892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 892

def coil_cooling_dx_multi_speed_apply_efficiency_and_curves(coil_cooling_dx_multi_speed, sql_db_vars_map)
  successfully_set_all_properties = true
  model = coil_cooling_dx_multi_speed.model
  multi_speed_heat_pump = coil_cooling_dx_multi_speed.containingHVACComponent.get.to_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed.get
  airloop = multi_speed_heat_pump.airLoopHVAC.get

  # Define the criteria to find the properties in the hvac standards data set
  search_criteria = coil_dx_find_search_criteria(coil_cooling_dx_multi_speed)
  capacity_w = coil_cooling_dx_multi_speed_find_capacity(coil_cooling_dx_multi_speed)

  # Find design outside air flow rate and flow fraction
  controller_oa = nil
  if airloop.airLoopHVACOutdoorAirSystem.is_initialized
    oa_system = airloop.airLoopHVACOutdoorAirSystem.get
    controller_oa = oa_system.getControllerOutdoorAir
  end
  min_oa_flow_rate = 0.0
  oaf = 0.0

  if controller_oa
    min_oa_flow_rate = nil
    if controller_oa.minimumOutdoorAirFlowRate.is_initialized
      min_oa_flow_rate = controller_oa.minimumOutdoorAirFlowRate.get
    elsif controller_oa.autosizedMinimumOutdoorAirFlowRate.is_initialized
      min_oa_flow_rate = controller_oa.autosizedMinimumOutdoorAirFlowRate.get
    end
    if min_oa_flow_rate then oaf = min_oa_flow_rate.to_f / airloop.autosizedDesignSupplyAirFlowRate.to_f end
  end

  # Find required capacity of each stage and total number of stages based on NECB rules
  # This implementation is limited to 4 stages only. The capacity of stages 1-3 is set to
  # 66 kW as stipulated by NECB. The capacity of the 4th stage is then allowed to exceed 66 kW
  # up to the design capacity.
  stage_cap = []
  num_stages = (capacity_w / (66.0 * 1000.0) + 0.5).round
  max_cap = 66.0 * 1000.0 * num_stages
  final_num_stages = num_stages
  case num_stages
  when 1
    stage_cap[0] = capacity_w / 2.0
    stage_cap[1] = 2.0 * stage_cap[0]
    final_num_stages = 2
  else
    stage_cap[0] = 66.0 * 1000.0
    stage_cap[1] = 2.0 * stage_cap[0]
    case num_stages
    when 2
    when 3
      stage_cap[2] = 3.0 * stage_cap[0]
    else
      final_num_stages = 4
      stage_cap[2] = 3.0 * stage_cap[0]
      stage_cap[3] = max_cap
    end
  end

  # Set final number of cooling stages and create missing stages if needed
  for istage in 2..final_num_stages - 1
    new_clg_stage = OpenStudio::Model::CoilCoolingDXMultiSpeedStageData.new(model)
    coil_cooling_dx_multi_speed.addStage(new_clg_stage)
  end
  multi_speed_heat_pump.setNumberofSpeedsforCooling(final_num_stages)

  # Set final capacities for each of the stages. The flow rate for each of the stages
  # is maintained above the outside air flow rate
  coil_cooling_dx_multi_speed.stages[0].setGrossRatedTotalCoolingCapacity(stage_cap[0])
  coil_cooling_dx_multi_speed.stages[1].setGrossRatedTotalCoolingCapacity(stage_cap[1])
  case coil_cooling_dx_multi_speed.stages.size
  when 2
    if oaf > 0.5 then multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate) end
  when 3
    coil_cooling_dx_multi_speed.stages[2].setGrossRatedTotalCoolingCapacity(stage_cap[2])
    if (oaf > 0.333) && (oaf <= 0.666)
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
    elsif oaf > 0.666
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed2SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
    end
  when 4
    coil_cooling_dx_multi_speed.stages[2].setGrossRatedTotalCoolingCapacity(stage_cap[2])
    coil_cooling_dx_multi_speed.stages[3].setGrossRatedTotalCoolingCapacity(stage_cap[3])
    if (oaf > 0.25) && (oaf <= 0.5)
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
    elsif (oaf > 0.5) && (oaf <= 0.75)
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed2SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
    elsif oaf > 0.75
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed2SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed3SupplyAirFlowRateDuringCoolingOperation(min_oa_flow_rate)
    end
  end

  capacity_btu_per_hr = OpenStudio.convert(stage_cap.last, 'W', 'Btu/hr').get
  capacity_kbtu_per_hr = OpenStudio.convert(stage_cap.last, 'W', 'kBtu/hr').get

  # Lookup efficiencies depending on whether it is a unitary AC or a heat pump
  ac_props = nil
  ac_props = if coil_dx_heat_pump?(coil_cooling_dx_multi_speed)
               model_find_object(standards_data['heat_pumps'], search_criteria, capacity_btu_per_hr, Date.today)
             else
               model_find_object(standards_data['unitary_acs'], search_criteria, capacity_btu_per_hr, Date.today)
             end

  # Check to make sure properties were found
  if ac_props.nil?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilCoolingDXMultiSpeed', "For #{coil_cooling_dx_multi_speed.name}, cannot find efficiency info, cannot apply efficiency standard.")
    successfully_set_all_properties = false
    return sql_db_vars_map
  end

  # get clg stages
  clg_stages = coil_cooling_dx_multi_speed.stages

  # Make the COOL-CAP-FT curve
  cool_cap_ft = model_add_curve(model, ac_props['cool_cap_ft'])
  if cool_cap_ft
    clg_stages.sort.each do |stage|
      stage.setTotalCoolingCapacityFunctionofTemperatureCurve(cool_cap_ft)
    end
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilCoolingDXMultiSpeed', "For #{coil_cooling_dx_multi_speed.name}, cannot find cool_cap_ft curve, will not be set.")
    successfully_set_all_properties = false
    return sql_db_vars_map
  end

  # Make the COOL-CAP-FFLOW curve
  cool_cap_fflow = model_add_curve(model, ac_props['cool_cap_fflow'])
  if cool_cap_fflow
    clg_stages.sort.each do |stage|
      stage.setTotalCoolingCapacityFunctionofFlowFractionCurve(cool_cap_fflow)
    end
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilCoolingDXMultiSpeed', "For #{coil_cooling_dx_multi_speed.name}, cannot find cool_cap_fflow curve, will not be set.")
    successfully_set_all_properties = false
    return sql_db_vars_map
  end

  # Make the COOL-EIR-FT curve
  cool_eir_ft = model_add_curve(model, ac_props['cool_eir_ft'])
  if cool_eir_ft
    clg_stages.sort.each do |stage|
      stage.setEnergyInputRatioFunctionofTemperatureCurve(cool_eir_ft)
    end
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilCoolingDXMultiSpeed', "For #{coil_cooling_dx_multi_speed.name}, cannot find cool_eir_ft curve, will not be set.")
    successfully_set_all_properties = false
    return sql_db_vars_map
  end

  # Make the COOL-EIR-FFLOW curve
  cool_eir_fflow = model_add_curve(model, ac_props['cool_eir_fflow'])
  if cool_eir_fflow
    clg_stages.sort.each do |stage|
      stage.setEnergyInputRatioFunctionofFlowFractionCurve(cool_eir_fflow)
    end
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilCoolingDXMultiSpeed', "For #{coil_cooling_dx_multi_speed.name}, cannot find cool_eir_fflow curve, will not be set.")
    successfully_set_all_properties = false
    return sql_db_vars_map
  end

  # Make the COOL-PLF-FPLR curve
  cool_plf_fplr = model_add_curve(model, ac_props['cool_plf_fplr'])
  if cool_plf_fplr
    clg_stages.sort.each do |stage|
      stage.setPartLoadFractionCorrelationCurve(cool_plf_fplr)
    end
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilCoolingDXMultiSpeed', "For #{coil_cooling_dx_multi_speed.name}, cannot find cool_plf_fplr curve, will not be set.")
    successfully_set_all_properties = false
    return sql_db_vars_map
  end

  # Set the COP values
  cop, new_comp_name = coil_cooling_dx_multi_speed_standard_minimum_cop(coil_cooling_dx_multi_speed)
  unless cop.nil?
    clg_stages.sort.each do |curr_istage|
      curr_istage.setGrossRatedCoolingCOP(cop)
    end
  end
  sql_db_vars_map[new_comp_name] = coil_cooling_dx_multi_speed.name.to_s
  coil_cooling_dx_multi_speed.setName(new_comp_name)

  # It was found that the heat pump OS object doesn't respond to the call to turn on from the
  # system availability manager night cycle. This EMS script is then implemented to check the status
  # of the system availability manager night cycle and force the heat pump to turn on when needed. The
  # heat pump is still turned on when its availability schedule calls for it.
  create_ems_to_turn_on_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed_for_night_cycle(multi_speed_heat_pump)

  return sql_db_vars_map
end

#coil_dx_heating_type(coil_dx, necb_reference_hp = false) ⇒ Object

NECB reference heat pump system heating type rules need to be flexible to account for

  1. DX htg/cooling + gas supplement htg

  2. Potential lack of AirLoopHVACUnitaryHeatPumpAirToAir or AirLoopHVACUnitarySystem

Parameters:

  • necb_reference_hp (Boolean) (defaults to: false)

    if true, NECB reference model rules for heat pumps will be used.



2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2427

def coil_dx_heating_type(coil_dx, necb_reference_hp = false)
  supp_htg_type = nil

  # If not heat pump reference case use the standard implementation.
  if !necb_reference_hp
    return super(coil_dx)
  else
    if coil_dx.airLoopHVAC.empty?
      if coil_dx.containingHVACComponent.is_initialized
        containing_comp = coil_dx.containingHVACComponent.get
        if containing_comp.to_AirLoopHVACUnitaryHeatPumpAirToAir.is_initialized
          supp_htg_coil = containing_comp.to_AirLoopHVACUnitaryHeatPumpAirToAir.get.supplementalHeatingCoil
          if supp_htg_coil.to_CoilHeatingElectric.is_initialized
            supp_htg_type = 'Electric Resistance or None'
          elsif supp_htg_coil.to_CoilHeatingGas.is_initialized or supp_htg_coil.to_CoilHeatingWater.is_initialized
            supp_htg_type = 'All Other'
          else # None
            supp_htg_type = 'Electric Resistance or None'
          end
        else
          # For other virtual wrapper, use method in Standard.DXCoil
          # Or add future wrappers here
          return super
        end
      end

    elsif coil_dx.airLoopHVAC.is_initialized # Heat pumps without a wrapper (lone DX coils in the air loop)
      airloop = coil_dx.airLoopHVAC.get
      num_of_DX_Coils = 0
      num_of_supp_coils = 0
      supp_htg_type = ''
      # Go through and determine number of each type of coils in air loop to determine supp_htg_type
      airloop.supplyComponents.each do |supply_component|
        if supply_component.to_CoilHeatingDXSingleSpeed.is_initialized or supply_component.to_CoilHeatingDXMultiSpeed.is_initialized
          supply_component.to_CoilHeatingDXVariableSpeed.is_initialized
          num_of_DX_Coils = num_of_DX_Coils + 1
        elsif supply_component.to_CoilCoolingDXSingleSpeed.is_initialized or supply_component.to_CoilCoolingDXTwoSpeed.is_initialized or
          supply_component.to_CoilCoolingDXTwoSpeed.is_initialized or supply_component.to_CoilCoolingDXVariableSpeed.is_initialized or
          supply_component.to_CoilCoolingDXMultiSpeed.is_initialized or
          supply_component.to_CoilCoolingDXCurveFitPerformance.is_initialized or
          supply_component.to_CoilCoolingDXTwoStageWithHumidityControlMode.is_initialized
          num_of_DX_Coils = num_of_DX_Coils + 1
        elsif supply_component.to_CoilHeatingGas.is_initialized or supply_component.to_CoilHeatingGasMultiStage.is_initialized or
          supply_component.to_CoilHeatingWater.is_initialized
          num_of_supp_coils = num_of_supp_coils + 1
          supp_htg_type = 'All Other'
        elsif supply_component.to_CoilHeatingElectric.is_initialized
          num_of_supp_coils = num_of_supp_coils + 1
          supp_htg_type = 'Electric Resistance or None'
        end
      end

      #Two possible heat pump configuration
      if num_of_DX_Coils == 2 && num_of_supp_coils == 1 #Scenario 1: 1 DX htg + 1 DX clg + 1 Non-DX htg coil
        puts "scenario 1 supp_htg_type #{supp_htg_type}"
        return supp_htg_type # return supplmental heating type
      else #Scenario 2: num_of_DX_Coils < 2 or num_of_supp_coils = 0;
        puts "scenario 2 supp_htg_type #{supp_htg_type}"
        puts "num_of_DX_Coils #{num_of_DX_Coils}"
        puts "num_of_supp_coils #{num_of_supp_coils}"
        return supp_htg_type = 'Electric Resistance or None'
      end
    end
  end
end

#coil_heating_dx_single_speed_find_capacity(coil_heating_dx_single_speed, necb_reference_hp = false) ⇒ Object



2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2334

def coil_heating_dx_single_speed_find_capacity(coil_heating_dx_single_speed, necb_reference_hp = false)
  # Set Rated heating capacity = 50% cooling coil capacity at -8.3 C outdoor [8.4.4.13 (2)(c)]

  if necb_reference_hp #NECB reference heat pump rules apply
    # grab paired cooling coil
    if coil_heating_dx_single_speed.airLoopHVAC.empty?

      if coil_heating_dx_single_speed.containingHVACComponent.is_initialized

        containing_comp = coil_heating_dx_single_speed.containingHVACComponent.get
        if containing_comp.to_AirLoopHVACUnitaryHeatPumpAirToAir.is_initialized
          clg_coil = containing_comp.to_AirLoopHVACUnitaryHeatPumpAirToAir.get.coolingCoil
        elsif containing_comp.to_AirLoopHVACUnitarySystem.is_initialized
          unitary = containing_comp.to_AirLoopHVACUnitarySystem.get
          if unitary.coolingCoil.is_initialized
            clg_coil = unitary.coolingCoil.get
          end
        end
        # @todo Add other unitary systems
      elsif coil_heating_dx_single_speed.containingZoneHVACComponent.is_initialized
        containing_comp = coil_heating_dx_single_speed.containingZoneHVACComponent.get
        # PTHP
        if containing_comp.to_ZoneHVACPackagedTerminalHeatPump.is_initialized
          pthp = containing_comp.to_ZoneHVACPackagedTerminalHeatPump.get
          clg_coil = containing_comp.to_ZoneHVACPackagedTerminalHeatPump.get.coolingCoil
        end
      end
    elsif coil_heating_dx_single_speed.airLoopHVAC.is_initialized
      air_loop = coil_heating_dx_single_speed.airLoopHVAC.get
      # Check for the presence of any other type of cooling coil
      clg_types = ['OS:Coil:Cooling:DX:SingleSpeed',
                  'OS:Coil:Cooling:DX:TwoSpeed',
                  'OS:Coil:Cooling:DX:MultiSpeed']
      clg_types.each do |ct|
        coils = air_loop.supplyComponents(ct.to_IddObjectType)
        next if coils.empty?
        clg_coil = coils[0]
        puts "coils = air_loop.supplyComponents(ct.to_IddObjectType) #{}"
        break # Stop on first DX cooling coil found
      end
    end

    # Paired cooling coil parameters
    clg_coil = clg_coil.to_CoilCoolingDXSingleSpeed.get
    capacity_w = coil_cooling_dx_single_speed_find_capacity(clg_coil)
    indoor_wb = 19.4 #rated indoor wb
    outdoor_db = -8.3 # outdoor db

    # heating capacity = capacity factor (function of temp) from biquadratic curve
    # with curve limits on minimum y/outdoor db (no extrapolation)
    cooling_cap_f_temp_curve = clg_coil.totalCoolingCapacityFunctionOfTemperatureCurve
    cooling_cap_f_temp_factor_min_y = cooling_cap_f_temp_curve.evaluate(indoor_wb,outdoor_db)
    htg_cap_w_min_y = capacity_w*0.5*cooling_cap_f_temp_factor_min_y

    # heating capacity = capacity factor (function of temp) from biquadratic curve
    # without curve limits on minimum y/outdoor db (extrapolate)
    cooling_cap_f_temp_const = 0.867905
    cooling_cap_f_temp_x = 0.0142459
    cooling_cap_f_temp_x2 = 0.00055436
    cooling_cap_f_temp_y = -0.0075575
    cooling_cap_f_temp_y2 = 3.3e-05
    cooling_cap_f_temp_xy = -0.0001918
    cooling_cap_f_temp_factor_no_min_y = cooling_cap_f_temp_const + cooling_cap_f_temp_x*indoor_wb + cooling_cap_f_temp_x2*indoor_wb**2 +
    cooling_cap_f_temp_y*outdoor_db + cooling_cap_f_temp_y2*outdoor_db**2 + cooling_cap_f_temp_xy*indoor_wb*outdoor_db
    htg_cap_w_no_min_y = capacity_w*0.5*cooling_cap_f_temp_factor_no_min_y

    puts "capacity_w #{capacity_w}"
    puts "cooling_cap_f_temp_factor_no_min_y #{cooling_cap_f_temp_factor_no_min_y}"
    puts "cooling_cap_f_temp_factor_min_y #{cooling_cap_f_temp_factor_min_y}"
    puts "htg_cap_w_no_min_y #{htg_cap_w_no_min_y}"
    puts "htg_cap_w_min_y #{htg_cap_w_min_y}"

    # use actual factor from -8.3 to compute rated heating capacity unless it's < 0
    if cooling_cap_f_temp_factor_no_min_y>0
      htg_cap_w = htg_cap_w_no_min_y
    else
      htg_cap_w = htg_cap_w_min_y
    end

    # Hardsize rated capacity of heating coil
    coil_heating_dx_single_speed.setRatedTotalHeatingCapacity(htg_cap_w)

    return htg_cap_w
  else # Do not follow NECB reference HP rule; proceed as usual
    return super(coil_heating_dx_single_speed)
  end
end

#coil_heating_gas_apply_efficiency_and_curves(coil_heating_gas) ⇒ Boolean

Applies the standard efficiency ratings and typical performance curves to this object.

Returns:

  • (Boolean)

    true if successful, false if not



851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 851

def coil_heating_gas_apply_efficiency_and_curves(coil_heating_gas)
  successfully_set_all_properties = true

  # Define the search criteria
  search_criteria = coil_heating_gas_find_search_criteria

  # Get the coil capacity
  capacity_w = coil_heating_gas_find_capacity(coil_heating_gas)
  capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get

  # lookup properties
  coil_table = @standards_data['furnaces']
  coil_props = model_find_object(coil_table, search_criteria, [capacity_btu_per_hr, 0.001].max, Date.today)

  # Check to make sure properties were found
  if coil_props.nil?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGas', "For #{coil_heating_gas.name}, cannot find efficiency info using #{search_criteria}, cannot apply efficiency standard.")
    successfully_set_all_properties = false
  end

  # Make the plf vs plr curve
  plffplr_curve = model_add_curve(coil_heating_gas.model, coil_props['efffplr'])
  if plffplr_curve
    coil_heating_gas.setPartLoadFractionCorrelationCurve(plffplr_curve)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGas', "For #{coil_heating_gas.name}, cannot find plffplr curve, will not be set.")
    successfully_set_all_properties = false
  end

  # Thermal efficiency
  thermal_eff = coil_heating_gas_standard_minimum_thermal_efficiency(coil_heating_gas)

  # Set the efficiency values
  coil_heating_gas.setGasBurnerEfficiency(thermal_eff.to_f)

  return successfully_set_all_properties
end

#coil_heating_gas_find_capacity(coil_heating_gas) ⇒ Hash

find furnace capacity

Returns:

  • (Hash)

    used for standards_lookup_table(model)



769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 769

def coil_heating_gas_find_capacity(coil_heating_gas)
  # Get the coil capacity
  capacity_w = nil
  if coil_heating_gas.nominalCapacity.is_initialized
    capacity_w = coil_heating_gas.nominalCapacity.get
  elsif coil_heating_gas.autosizedNominalCapacity.is_initialized
    capacity_w = coil_heating_gas.autosizedNominalCapacity.get
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGas', "For #{coil_heating_gas.name} capacity is not available, cannot apply efficiency standard.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  return capacity_w
end

#coil_heating_gas_find_search_criteriaHash

find search criteria

Returns:

  • (Hash)

    used for standards_lookup_table(model)



756
757
758
759
760
761
762
763
764
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 756

def coil_heating_gas_find_search_criteria
  # Define the criteria to find the furnace properties
  # in the hvac standards data set.
  search_criteria = {}
  search_criteria['fluid_type'] = 'Air'
  search_criteria['fuel_type'] = 'Gas'

  return search_criteria
end

#coil_heating_gas_multi_stage_apply_efficiency_and_curves(coil_heating_gas_multi_stage) ⇒ Boolean

Applies the standard efficiency ratings and typical performance curves to this object.

Returns:

  • (Boolean)

    true if successful, false if not



1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1140

def coil_heating_gas_multi_stage_apply_efficiency_and_curves(coil_heating_gas_multi_stage)
  successfully_set_all_properties = true
  model = coil_heating_gas_multi_stage.model

  # get multi speed heat pump and air loop
  multi_speed_heat_pump = nil
  multi_speed_heat_pumps = model.getAirLoopHVACUnitaryHeatPumpAirToAirMultiSpeeds
  multi_speed_heat_pumps.sort.each do |iheat_pump|
    htg_coil = iheat_pump.heatingCoil
    if htg_coil.name.to_s.strip == coil_heating_gas_multi_stage.name.to_s.strip
      multi_speed_heat_pump = iheat_pump
      break
    end
  end
  airloop = multi_speed_heat_pump.airLoopHVAC.get

  # Define the criteria to find the properties in the hvac standards data set.
  search_criteria = coil_heating_gas_multi_stage_find_search_criteria(coil_heating_gas_multi_stage)
  fuel_type = search_criteria['fuel_type']
  capacity_w = coil_heating_gas_multi_stage_find_capacity(coil_heating_gas_multi_stage)

  # Find system design outside air flow rate and fraction
  controller_oa = nil
  if airloop.airLoopHVACOutdoorAirSystem.is_initialized
    oa_system = airloop.airLoopHVACOutdoorAirSystem.get
    controller_oa = oa_system.getControllerOutdoorAir
  end
  min_oa_flow_rate = 0.0
  oaf = 0.0
  if controller_oa
    min_oa_flow_rate = nil
    if controller_oa.minimumOutdoorAirFlowRate.is_initialized
      min_oa_flow_rate = controller_oa.minimumOutdoorAirFlowRate.get
    elsif controller_oa.autosizedMinimumOutdoorAirFlowRate.is_initialized
      min_oa_flow_rate = controller_oa.autosizedMinimumOutdoorAirFlowRate.get
    end
    if min_oa_flow_rate then oaf = min_oa_flow_rate.to_f / airloop.autosizedDesignSupplyAirFlowRate.to_f end
  end

  # Find capacities of each of the stages and the total number of stages required based on NECB rules.
  # This implementation is limited to 4 stages. The capacity of stages 1-3 is set to 66 kW as stipulated
  # by NECB. The capacity of the 4th stage can exceed 66 kW up to the design capacity.
  htg_stages = coil_heating_gas_multi_stage.stages
  num_stages = (capacity_w / (66.0 * 1000.0) + 0.5).round
  max_cap = 66.0 * 1000.0 * num_stages
  stage_cap = []
  final_num_stages = num_stages
  if capacity_w == 0.001
    final_num_stages = 1
    stage_cap[0] = capacity_w
  else
    case num_stages
    when 1
      stage_cap[0] = capacity_w / 2.0
      stage_cap[1] = 2.0 * stage_cap[0]
      final_num_stages = 2
    else
      stage_cap[0] = 66.0 * 1000.0
      stage_cap[1] = 2.0 * stage_cap[0]
      case num_stages
      when 2
      when 3
        stage_cap[2] = 3.0 * stage_cap[0]
      else
        final_num_stages = 4
        stage_cap[2] = 3.0 * stage_cap[0]
        stage_cap[3] = max_cap
      end
    end
  end

  # Set final number of stages and create missing stages if needed
  for istage in 1..final_num_stages - 1
    new_htg_stage = OpenStudio::Model::CoilHeatingGasMultiStageStageData.new(model)
    coil_heating_gas_multi_stage.addStage(new_htg_stage)
  end
  multi_speed_heat_pump.setNumberofSpeedsforHeating(final_num_stages)

  # Set final capacities for each of the stages. The air flow rate for each of the stages
  # is maintained above the outside air flow rate
  coil_heating_gas_multi_stage.stages[0].setNominalCapacity(stage_cap[0])
  case coil_heating_gas_multi_stage.stages.size
  when 2
    coil_heating_gas_multi_stage.stages[1].setNominalCapacity(stage_cap[1])
    if oaf > 0.5 then multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate) end
  when 3
    coil_heating_gas_multi_stage.stages[1].setNominalCapacity(stage_cap[1])
    coil_heating_gas_multi_stage.stages[2].setNominalCapacity(stage_cap[2])
    if (oaf > 0.333) && (oaf <= 0.666)
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
    elsif oaf > 0.666
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed2SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
    end
  when 4
    coil_heating_gas_multi_stage.stages[1].setNominalCapacity(stage_cap[1])
    coil_heating_gas_multi_stage.stages[2].setNominalCapacity(stage_cap[2])
    coil_heating_gas_multi_stage.stages[3].setNominalCapacity(stage_cap[3])
    if (oaf > 0.25) && (oaf <= 0.5)
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
    elsif (oaf > 0.5) && (oaf <= 0.75)
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed2SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
    elsif oaf > 0.75
      multi_speed_heat_pump.setSpeed1SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed2SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
      multi_speed_heat_pump.setSpeed3SupplyAirFlowRateDuringHeatingOperation(min_oa_flow_rate)
    end
  end

  # Convert capacity to Btu/hr
  capacity_btu_per_hr = OpenStudio.convert(stage_cap.last, 'W', 'Btu/hr').get
  capacity_kbtu_per_hr = OpenStudio.convert(stage_cap.last, 'W', 'kBtu/hr').get

  # Lookup efficiencies
  heater_props = nil
  heater_props = model_find_object(standards_data['furnaces'], search_criteria, capacity_btu_per_hr, Date.today)

  # Check to make sure properties were found
  if heater_props.nil?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGasMultiSpeed', "For #{coil_heating_gas_multi_stage.name}, cannot find efficiency info, cannot apply efficiency standard.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  # Make the EFFPLR curve
  efffplr = model_add_curve(coil_heating_gas_multi_stage.model, heater_props['efffplr'])
  if efffplr
    coil_heating_gas_multi_stage.setPartLoadFractionCorrelationCurve(efffplr)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGasMultiStage', "For #{coil_heating_gas_multi_stage.name}, cannot find efffplr curve, will not be set.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  # Get the minimum efficiency standards
  thermal_eff = nil

  # If specified as AFUE
  unless heater_props['minimum_annual_fuel_utilization_efficiency'].nil?
    min_afue = heater_props['minimum_annual_fuel_utilization_efficiency']
    thermal_eff = afue_to_thermal_eff(min_afue)
    new_comp_name = "#{coil_heating_gas_multi_stage.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{min_afue} AFUE"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CoilHeatingGasMultiStage', "For #{template}: #{coil_heating_gas_multi_stage.name}: #{fuel_type} Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; AFUE = #{min_afue}")
  end

  # If specified as thermal efficiency
  unless heater_props['minimum_thermal_efficiency'].nil?
    thermal_eff = heater_props['minimum_thermal_efficiency']
    new_comp_name = "#{coil_heating_gas_multi_stage.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{thermal_eff} Thermal Eff"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CoilHeatingGasMultiStage', "For #{template}: #{coil_heating_gas_multi_stage.name}: #{fuel_type} Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; Thermal Efficiency = #{thermal_eff}")
  end

  # If specified as combustion efficiency
  unless heater_props['minimum_combustion_efficiency'].nil?
    min_comb_eff = heater_props['minimum_combustion_efficiency']
    thermal_eff = combustion_eff_to_thermal_eff(min_comb_eff)
    new_comp_name = "#{coil_heating_gas_multi_stage.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{min_comb_eff} Combustion Eff"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.BoilerHotWater', "For #{template}: #{coil_heating_gas_multi_stage.name}: #{fuel_type} Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; Combustion Efficiency = #{min_comb_eff}")
  end
  coil_heating_gas_multi_stage.setName(new_comp_name)

  # Set the name
  coil_heating_gas_multi_stage.setName(new_comp_name)

  # Get heating stages
  htg_stages = coil_heating_gas_multi_stage.stages

  # Set the efficiency values
  unless thermal_eff.nil?
    htg_stages.sort.each do |stage|
      stage.setGasBurnerEfficiency(thermal_eff)
    end
  end

  return successfully_set_all_properties
end

#coil_heating_gas_standard_minimum_thermal_efficiency(coil_heating_gas, rename = false) ⇒ Double

Finds lookup object in standards and return minimum thermal efficiency

Returns:

  • (Double)

    minimum thermal efficiency



788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 788

def coil_heating_gas_standard_minimum_thermal_efficiency(coil_heating_gas, rename = false)
  # Get the coil properties
  search_criteria = coil_heating_gas_find_search_criteria
  capacity_w = coil_heating_gas_find_capacity(coil_heating_gas)
  capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
  capacity_kbtu_per_hr = OpenStudio.convert(capacity_w, 'W', 'kBtu/hr').get

  # Get the minimum efficiency standards
  thermal_eff = nil

  # Get the coil properties
  coil_table = @standards_data['furnaces']
  coil_props = model_find_object(coil_table, search_criteria, [capacity_btu_per_hr, 0.001].max)

  unless coil_props
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGas', "For #{coil_heating_gas.name}, cannot find coil props, cannot apply efficiency standard.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  # New name initial value
  new_comp_name = coil_heating_gas.name

  # If specified as AFUE
  unless coil_props['minimum_annual_fuel_utilization_efficiency'].nil?
    min_afue = coil_props['minimum_annual_fuel_utilization_efficiency']
    thermal_eff = afue_to_thermal_eff(min_afue)
    new_comp_name = "#{coil_heating_gas.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{min_afue} AFUE"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CoilHeatingGas', "For #{template}: #{coil_heating_gas.name}: Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; AFUE = #{min_afue}")
  end

  # If specified as thermal efficiency
  unless coil_props['minimum_thermal_efficiency'].nil?
    thermal_eff = coil_props['minimum_thermal_efficiency']
    new_comp_name = "#{coil_heating_gas.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{thermal_eff} Thermal Eff"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CoilHeatingGas', "For #{template}: #{coil_heating_gas.name}: Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; Thermal Efficiency = #{thermal_eff}")
  end

  # If specified as combustion efficiency
  unless coil_props['minimum_combustion_efficiency'].nil?
    min_comb_eff = coil_props['minimum_combustion_efficiency']
    thermal_eff = combustion_eff_to_thermal_eff(min_comb_eff)
    new_comp_name = "#{coil_heating_gas.name} #{capacity_kbtu_per_hr.round}kBtu/hr #{min_comb_eff} Combustion Eff"
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CoilHeatingGas', "For #{template}: #{coil_heating_gas.name}: Capacity = #{capacity_kbtu_per_hr.round}kBtu/hr; Combustion Efficiency = #{min_comb_eff}")
  end

  unless thermal_eff
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CoilHeatingGas', "For #{CoilHeatingGas.name}, cannot find coil efficiency, cannot apply efficiency standard.")
    successfully_set_all_properties = false
    return successfully_set_all_properties
  end

  # Rename
  if rename
    coil_heating_gas.setName(new_comp_name)
  end

  return thermal_eff
end

#common_air_loop(model:, system_data:) ⇒ Object



2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2104

def common_air_loop(model:, system_data:)
  mau_air_loop = OpenStudio::Model::AirLoopHVAC.new(model)
  mau_air_loop.setName(system_data[:name])
  air_loop_sizing = mau_air_loop.sizingSystem
  air_loop_sizing.autosizeDesignOutdoorAirFlowRate
  air_loop_sizing.setPreheatDesignTemperature(system_data[:PreheatDesignTemperature]) unless system_data[:PreheatDesignTemperature].nil?
  air_loop_sizing.setPreheatDesignHumidityRatio(system_data[:PreheatDesignHumidityRatio]) unless system_data[:PreheatDesignHumidityRatio].nil?
  air_loop_sizing.setPrecoolDesignTemperature(system_data[:PrecoolDesignTemperature]) unless system_data[:PrecoolDesignTemperature].nil?
  air_loop_sizing.setPrecoolDesignHumidityRatio(system_data[:PrecoolDesignHumidityRatio]) unless system_data[:PrecoolDesignHumidityRatio].nil?
  air_loop_sizing.setSizingOption(system_data[:SizingOption]) unless system_data[:SizingOption].nil?
  air_loop_sizing.setCoolingDesignAirFlowMethod(system_data[:CoolingDesignAirFlowMethod]) unless system_data[:CoolingDesignAirFlowMethod].nil?
  air_loop_sizing.setCoolingDesignAirFlowRate(system_data[:CoolingDesignAirFlowRate]) unless system_data[:CoolingDesignAirFlowRate].nil?
  air_loop_sizing.setHeatingDesignAirFlowMethod(system_data[:HeatingDesignAirFlowMethod]) unless system_data[:HeatingDesignAirFlowMethod].nil?
  air_loop_sizing.setHeatingDesignAirFlowRate(system_data[:HeatingDesignAirFlowRate]) unless system_data[:HeatingDesignAirFlowRate].nil?
  air_loop_sizing.setSystemOutdoorAirMethod(system_data[:SystemOutdoorAirMethod]) unless system_data[:SystemOutdoorAirMethod].nil?
  air_loop_sizing.setCentralCoolingDesignSupplyAirHumidityRatio(system_data[:CentralCoolingDesignSupplyAirHumidityRatio]) unless system_data[:CentralCoolingDesignSupplyAirHumidityRatio].nil?
  air_loop_sizing.setCentralHeatingDesignSupplyAirHumidityRatio(system_data[:CentralHeatingDesignSupplyAirHumidityRatio]) unless system_data[:CentralHeatingDesignSupplyAirHumidityRatio].nil?
  air_loop_sizing.setTypeofLoadtoSizeOn(system_data[:TypeofLoadtoSizeOn]) unless system_data[:TypeofLoadtoSizeOn].nil?
  air_loop_sizing.setCentralCoolingDesignSupplyAirTemperature(system_data[:CentralCoolingDesignSupplyAirTemperature]) unless system_data[:CentralCoolingDesignSupplyAirTemperature].nil?
  air_loop_sizing.setCentralHeatingDesignSupplyAirTemperature(system_data[:CentralHeatingDesignSupplyAirTemperature]) unless system_data[:CentralHeatingDesignSupplyAirTemperature].nil?
  air_loop_sizing.setAllOutdoorAirinCooling(system_data[:AllOutdoorAirinCooling]) unless system_data[:AllOutdoorAirinCooling].nil?
  air_loop_sizing.setAllOutdoorAirinHeating(system_data[:AllOutdoorAirinHeating]) unless system_data[:AllOutdoorAirinHeating].nil?
  if model.version < OpenStudio::VersionString.new('2.7.0')
    air_loop_sizing.setMinimumSystemAirFlowRatio(system_data[:MinimumSystemAirFlowRatio]) unless system_data[:MinimumSystemAirFlowRatio].nil?
  else
    air_loop_sizing.setCentralHeatingMaximumSystemAirFlowRatio(system_data[:MinimumSystemAirFlowRatio]) unless system_data[:MinimumSystemAirFlowRatio].nil?
  end
  return mau_air_loop
end

#convert_arg_to_bool(variable:, default:) ⇒ Object

This method converts arguments to bool. Anything other than a bool false or string ‘false’ is converted to a bool true. Bool false and case insesitive string false are turned into bool false.



28
29
30
31
32
33
34
35
36
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 28

def convert_arg_to_bool(variable:, default:)
  return true if variable.nil?
  if variable.is_a? String
    return true if variable.to_s.downcase == 'necb_default'
    return false if variable.to_s.downcase == 'false'
  end
  return false if variable == false
  return true
end

#convert_arg_to_f(variable:, default:) ⇒ Object

This is a helper method to convert arguments that may support ‘NECB_Default, and nils to convert to float’



17
18
19
20
21
22
23
24
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 17

def convert_arg_to_f(variable:, default:)
  return variable if variable.kind_of?(Numeric)
  return default if variable.nil? || (variable == 'NECB_Default')
  return unless variable.kind_of?(String)

  variable = variable.strip
  return variable.to_f
end

#corrupt_standards_databaseObject

2019-05-23 ckirney This is an ugly, disgusting, hack (hence the name) that I dreamed out so that we could quickly and easily finish the merge from the nrcan branch (using OS 2.6.0) to master (using OS 2.8.0). This must be revised and a more elegant solution found.

This method takes everything in the @standards_data hash and adds it to the main @standards_data hash. This was done because other contributors insist on using the ‘model_find_object’ method which is passed a hash and some search criteria. The ‘model_find_objects’ then looks through the hash to information matching the search criteria. NECB standards assumes that the ‘standards_lookup_table_first’ method is used. This does basically the some thing as ‘model_find_objects’ only it assumes that you are looking in the standards hash and you tell it which table in the standards hash to look for.



1297
1298
1299
1300
1301
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1297

def corrupt_standards_database
  @standards_data['tables'].each do |table|
    @standards_data[table[0]] = table[1]['table']
  end
end

#create_base_data(model) ⇒ Object

Generates the base data hash mainly used to perform qaqc.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 183

def create_base_data(model)
  # construct command with local libs
  os_version = OpenStudio.openStudioLongVersion
  eplus_version = OpenStudio.energyPlusVersion
  puts "\n\n\nOS_version is [#{os_version.strip}]"
  puts "\n\n\nEP_version is [#{eplus_version.strip}]"

  # Ensure all surfaces are unique.
  surfaces = model.getSurfaces.sort

  # Sort surfaces by type

  interior_surfaces = BTAP::Geometry::Surfaces.filter_by_boundary_condition(surfaces, ['Surface', 'Adiabatic'])
  interior_floors = BTAP::Geometry::Surfaces.filter_by_surface_types(interior_surfaces, 'Floor')
  outdoor_surfaces = BTAP::Geometry::Surfaces.filter_by_boundary_condition(surfaces, 'Outdoors')
  outdoor_walls = BTAP::Geometry::Surfaces.filter_by_surface_types(outdoor_surfaces, 'Wall')
  outdoor_roofs = BTAP::Geometry::Surfaces.filter_by_surface_types(outdoor_surfaces, 'RoofCeiling')
  outdoor_floors = BTAP::Geometry::Surfaces.filter_by_surface_types(outdoor_surfaces, 'Floor')
  outdoor_subsurfaces = outdoor_surfaces.flat_map(&:subSurfaces)

  ground_surfaces = BTAP::Geometry::Surfaces.filter_by_boundary_condition(surfaces, ['Ground', 'Foundation'])
  ground_walls = BTAP::Geometry::Surfaces.filter_by_surface_types(ground_surfaces, 'Wall')
  ground_roofs = BTAP::Geometry::Surfaces.filter_by_surface_types(ground_surfaces, 'RoofCeiling')
  ground_floors = BTAP::Geometry::Surfaces.filter_by_surface_types(ground_surfaces, 'Floor')

  windows = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(outdoor_subsurfaces, ['FixedWindow', 'OperableWindow'])
  skylights = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(outdoor_subsurfaces, ['Skylight', 'TubularDaylightDiffuser', 'TubularDaylightDome'])
  doors = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(outdoor_subsurfaces, ['Door', 'GlassDoor'])
  overhead_doors = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(outdoor_subsurfaces, ['OverheadDoor'])

  # Peaks
  electric_peak = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters'" \
                                                                     " AND ReportForString='Entire Facility' AND TableName='Annual and Peak Values - Electricity' AND RowName='Electricity:Facility'" \
                                                                     " AND ColumnName='Electricity Maximum Value' AND Units='W'")
  natural_gas_peak = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters'" \
                                                                        " AND ReportForString='Entire Facility' AND TableName='Annual and Peak Values - Natural Gas' AND RowName='NaturalGas:Facility'" \
                                                                        " AND ColumnName='Natural Gas Maximum Value' AND Units='W'")

  get_sql_tables_to_json(model)

  # Create hash to store all the collected data.
  qaqc = {}
  qaqc[:sql_data] = get_sql_tables_to_json(model)
  error_warning = []
  # Store Building data.
  qaqc[:building] = {}
  qaqc[:building][:name] = model.building.get.name.get
  qaqc[:building][:conditioned_floor_area_m2] = nil
  if model.building.get.conditionedFloorArea.empty?
    error_warning << "model.building.get.conditionedFloorArea() is empty for #{model.building.get.name.get}"
  else
    qaqc[:building][:conditioned_floor_area_m2] = model.building.get.conditionedFloorArea.get
  end
  qaqc[:building][:exterior_area_m2] = model.building.get.exteriorSurfaceArea # m2
  qaqc[:building][:volume] = model.building.get.airVolume # m3
  qaqc[:building][:number_of_stories] = model.getBuildingStorys.size
  qaqc[:building][:standards_number_of_stories] = nil
  qaqc[:building][:standards_number_of_stories] = model.building.get.standardsNumberOfStories.get unless model.building.get.standardsNumberOfStories.empty?
  qaqc[:building][:standards_number_of_above_ground_stories] = nil
  qaqc[:building][:standards_number_of_above_ground_stories] = model.building.get.standardsNumberOfAboveGroundStories.get unless model.building.get.standardsNumberOfAboveGroundStories.empty?
  qaqc[:building][:standards_number_of_living_units] = nil
  qaqc[:building][:standards_number_of_living_units] = model.building.get.standardsNumberOfLivingUnits.get unless model.building.get.standardsNumberOfLivingUnits.empty?
  qaqc[:building][:nominal_floor_to_ceiling_height] = nil
  qaqc[:building][:nominal_floor_to_ceiling_height] = model.building.get.nominalFloortoCeilingHeight.get unless model.building.get.nominalFloortoCeilingHeight.empty?
  qaqc[:building][:nominal_floor_to_floor_height] = nil
  qaqc[:building][:nominal_floor_to_floor_height] = model.building.get.nominalFloortoFloorHeight.get unless model.building.get.nominalFloortoFloorHeight.empty?

  # Store Geography Data
  qaqc[:geography] = {}
  qaqc[:geography][:hdd_necb] = get_necb_hdd18(model: model, necb_hdd: true)
  qaqc[:geography][:hdd] = get_necb_hdd18(model: model, necb_hdd: false)
  weather_file_path = model.weatherFile.get.path.get.to_s
  stat_file_path = weather_file_path.gsub('.epw', '.stat')
  stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
  qaqc[:geography][:cdd] = stat_file.cdd18
  qaqc[:geography][:climate_zone] = NECB2011.new.get_climate_zone_name(qaqc[:geography][:hdd])
  qaqc[:geography][:city] = model.getWeatherFile.city
  qaqc[:geography][:state_province_region] = model.getWeatherFile.stateProvinceRegion
  qaqc[:geography][:country] = model.getWeatherFile.country
  qaqc[:geography][:latitude] = model.getWeatherFile.latitude
  qaqc[:geography][:longitude] = model.getWeatherFile.longitude

  # Spacetype Breakdown
  qaqc[:spacetype_area_breakdown] = {}
  model.getSpaceTypes.sort.each do |spaceType|
    next if spaceType.floorArea == 0

    # data for space type breakdown
    display = spaceType.name.get
    floor_area_si = 0
    # loop through spaces so I can skip if not included in floor area
    spaceType.spaces.sort.each do |space|
      next if !space.partofTotalFloorArea

      floor_area_si += space.floorArea * space.multiplier
    end
    qaqc[:spacetype_area_breakdown][spaceType.name.get.gsub(/\s+/, '_').downcase.to_sym] = floor_area_si
  end

  # Economics Section
  qaqc[:economics] = {}
  provinces_names_map = { 'QC' => 'Quebec', 'NL' => 'Newfoundland and Labrador', 'NS' => 'Nova Scotia', 'PE' => 'Prince Edward Island', 'ON' => 'Ontario', 'MB' => 'Manitoba', 'SK' => 'Saskatchewan', 'AB' => 'Alberta', 'BC' => 'British Columbia', 'YT' => 'Yukon', 'NT' => 'Northwest Territories', 'NB' => 'New Brunswick', 'NU' => 'Nunavut' }
  neb_prices_csv_file_name = "#{File.dirname(__FILE__)}/qaqc_resources/neb_end_use_prices.csv"
  puts neb_prices_csv_file_name
  building_type = 'Commercial'
  province = provinces_names_map[qaqc[:geography][:state_province_region]]
  neb_fuel_list = ['Electricity', 'Natural Gas', 'Oil']
  neb_eplus_fuel_map = { 'Electricity' => 'Electricity', 'Natural Gas' => 'Gas', 'Oil' => 'FuelOilNo2' }
  qaqc[:economics][:total_neb_cost] = 0.0
  qaqc[:economics][:total_neb_cost_per_m2] = 0.0
  neb_eplus_fuel_map.each do |neb_fuel, ep_fuel|
    search_info = {
      0 => building_type,
      1 => province,
      2 => neb_fuel
    }
    row = look_up_csv_data(neb_prices_csv_file_name, search_info)
    neb_fuel_cost = row['2020']
    fuel_consumption_gj = 0.0
    if neb_fuel == 'Electricity' || neb_fuel == 'Natural Gas'
      if model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters' AND ReportForString='Entire Facility' AND
      TableName='Annual and Peak Values - #{ep_fuel}' AND RowName='#{ep_fuel}:Facility' AND ColumnName='#{ep_fuel} Annual Value' AND Units='GJ'").is_initialized
        fuel_consumption_gj = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters' AND ReportForString='Entire Facility' AND
      TableName='Annual and Peak Values - #{ep_fuel}' AND RowName='#{ep_fuel}:Facility' AND ColumnName='#{ep_fuel} Annual Value' AND Units='GJ'").get
      end
    else
      if model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters' AND ReportForString='Entire Facility' AND
      TableName='Annual and Peak Values - Other' AND RowName='#{ep_fuel}:Facility' AND ColumnName='Annual Value' AND Units='GJ'").is_initialized
        fuel_consumption_gj = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters' AND ReportForString='Entire Facility' AND
      TableName='Annual and Peak Values - Other' AND RowName='#{ep_fuel}:Facility' AND ColumnName='Annual Value' AND Units='GJ'").get
      end
    end
    qaqc[:economics][:"#{neb_fuel}_neb_cost"] = fuel_consumption_gj * neb_fuel_cost.to_f
    qaqc[:economics][:"#{neb_fuel}_neb_cost_per_m2"] = qaqc[:economics][:"#{neb_fuel}_neb_cost"] / qaqc[:building][:conditioned_floor_area_m2] unless model.building.get.conditionedFloorArea.empty?
    qaqc[:economics][:total_neb_cost] += qaqc[:economics][:"#{neb_fuel}_neb_cost"]
    qaqc[:economics][:total_neb_cost_per_m2] += qaqc[:economics][:"#{neb_fuel}_neb_cost_per_m2"] || 0.0
  end

  # Fuel cost based local utility rates
  costing_rownames = model.sqlFile.get.execAndReturnVectorOfString("SELECT RowName FROM TabularDataWithStrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-7. Energy Cost Summary' AND ColumnName='Total Energy Cost'")
  #==> ["Electricity", "Natural Gas", "Additional", "Total"]
  costing_rownames = validate_optional(costing_rownames, model, 'N/A')
  if costing_rownames != 'N/A'
    costing_rownames.each do |rowname|
      case rowname
        when 'Electricity'
          qaqc[:economics][:electricity_cost] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-7. Energy Cost Summary' AND ColumnName='Total Energy Cost' AND RowName='#{rowname}'").get
          qaqc[:economics][:electricity_cost_per_m2] = qaqc[:economics][:electricity_cost] / qaqc[:building][:conditioned_floor_area_m2] unless model.building.get.conditionedFloorArea.empty?
        when 'Natural Gas'
          qaqc[:economics][:natural_gas_cost] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-7. Energy Cost Summary' AND ColumnName='Total Energy Cost' AND RowName='#{rowname}'").get
          qaqc[:economics][:natural_gas_cost_per_m2] = qaqc[:economics][:natural_gas_cost] / qaqc[:building][:conditioned_floor_area_m2] unless model.building.get.conditionedFloorArea.empty?

        when 'Additional'
          qaqc[:economics][:additional_cost] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-7. Energy Cost Summary' AND ColumnName='Total Energy Cost' AND RowName='#{rowname}'").get
          qaqc[:economics][:additional_cost_per_m2] = qaqc[:economics][:additional_cost] / qaqc[:building][:conditioned_floor_area_m2] unless model.building.get.conditionedFloorArea.empty?

        when 'Total'
          qaqc[:economics][:total_cost] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-7. Energy Cost Summary' AND ColumnName='Total Energy Cost' AND RowName='#{rowname}'").get
          qaqc[:economics][:total_cost_per_m2] = qaqc[:economics][:total_cost] / qaqc[:building][:conditioned_floor_area_m2] unless model.building.get.conditionedFloorArea.empty?
      end
    end
  else
    error_warning << "costing is unavailable because the sql statement is nil RowName FROM TabularDataWithStrings WHERE ReportName='LEEDsummary' AND ReportForString='Entire Facility' AND TableName='EAp2-7. Energy Cost Summary' AND ColumnName='Total Energy Cost'"
  end

  # Store end_use data
  end_uses = [
    'Heating',
    'Cooling',
    'Interior Lighting',
    'Exterior Lighting',
    'Interior Equipment',
    'Exterior Equipment',
    'Fans',
    'Pumps',
    'Heat Rejection',
    'Humidification',
    'Heat Recovery',
    'Water Systems',
    'Refrigeration',
    'Generators',
    'Total End Uses'
  ]

  fuels = [
    ['Electricity', 'GJ'],
    ['Natural Gas', 'GJ'],
    ['Additional Fuel', 'GJ'],
    ['District Cooling', 'GJ'],
    ['District Heating', 'GJ']
  ]

  qaqc[:end_uses] = {}
  qaqc[:end_uses_eui] = {}
  end_uses.each do |use_type|
    qaqc[:end_uses]["#{use_type.gsub(/\s+/, '_').downcase.to_sym}_gj"] = 0
    qaqc[:end_uses_eui]["#{use_type.gsub(/\s+/, '_').downcase.to_sym}_gj_per_m2"] = 0
    fuels.each do |fuel_type|
      value = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='End Uses' AND RowName='#{use_type}' AND ColumnName='#{fuel_type[0]}' AND Units='#{fuel_type[1]}'")
      if value.empty? || (value.get == 0)
      else
        qaqc[:end_uses]["#{use_type.gsub(/\s+/, '_').downcase.to_sym}_gj"] += value.get
        unless qaqc[:building][:conditioned_floor_area_m2].nil?
          qaqc[:end_uses_eui]["#{use_type.gsub(/\s+/, '_').downcase.to_sym}_gj_per_m2"] += value.get / qaqc[:building][:conditioned_floor_area_m2]
        end
      end
    end
    value = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='End Uses' AND RowName='#{use_type}' AND ColumnName='Water' AND Units='m3'")
    if value.empty? || (value.get == 0)
    else
      qaqc[:end_uses]["#{use_type.gsub(/\s+/, '_').downcase.to_sym}_water_m3"] = value.get
      unless qaqc[:building][:conditioned_floor_area_m2].nil?
        qaqc[:end_uses_eui]["#{use_type.gsub(/\s+/, '_').downcase.to_sym}_water_m3_per_m2"] = value.get / qaqc[:building][:conditioned_floor_area_m2]
      end
    end
  end

  # Store Peak Data
  qaqc[:meter_peaks] = {}
  qaqc[:meter_peaks][:electric_w] = electric_peak.empty? ? 'NA' : electric_peak.get
  qaqc[:meter_peaks][:natural_gas_w] = natural_gas_peak.empty? ? 'NA' : natural_gas_peak.get

  # Store unmet hour data
  qaqc[:unmet_hours] = {}
  qaqc[:unmet_hours][:cooling] = model.getFacility.hoursCoolingSetpointNotMet.get unless model.getFacility.hoursCoolingSetpointNotMet.empty?
  qaqc[:unmet_hours][:heating] = model.getFacility.hoursHeatingSetpointNotMet.get unless model.getFacility.hoursHeatingSetpointNotMet.empty?

  # puts "\n\n\n#{costing_rownames}\n\n\n"
  # Padmassun's Code -- Tarrif end

  # Padmassun's Code -- Service Hotwater Heating *start*
  qaqc[:service_water_heating] = {}
  qaqc[:service_water_heating][:total_nominal_occupancy] = -1
  # qaqc[:service_water_heating][:total_nominal_occupancy]=model.sqlFile().get().execAndReturnVectorOfDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='OutdoorAirSummary' AND ReportForString='Entire Facility' AND TableName='Average Outdoor Air During Occupied Hours' AND ColumnName='Nominal Number of Occupants'").get.inject(0, :+)
  qaqc[:service_water_heating][:total_nominal_occupancy] = get_total_nominal_capacity(model)

  qaqc[:service_water_heating][:electricity_per_year] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='End Uses' AND ColumnName='Electricity' AND RowName='Water Systems'")
  qaqc[:service_water_heating][:electricity_per_year] = validate_optional(qaqc[:service_water_heating][:electricity_per_year], model, -1)

  qaqc[:service_water_heating][:electricity_per_day] = qaqc[:service_water_heating][:electricity_per_year] / 365.5
  qaqc[:service_water_heating][:electricity_per_day_per_occupant] = qaqc[:service_water_heating][:electricity_per_day] / qaqc[:service_water_heating][:total_nominal_occupancy]

  qaqc[:service_water_heating][:natural_gas_per_year] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='End Uses' AND ColumnName='Natural Gas' AND RowName='Water Systems'")
  qaqc[:service_water_heating][:natural_gas_per_year] = validate_optional(qaqc[:service_water_heating][:natural_gas_per_year], model, -1)

  qaqc[:service_water_heating][:additional_fuel_per_year] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='End Uses' AND ColumnName='Additional Fuel' AND RowName='Water Systems'")
  qaqc[:service_water_heating][:additional_fuel_per_year] = validate_optional(qaqc[:service_water_heating][:additional_fuel_per_year], model, -1)

  qaqc[:service_water_heating][:water_m3_per_year] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='AnnualBuildingUtilityPerformanceSummary' AND ReportForString='Entire Facility' AND TableName='End Uses' AND ColumnName='Water' AND RowName='Water Systems'")
  qaqc[:service_water_heating][:water_m3_per_year] = validate_optional(qaqc[:service_water_heating][:water_m3_per_year], model, -1)

  qaqc[:service_water_heating][:water_m3_per_day] = qaqc[:service_water_heating][:water_m3_per_year] / 365.5
  qaqc[:service_water_heating][:water_m3_per_day_per_occupant] = qaqc[:service_water_heating][:water_m3_per_day] / qaqc[:service_water_heating][:total_nominal_occupancy]
  # puts qaqc[:service_water_heating][:total_nominal_occupancy]
  # Padmassun's Code -- Service Hotwater Heating *end*

  # Store Envelope data.
  qaqc[:envelope] = {}
  # Get Areas
  qaqc[:envelope][:outdoor_walls_area_m2] = outdoor_walls.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }
  qaqc[:envelope][:outdoor_roofs_area_m2] = outdoor_roofs.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }
  qaqc[:envelope][:outdoor_floors_area_m2] = outdoor_floors.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }
  qaqc[:envelope][:ground_walls_area_m2] = ground_walls.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }
  qaqc[:envelope][:ground_roofs_area_m2] = ground_roofs.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }
  qaqc[:envelope][:ground_floors_area_m2] = ground_floors.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }
  qaqc[:envelope][:interior_floors_area_m2] = interior_floors.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier }

  # Subsurface areas
  qaqc[:envelope][:windows_area_m2] = windows.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier * e.multiplier }
  qaqc[:envelope][:skylights_area_m2] = skylights.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier * e.multiplier }
  qaqc[:envelope][:doors_area_m2] = doors.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier * e.multiplier }
  qaqc[:envelope][:overhead_doors_area_m2] = overhead_doors.inject(0) { |sum, e| sum + e.netArea * e.space.get.multiplier * e.multiplier }

  # Total Building Surface Area.
  qaqc[:envelope][:total_exterior_area_m2] = qaqc[:envelope][:outdoor_walls_area_m2] +
                                             qaqc[:envelope][:outdoor_roofs_area_m2] +
                                             qaqc[:envelope][:outdoor_floors_area_m2] +
                                             qaqc[:envelope][:ground_walls_area_m2] +
                                             qaqc[:envelope][:ground_roofs_area_m2] +
                                             qaqc[:envelope][:ground_floors_area_m2] +
                                             qaqc[:envelope][:windows_area_m2] +
                                             qaqc[:envelope][:skylights_area_m2] +
                                             qaqc[:envelope][:doors_area_m2] +
                                             qaqc[:envelope][:overhead_doors_area_m2]
  # Total Building Ground Surface Area.
  qaqc[:envelope][:total_ground_area_m2] = qaqc[:envelope][:ground_walls_area_m2] +
                                           qaqc[:envelope][:ground_roofs_area_m2] +
                                           qaqc[:envelope][:ground_floors_area_m2]
  # Total Building Outdoor Surface Area.
  qaqc[:envelope][:total_outdoor_area_m2] = qaqc[:envelope][:outdoor_walls_area_m2] +
                                            qaqc[:envelope][:outdoor_roofs_area_m2] +
                                            qaqc[:envelope][:outdoor_floors_area_m2] +
                                            qaqc[:envelope][:windows_area_m2] +
                                            qaqc[:envelope][:skylights_area_m2] +
                                            qaqc[:envelope][:doors_area_m2] +
                                            qaqc[:envelope][:overhead_doors_area_m2]

  # Average Conductances by surface Type
  qaqc[:envelope][:outdoor_walls_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(outdoor_walls).round(4) if !outdoor_walls.empty?
  qaqc[:envelope][:outdoor_roofs_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(outdoor_roofs).round(4) if !outdoor_roofs.empty?
  qaqc[:envelope][:outdoor_floors_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(outdoor_floors).round(4) if !outdoor_floors.empty?
  qaqc[:envelope][:ground_walls_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(ground_walls).round(4) if !ground_walls.empty?
  qaqc[:envelope][:ground_roofs_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(ground_roofs).round(4) if !ground_roofs.empty?
  qaqc[:envelope][:ground_floors_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(ground_floors).round(4) if !ground_floors.empty?
  qaqc[:envelope][:windows_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(windows).round(4) if !windows.empty?
  qaqc[:envelope][:skylights_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(skylights).round(4) if !skylights.empty?
  qaqc[:envelope][:doors_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(doors).round(4) if !doors.empty?
  qaqc[:envelope][:overhead_doors_average_conductance_w_per_m2_k] = OpenstudioStandards::Constructions.surfaces_get_conductance(overhead_doors).round(4) if !overhead_doors.empty?

  # #Average Conductances for building whole weight factors
  !outdoor_walls.empty? ? o_wall_cond_weight = qaqc[:envelope][:outdoor_walls_average_conductance_w_per_m2_k] * qaqc[:envelope][:outdoor_walls_area_m2] : o_wall_cond_weight = 0
  !outdoor_roofs.empty? ? o_roof_cond_weight = qaqc[:envelope][:outdoor_roofs_average_conductance_w_per_m2_k] * qaqc[:envelope][:outdoor_roofs_area_m2] : o_roof_cond_weight = 0
  !outdoor_floors.empty? ? o_floor_cond_weight = qaqc[:envelope][:outdoor_floors_average_conductance_w_per_m2_k] * qaqc[:envelope][:outdoor_floors_area_m2] : o_floor_cond_weight = 0
  !ground_walls.empty?   ? g_wall_cond_weight = qaqc[:envelope][:ground_walls_average_conductance_w_per_m2_k] * qaqc[:envelope][:ground_walls_area_m2] : g_wall_cond_weight = 0
  !ground_roofs.empty?   ? g_roof_cond_weight = qaqc[:envelope][:ground_roofs_average_conductance_w_per_m2_k] * qaqc[:envelope][:ground_roofs_area_m2] : g_roof_cond_weight = 0
  !ground_floors.empty?  ? g_floor_cond_weight = qaqc[:envelope][:ground_floors_average_conductance_w_per_m2_k] * qaqc[:envelope][:ground_floors_area_m2] : g_floor_cond_weight = 0
  !windows.empty?        ? win_cond_weight = qaqc[:envelope][:windows_average_conductance_w_per_m2_k] * qaqc[:envelope][:windows_area_m2] : win_cond_weight = 0
  # doors.size > 0 ? sky_cond_weight = qaqc[:envelope][:skylights_average_conductance_w_per_m2_k] * qaqc[:envelope][:skylights_area_m2] : sky_cond_weight = 0
  if !doors.empty? && !qaqc[:envelope][:skylights_average_conductance_w_per_m2_k].nil? && !qaqc[:envelope][:skylights_area_m2].nil?
    sky_cond_weight = qaqc[:envelope][:skylights_average_conductance_w_per_m2_k] * qaqc[:envelope][:skylights_area_m2]
  else
    sky_cond_weight = 0
  end
  !overhead_doors.empty? ? door_cond_weight = qaqc[:envelope][:doors_average_conductance_w_per_m2_k] * qaqc[:envelope][:doors_area_m2] : door_cond_weight = 0
  !overhead_doors.empty? ? overhead_door_cond_weight = qaqc[:envelope][:overhead_doors_average_conductance_w_per_m2_k] * qaqc[:envelope][:overhead_doors_area_m2] : overhead_door_cond_weight = 0

  # Building Average Conductance
  qaqc[:envelope][:building_outdoor_average_conductance_w_per_m2_k] = (
  o_floor_cond_weight +
      o_roof_cond_weight +
      o_wall_cond_weight +
      win_cond_weight +
      sky_cond_weight +
      door_cond_weight +
      overhead_door_cond_weight) / qaqc[:envelope][:total_outdoor_area_m2]

  # Building Average Ground Conductance
  qaqc[:envelope][:building_ground_average_conductance_w_per_m2_k] = (
  g_floor_cond_weight +
      g_roof_cond_weight +
      g_wall_cond_weight) / qaqc[:envelope][:total_ground_area_m2]

  # Building Average Conductance
  qaqc[:envelope][:building_average_conductance_w_per_m2_k] = (
  (qaqc[:envelope][:building_ground_average_conductance_w_per_m2_k] * qaqc[:envelope][:total_ground_area_m2]) +
      (qaqc[:envelope][:building_outdoor_average_conductance_w_per_m2_k] * qaqc[:envelope][:total_outdoor_area_m2])
) /
                                                              (qaqc[:envelope][:total_ground_area_m2] + qaqc[:envelope][:total_outdoor_area_m2])

  qaqc[:envelope][:fdwr] = (BTAP::Geometry.get_fwdr(model) * 100.0).round(1)
  qaqc[:envelope][:srr] = (BTAP::Geometry.get_srr(model) * 100.0).round(1)

  qaqc[:envelope][:constructions] = {}
  qaqc[:envelope][:constructions][:exterior_fenestration] = []
  constructions = []
  outdoor_subsurfaces.each { |surface| constructions << surface.construction.get }
  ext_const_base = Hash.new(0)
  constructions.each { |name| ext_const_base[name.to_Construction.get] += 1 }
  # iterate thought each construction and get store data
  ext_const_base.sort.each do |construction, count|
    construction_info = {}
    qaqc[:envelope][:constructions][:exterior_fenestration] << construction_info
    construction_info[:name] = construction.name.get
    construction_info[:net_area_m2] = construction.getNetArea.round(2)
    construction_info[:thermal_conductance_m2_w_per_k] = OpenstudioStandards::Constructions.construction_get_conductance(construction).round(3)
    construction_info[:solar_transmittance] = OpenstudioStandards::Constructions.construction_get_solar_transmittance(construction).round(3)
    construction_info[:visible_tranmittance] = OpenstudioStandards::Constructions.construction_get_visible_transmittance(construction).round(3)
  end

  # Exterior
  qaqc[:envelope][:constructions][:exterior_opaque] = []
  constructions = []
  outdoor_surfaces.each { |surface| constructions << surface.construction.get }
  ext_const_base = Hash.new(0)
  constructions.each { |name| ext_const_base[name.to_Construction.get] += 1 }
  # iterate thought each construction and get store data
  ext_const_base.sort.each do |construction, count|
    construction_info = {}
    qaqc[:envelope][:constructions][:exterior_opaque] << construction_info
    construction_info[:name] = construction.name.get
    construction_info[:net_area_m2] = construction.getNetArea.round(2)
    construction_info[:thermal_conductance_m2_w_per_k] = construction.thermalConductance.get.round(3) if construction.thermalConductance.is_initialized
    construction_info[:net_area_m2] = construction.to_Construction.get.getNetArea.round(2)
    construction_info[:solar_absorptance] = construction.to_Construction.get.layers[0].exteriorVisibleAbsorptance.get
  end

  # Ground
  qaqc[:envelope][:constructions][:ground] = []
  constructions = []
  ground_surfaces.each { |surface| constructions << surface.construction.get }
  ext_const_base = Hash.new(0)
  constructions.each { |name| ext_const_base[name.to_Construction.get] += 1 }
  # iterate thought each construction and get store data
  ext_const_base.sort.each do |construction, count|
    construction_info = {}
    qaqc[:envelope][:constructions][:ground] << construction_info
    construction_info[:name] = construction.name.get
    construction_info[:net_area_m2] = construction.getNetArea.round(2)
    construction_info[:thermal_conductance_m2_w_per_k] = construction.thermalConductance.get.round(3) if construction.thermalConductance.is_initialized
    construction_info[:net_area_m2] = construction.to_Construction.get.getNetArea.round(2)
    construction_info[:solar_absorptance] = construction.to_Construction.get.layers[0].exteriorVisibleAbsorptance.get
  end

  qaqc[:envelope][:average_thermal_conductance_m2_w_per_k] =
    # Store Space data.
    qaqc[:spaces] = []
  model.getSpaces.sort.each do |space|
    spaceinfo = {}
    qaqc[:spaces] << spaceinfo
    spaceinfo[:name] = space.name.get # name should be defined test
    spaceinfo[:multiplier] = space.multiplier
    spaceinfo[:volume] = space.volume # should be greater than zero
    spaceinfo[:exterior_wall_area] = space.exteriorWallArea # just for information.
    spaceinfo[:space_type_name] = space.spaceType.get.name.get unless space.spaceType.empty? # should have a space types name defined.
    spaceinfo[:thermal_zone] = space.thermalZone.get.name.get unless space.thermalZone.empty? # should be assigned a thermalzone name.
    # puts space.name.get
    # puts space.thermalZone.empty?
    spaceinfo[:breathing_zone_outdoor_airflow_vbz] = -1
    breathing_zone_outdoor_airflow_vbz = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='Standard62.1Summary' AND ReportForString='Entire Facility' AND TableName='Zone Ventilation Parameters' AND ColumnName='Breathing Zone Outdoor Airflow - Vbz' AND Units='m3/s' AND RowName='#{spaceinfo[:thermal_zone].to_s.upcase}' ")
    spaceinfo[:breathing_zone_outdoor_airflow_vbz] = breathing_zone_outdoor_airflow_vbz.get unless breathing_zone_outdoor_airflow_vbz.empty?
    spaceinfo[:infiltration_method] = 'N/A'
    spaceinfo[:infiltration_flow_per_m2] = -1.0
    if space.spaceInfiltrationDesignFlowRates[0].nil?
      error_warning << "space.spaceInfiltrationDesignFlowRates[0] is empty for #{spaceinfo[:name]}"
      error_warning << "space.spaceInfiltrationDesignFlowRates[0].designFlowRateCalculationMethod is empty for #{spaceinfo[:name]}"
      error_warning << "space.spaceInfiltrationDesignFlowRates[0].flowperExteriorSurfaceArea is empty for #{spaceinfo[:name]}"
    else
      spaceinfo[:infiltration_method] = space.spaceInfiltrationDesignFlowRates[0].designFlowRateCalculationMethod
      spaceinfo[:infiltration_flow_per_m2] = 'N/A'
      spaceinfo[:infiltration_flow_per_m2] = space.spaceInfiltrationDesignFlowRates[0].flowperExteriorSurfaceArea.get.round(5) unless space.spaceInfiltrationDesignFlowRates[0].flowperExteriorSurfaceArea.empty?
    end

    # the following should have values unless the spacetype is "undefined" other they should be set to the correct NECB values.
    if space.spaceType.empty?
      error_warning << "space.spaceType is empty for #{space.name.get}"
    else
      spaceinfo[:occupancy_schedule] = nil
      if space.spaceType.get.defaultScheduleSet.empty?
        error_warning << "space.spaceType.get.defaultScheduleSet is empty for #{space.name.get}"
      else
        if space.spaceType.get.defaultScheduleSet.get.numberofPeopleSchedule.empty?
          error_warning << "space.spaceType.get.defaultScheduleSet.get.numberofPeopleSchedule is empty for #{space.name.get}"
        else
          spaceinfo[:occupancy_schedule] = space.spaceType.get.defaultScheduleSet.get.numberofPeopleSchedule.get.name.get # should not empty.
        end
      end

      spaceinfo[:occ_per_m2] = space.spaceType.get.people[0].peopleDefinition.peopleperSpaceFloorArea.get.round(3) unless space.spaceType.get.people[0].nil? || space.spaceType.get.people[0].peopleDefinition.peopleperSpaceFloorArea.empty?
      if space.spaceType.get.lights[0].nil?
        error_warning << "space.spaceType.get.lights[0] is nil for Space:[#{space.name.get}] Space Type:[#{spaceinfo[:space_type_name]}]"
      else
        spaceinfo[:lighting_w_per_m2] = space.spaceType.get.lights[0].lightsDefinition.wattsperSpaceFloorArea # .get.round(3) unless space.spaceType.get.lights[0].nil?
        spaceinfo[:lighting_w_per_m2] = validate_optional(spaceinfo[:lighting_w_per_m2], model, -1.0)
        unless spaceinfo[:lighting_w_per_m2].nil?
          spaceinfo[:lighting_w_per_m2] = spaceinfo[:lighting_w_per_m2].round(3)
        end
      end
      # spaceinfo[:electric_w_per_m2] = space.spaceType.get.electricEquipment[0].electricEquipmentDefinition.wattsperSpaceFloorArea.get.round(3) unless space.spaceType.get.electricEquipment[0].nil?

      unless space.spaceType.get.electricEquipment[0].nil?
        unless space.spaceType.get.electricEquipment[0].electricEquipmentDefinition.wattsperSpaceFloorArea.empty?
          spaceinfo[:electric_w_per_m2] = space.spaceType.get.electricEquipment[0].electricEquipmentDefinition.wattsperSpaceFloorArea.get.round(3)
        end
      end
      spaceinfo[:shw_m3_per_s] = space.waterUseEquipment[0].waterUseEquipmentDefinition.peakFlowRate.round(3) unless space.waterUseEquipment[0].nil?
      spaceinfo[:waterUseEquipment] = []
      if !space.waterUseEquipment.empty?
        waterUseEquipment_info = {}
        spaceinfo[:waterUseEquipment] << waterUseEquipment_info
        waterUseEquipment_info[:peak_flow_rate] = space.waterUseEquipment[0].waterUseEquipmentDefinition.peakFlowRate
        waterUseEquipment_info[:peak_flow_rate_per_area] = waterUseEquipment_info[:peak_flow_rate] / space.floorArea
        area_per_occ = space.spaceType.get.people[0].nil? ? 0.0 : validate_optional(space.spaceType.get.people[0].spaceFloorAreaPerPerson, model, -1.0)
        #                             Watt per person =             m3/s/m3                * 1000W/kW * (specific heat * dT) * m2/person
        waterUseEquipment_info[:shw_watts_per_person] = waterUseEquipment_info[:peak_flow_rate_per_area] * 1000 * (4.19 * 44.4) * 1000 * area_per_occ
        # puts waterUseEquipment_info[:shw_watts_per_ponce the erson]
        # puts "\n\n\n"
      end
    end
  end

  # Store Thermal zone data
  qaqc[:thermal_zones] = []
  model.getThermalZones.sort.each do |zone|
    zoneinfo = {}
    qaqc[:thermal_zones] << zoneinfo
    zoneinfo[:name] = zone.name.get
    zoneinfo[:floor_area] = zone.floorArea
    zoneinfo[:multiplier] = zone.multiplier
    zoneinfo[:is_conditioned] = 'N/A'
    if zone.isConditioned.empty?
      error_warning << "zone.isConditioned is empty for #{zone.name.get}"
    else
      zoneinfo[:is_conditioned] = zone.isConditioned.get
    end

    zoneinfo[:is_ideal_air_loads] = zone.useIdealAirLoads
    zoneinfo[:heating_sizing_factor] = -1.0
    if zone.sizingZone.zoneHeatingSizingFactor.empty?
      error_warning << "zone.sizingZone.zoneHeatingSizingFactor is empty for #{zone.name.get}"
    else
      zoneinfo[:heating_sizing_factor] = zone.sizingZone.zoneHeatingSizingFactor.get
    end

    zoneinfo[:cooling_sizing_factor] = -1.0 # zone.sizingZone.zoneCoolingSizingFactor.get
    if zone.sizingZone.zoneCoolingSizingFactor.empty?
      error_warning << "zone.sizingZone.zoneCoolingSizingFactor is empty for #{zone.name.get}"
    else
      zoneinfo[:cooling_sizing_factor] = zone.sizingZone.zoneCoolingSizingFactor.get
    end

    zoneinfo[:zone_heating_design_supply_air_temperature] = zone.sizingZone.zoneHeatingDesignSupplyAirTemperature
    zoneinfo[:zone_cooling_design_supply_air_temperature] = zone.sizingZone.zoneCoolingDesignSupplyAirTemperature
    zoneinfo[:spaces] = []
    zone.spaces.sort.each do |space|
      spaceinfo = {}
      zoneinfo[:spaces] << spaceinfo
      spaceinfo[:name] = space.name.get
      spaceinfo[:type] = space.spaceType.get.name.get unless space.spaceType.empty?
    end
    zoneinfo[:equipment] = []
    zone.equipmentInHeatingOrder.each do |equipment|
      item = {}
      zoneinfo[:equipment] << item
      item[:name] = equipment.name.get
      if equipment.to_ZoneHVACComponent.is_initialized
        item[:type] = 'ZoneHVACComponent'
      elsif equipment.to_StraightComponent.is_initialized
        item[:type] = 'StraightComponent'
      end
    end
    # zone
  end
  # Store Air Loop Information
  qaqc[:air_loops] = []
  model.getAirLoopHVACs.sort.each do |air_loop|
    air_loop_info = {}
    air_loop_info[:name] = air_loop.name.get
    air_loop_info[:thermal_zones] = []
    air_loop_info[:total_floor_area_served] = 0.0
    air_loop_info[:total_breathing_zone_outdoor_airflow_vbz] = 0.0
    air_loop.thermalZones.sort.each do |zone|
      air_loop_info[:thermal_zones] << zone.name.get
      vbz = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='Standard62.1Summary' AND ReportForString='Entire Facility' AND TableName='Zone Ventilation Parameters' AND ColumnName='Breathing Zone Outdoor Airflow - Vbz' AND Units='m3/s' AND RowName='#{zone.name.get.to_s.upcase}' ")
      vbz = validate_optional(vbz, model, 0)
      air_loop_info[:total_breathing_zone_outdoor_airflow_vbz] += vbz
      air_loop_info[:total_floor_area_served] += zone.floorArea * zone.multiplier.to_f
    end
    air_loop_info[:area_outdoor_air_rate_m3_per_s_m2] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='Standard62.1Summary' AND ReportForString='Entire Facility' AND TableName='System Ventilation Parameters' AND ColumnName='Area Outdoor Air Rate - Ra' AND Units='m3/s-m2' AND RowName='#{air_loop_info[:name].to_s.upcase}' ")
    air_loop_info[:area_outdoor_air_rate_m3_per_s_m2] = validate_optional(air_loop_info[:area_outdoor_air_rate_m3_per_s_m2], model, -1.0)

    air_loop_info[:outdoor_air_L_per_s] = -1.0
    unless air_loop_info[:area_outdoor_air_rate_m3_per_s_m2] == -1.0
      air_loop_info[:outdoor_air_L_per_s] = air_loop_info[:area_outdoor_air_rate_m3_per_s_m2] * air_loop_info[:total_floor_area_served] * 1000
    end
    # Fan

    unless air_loop.supplyFan.empty?
      air_loop_info[:supply_fan] = {}
      if air_loop.supplyFan.get.to_FanConstantVolume.is_initialized
        air_loop_info[:supply_fan][:type] = 'CV'
        fan = air_loop.supplyFan.get.to_FanConstantVolume.get
      elsif air_loop.supplyFan.get.to_FanVariableVolume.is_initialized
        air_loop_info[:supply_fan][:type] = 'VV'
        fan = air_loop.supplyFan.get.to_FanVariableVolume.get
      end
      air_loop_info[:supply_fan][:name] = fan.name.get
      # puts "\n\n\n\n#{fan.name.get}\n\n\n\n"
      air_loop_info[:supply_fan][:fan_efficiency] = fan.fanEfficiency
      air_loop_info[:supply_fan][:motor_efficiency] = fan.motorEfficiency
      air_loop_info[:supply_fan][:pressure_rise] = fan.pressureRise
      air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] = -1.0

      max_air_flow_info = model.sqlFile.get.execAndReturnVectorOfString("SELECT RowName FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Fans' AND ColumnName='Max Air Flow Rate' AND Units='m3/s' ")
      max_air_flow_info = validate_optional(max_air_flow_info, model, 'N/A')
      if max_air_flow_info == 'N/A'
        error_warning << "max_air_flow_info is nil because the following sql statement returned nil: RowName FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Fans' AND ColumnName='Max Air Flow Rate' AND Units='m3/s' "
      else
        if max_air_flow_info.include? air_loop_info[:supply_fan][:name].to_s.upcase.to_s
          air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Fans' AND ColumnName='Max Air Flow Rate' AND Units='m3/s' AND RowName='#{air_loop_info[:supply_fan][:name].upcase}' ").get
          air_loop_info[:supply_fan][:rated_electric_power_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Fans' AND ColumnName='Rated Electric Power' AND Units='W' AND RowName='#{air_loop_info[:supply_fan][:name].upcase}' ")
          # Version 3.2.0 has renamed  rated electric_power  to rated electricity rate
          if air_loop_info[:supply_fan][:rated_electric_power_w].empty?
            air_loop_info[:supply_fan][:rated_electric_power_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Fans' AND ColumnName='Rated Electricity Rate' AND Units='W' AND RowName='#{air_loop_info[:supply_fan][:name].upcase}' ")
          end
          air_loop_info[:supply_fan][:rated_electric_power_w] = air_loop_info[:supply_fan][:rated_electric_power_w].get
          else
          error_warning << "#{air_loop_info[:supply_fan][:name]} does not exist in sql file WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Fans' AND ColumnName='Max Air Flow Rate' AND Units='m3/s'"
        end
      end
    end

    # economizer
    air_loop_info[:economizer] = {}
    air_loop_info[:economizer][:name] = air_loop.airLoopHVACOutdoorAirSystem.get.getControllerOutdoorAir.name.get unless air_loop.airLoopHVACOutdoorAirSystem.empty? or air_loop.airLoopHVACOutdoorAirSystem.get.getControllerOutdoorAir.name.empty?
    air_loop_info[:economizer][:control_type] = air_loop.airLoopHVACOutdoorAirSystem.get.getControllerOutdoorAir.getEconomizerControlType unless air_loop.airLoopHVACOutdoorAirSystem.empty?
    # DX cooling coils
    air_loop_info[:cooling_coils] = {}
    air_loop_info[:cooling_coils][:dx_single_speed] = []
    air_loop_info[:cooling_coils][:dx_two_speed] = []
    air_loop_info[:cooling_coils][:coil_cooling_water] = []

    # Heating Coil
    air_loop_info[:heating_coils] = {}
    air_loop_info[:heating_coils][:coil_heating_gas] = []
    air_loop_info[:heating_coils][:coil_heating_electric] = []
    air_loop_info[:heating_coils][:coil_heating_water] = []

    # Heat Excahnger
    air_loop_info[:heat_exchanger] = {}

    air_loop.supplyComponents.each do |supply_comp|
      if supply_comp.to_CoilHeatingGas.is_initialized
        coil = {}
        air_loop_info[:heating_coils][:coil_heating_gas] << coil
        gas = supply_comp.to_CoilHeatingGas.get
        coil[:name] = gas.name.get
        coil[:type] = 'Gas'
        coil[:efficency] = gas.gasBurnerEfficiency
        # coil[:nominal_capacity]= gas.nominalCapacity()
        coil[:nominal_capacity] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Heating Coils' AND ColumnName='Nominal Total Capacity' AND RowName='#{coil[:name].to_s.upcase}'")
        coil[:nominal_capacity] = validate_optional(coil[:nominal_capacity], model, -1.0)
      end
      if supply_comp.to_CoilHeatingElectric.is_initialized
        coil = {}
        air_loop_info[:heating_coils][:coil_heating_electric] << coil
        electric = supply_comp.to_CoilHeatingElectric.get
        coil[:name] = electric.name.get
        coil[:type] = 'Electric'
        coil[:nominal_capacity] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Heating Coils' AND ColumnName='Nominal Total Capacity' AND RowName='#{coil[:name].to_s.upcase}'")
        coil[:nominal_capacity] = validate_optional(coil[:nominal_capacity], model, -1.0)
      end
      if supply_comp.to_CoilHeatingWater.is_initialized
        coil = {}
        air_loop_info[:heating_coils][:coil_heating_water] << coil
        water = supply_comp.to_CoilHeatingWater.get
        coil[:name] = water.name.get
        coil[:type] = 'Water'
        coil[:nominal_capacity] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Heating Coils' AND ColumnName='Nominal Total Capacity' AND RowName='#{coil[:name].to_s.upcase}'")
        coil[:nominal_capacity] = validate_optional(coil[:nominal_capacity], model, -1.0)
      end
      if supply_comp.to_HeatExchangerAirToAirSensibleAndLatent.is_initialized
        heatExchanger = supply_comp.to_HeatExchangerAirToAirSensibleAndLatent.get
        air_loop_info[:heat_exchanger][:name] = heatExchanger.name.get
      end
    end
    # I dont think i need to get the type of heating coil from the sql file, because the coils are differentiated by class, and I have hard coded the information
    # model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName= 'Heating Coils' AND ColumnName='Type' ").get #padmussen to complete #AND RowName='#{air_loop_info[:heating_coils][:name].upcase}'

    # Collect all the fans into the the array.
    air_loop.supplyComponents.each do |supply_comp|
      if supply_comp.to_CoilCoolingDXSingleSpeed.is_initialized
        coil = {}
        air_loop_info[:cooling_coils][:dx_single_speed] << coil
        single_speed = supply_comp.to_CoilCoolingDXSingleSpeed.get
        coil[:name] = single_speed.name.get
        coil[:cop] = single_speed.ratedCOP.to_f
        coil[:nominal_total_capacity_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Cooling Coils' AND ColumnName='Nominal Total Capacity' AND RowName='#{coil[:name].upcase}' ")
        coil[:nominal_total_capacity_w] = validate_optional(coil[:nominal_total_capacity_w], model, -1.0)
      end
      if supply_comp.to_CoilCoolingDXTwoSpeed.is_initialized
        coil = {}
        air_loop_info[:cooling_coils][:dx_two_speed] << coil
        two_speed = supply_comp.to_CoilCoolingDXTwoSpeed.get
        coil[:name] = two_speed.name.get
        coil[:cop_low] = two_speed.ratedLowSpeedCOP.to_f
        coil[:cop_high] = two_speed.ratedHighSpeedCOP.to_f
        coil[:nominal_total_capacity_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Cooling Coils' AND ColumnName='Nominal Total Capacity' AND RowName='#{coil[:name].upcase}' ")
        coil[:nominal_total_capacity_w] = validate_optional(coil[:nominal_total_capacity_w], model, -1.0)
      end
      if supply_comp.to_CoilCoolingWater.is_initialized
        coil = {}
        air_loop_info[:cooling_coils][:coil_cooling_water] << coil
        coil_cooling_water = supply_comp.to_CoilCoolingWater.get
        coil[:name] = coil_cooling_water.name.get
        coil[:nominal_total_capacity_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Cooling Coils' AND ColumnName='Nominal Total Capacity' AND RowName='#{coil[:name].upcase}' ")
        coil[:nominal_total_capacity_w] = validate_optional(coil[:nominal_total_capacity_w], model, -1.0)
        coil[:nominal_sensible_heat_ratio] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Cooling Coils' AND ColumnName='Nominal Sensible Heat Ratio' AND RowName='#{coil[:name].upcase}' ")
        coil[:nominal_sensible_heat_ratio] = validate_optional(coil[:nominal_sensible_heat_ratio], model, -1.0)
      end
    end
    qaqc[:air_loops] << air_loop_info
  end

  qaqc[:plant_loops] = []
  model.getPlantLoops.sort.each do |plant_loop|
    plant_loop_info = {}
    qaqc[:plant_loops] << plant_loop_info
    plant_loop_info[:name] = plant_loop.name.get

    sizing = plant_loop.sizingPlant
    plant_loop_info[:design_loop_exit_temperature] = sizing.designLoopExitTemperature
    plant_loop_info[:loop_design_temperature_difference] = sizing.loopDesignTemperatureDifference

    # Create Container for plant equipment arrays.
    plant_loop_info[:pumps] = []
    plant_loop_info[:boilers] = []
    plant_loop_info[:chiller_electric_eir] = []
    plant_loop_info[:cooling_tower_single_speed] = []
    plant_loop_info[:water_heater_mixed] = []
    plant_loop.supplyComponents.each do |supply_comp|
      # Collect Constant Speed
      if supply_comp.to_PumpConstantSpeed.is_initialized
        pump = supply_comp.to_PumpConstantSpeed.get
        pump_info = {}
        plant_loop_info[:pumps] << pump_info
        pump_info[:name] = pump.name.get
        pump_info[:type] = 'Pump:ConstantSpeed'
        pump_info[:head_pa] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Pumps' AND ColumnName='Head' AND RowName='#{pump_info[:name].upcase}' ")
        pump_info[:head_pa] = validate_optional(pump_info[:head_pa], model, -1.0)
        pump_info[:water_flow_m3_per_s] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Pumps' AND ColumnName='Water Flow' AND RowName='#{pump_info[:name].upcase}' ")
        pump_info[:water_flow_m3_per_s] = validate_optional(pump_info[:water_flow_m3_per_s], model, -1.0)
        pump_info[:electric_power_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Pumps' AND ColumnName='Electric Power' AND RowName='#{pump_info[:name].upcase}' ")
        pump_info[:electric_power_w] = validate_optional(pump_info[:electric_power_w], model, -1.0)
        pump_info[:motor_efficency] = pump.motorEfficiency
      end

      # Collect Variable Speed
      if supply_comp.to_PumpVariableSpeed.is_initialized
        pump = supply_comp.to_PumpVariableSpeed.get
        pump_info = {}
        plant_loop_info[:pumps] << pump_info
        pump_info[:name] = pump.name.get
        pump_info[:type] = 'Pump:VariableSpeed'
        pump_info[:head_pa] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Pumps' AND ColumnName='Head' AND RowName='#{pump_info[:name].upcase}' ")
        pump_info[:head_pa] = validate_optional(pump_info[:head_pa], model, -1.0)
        pump_info[:water_flow_m3_per_s] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Pumps' AND ColumnName='Water Flow' AND RowName='#{pump_info[:name].upcase}' ")
        pump_info[:water_flow_m3_per_s] = validate_optional(pump_info[:water_flow_m3_per_s], model, -1.0)
        pump_info[:electric_power_w] = model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='EquipmentSummary' AND ReportForString='Entire Facility' AND TableName='Pumps' AND ColumnName='Electric Power' AND RowName='#{pump_info[:name].upcase}' ")
        pump_info[:electric_power_w] = validate_optional(pump_info[:electric_power_w], model, -1.0)
        pump_info[:motor_efficency] = pump.motorEfficiency
      end

      # Collect HotWaterBoilers
      if supply_comp.to_BoilerHotWater.is_initialized
        boiler = supply_comp.to_BoilerHotWater.get
        boiler_info = {}
        plant_loop_info[:boilers] << boiler_info
        boiler_info[:name] = boiler.name.get
        boiler_info[:type] = 'Boiler:HotWater'
        boiler_info[:fueltype] = boiler.fuelType
        boiler_info[:nominal_capacity] = boiler.nominalCapacity
        boiler_info[:nominal_capacity] = validate_optional(boiler_info[:nominal_capacity], model, -1.0)
      end

      # Collect ChillerElectricEIR
      if supply_comp.to_ChillerElectricEIR.is_initialized
        chiller = supply_comp.to_ChillerElectricEIR.get
        chiller_info = {}
        plant_loop_info[:chiller_electric_eir] << chiller_info
        chiller_info[:name] = chiller.name.get
        chiller_info[:type] = 'Chiller:Electric:EIR'
        chiller_info[:reference_capacity] = validate_optional(chiller.referenceCapacity, model, -1.0)
        chiller_info[:reference_leaving_chilled_water_temperature] = chiller.referenceLeavingChilledWaterTemperature
      end

      # Collect CoolingTowerSingleSpeed
      if supply_comp.to_CoolingTowerSingleSpeed.is_initialized
        coolingTower = supply_comp.to_CoolingTowerSingleSpeed.get
        coolingTower_info = {}
        plant_loop_info[:cooling_tower_single_speed] << coolingTower_info
        coolingTower_info[:name] = coolingTower.name.get
        coolingTower_info[:type] = 'CoolingTower:SingleSpeed'
        coolingTower_info[:fan_power_at_design_air_flow_rate] = validate_optional(coolingTower.fanPoweratDesignAirFlowRate, model, -1.0)

      end

      # Collect WaterHeaterMixed
      if supply_comp.to_WaterHeaterMixed.is_initialized
        waterHeaterMixed = supply_comp.to_WaterHeaterMixed.get
        waterHeaterMixed_info = {}
        plant_loop_info[:water_heater_mixed] << waterHeaterMixed_info
        waterHeaterMixed_info[:name] = waterHeaterMixed.name.get
        waterHeaterMixed_info[:type] = 'WaterHeater:Mixed'
        waterHeaterMixed_info[:heater_thermal_efficiency] = waterHeaterMixed.heaterThermalEfficiency.get unless waterHeaterMixed.heaterThermalEfficiency.empty?
        waterHeaterMixed_info[:heater_fuel_type] = waterHeaterMixed.heaterFuelType
      end
    end

    qaqc[:eplusout_err] = {}
    warnings = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='0' ")
    warnings = validate_optional(warnings, model, 'N/A')
    unless warnings == 'N/A'
      qaqc[:eplusout_err][:warnings] = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='0' ").get
      qaqc[:eplusout_err][:fatal] = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='2' ").get
      qaqc[:eplusout_err][:severe] = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='1' ").get
    end

    qaqc[:ruby_warnings] = error_warning
  end

  qaqc[:code_metrics] = {}
  qaqc[:code_metrics]['heating_gj']  = qaqc[:end_uses]['heating_gj']
  qaqc[:code_metrics]['cooling_gj']  = qaqc[:end_uses]['cooling_gj']
  qaqc[:code_metrics][:ep_conditioned_floor_area_m2] = qaqc[:building][:conditioned_floor_area_m2]
  qaqc[:code_metrics][:os_conditioned_floor_area_m2] = qaqc[:envelope][:interior_floors_area_m2] +
                                                       qaqc[:envelope][:outdoor_floors_area_m2] +
                                                       qaqc[:envelope][:ground_floors_area_m2]
  # TEDI
  unless qaqc[:building][:conditioned_floor_area_m2].nil?
    qaqc[:code_metrics][:building_tedi_gj_per_m2] = (qaqc[:end_uses]['heating_gj'] + qaqc[:end_uses]['cooling_gj']
                                                    ) / qaqc[:building][:conditioned_floor_area_m2]
    # Mech TEDI?
    qaqc[:code_metrics][:building_medi_gj_per_m2] = (qaqc[:end_uses]['fans_gj'] +
        qaqc[:end_uses]['pumps_gj'] +
        qaqc[:end_uses]['heat_rejection_gj'] +
        qaqc[:end_uses]['humidification_gj'] +
        qaqc[:end_uses]['heat_recovery_gj']
    ) / qaqc[:building][:conditioned_floor_area_m2]
  end


  return qaqc
end

#create_ems_to_turn_on_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed_for_night_cycle(multi_speed_heat_pump) ⇒ Object

Create EMS to turn on “AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed” in response to a call from the night cycle availability manager of the air loop. It was found that this object doesn’t respond properly to this call from the night cycle



1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1088

def create_ems_to_turn_on_AirLoopHVACUnitaryHeatPumpAirToAirMultiSpeed_for_night_cycle(multi_speed_heat_pump)
  model = multi_speed_heat_pump.model
  avail_manager_name = nil
  if multi_speed_heat_pump.airLoopHVAC.is_initialized
    if !multi_speed_heat_pump.airLoopHVAC.get.availabilityManagers.empty?
      avail_manager_name = multi_speed_heat_pump.airLoopHVAC.get.availabilityManagers[0].name.to_s
    end
  end
  return unless avail_manager_name

  avail_manager_out_var_name = 'Availability Manager Night Cycle Control Status'
  avail_manager_out_var = OpenStudio::Model::OutputVariable.new(avail_manager_out_var_name, model)
  avail_manager_out_var.setKeyValue(avail_manager_name)
  avail_manager_out_var.setReportingFrequency('Timestep')
  night_cycle_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, avail_manager_out_var)
  heat_pump_avail_sch = nil
  if multi_speed_heat_pump.availabilitySchedule.is_initialized
    heat_pump_avail_sch = multi_speed_heat_pump.availabilitySchedule.get
  elsif multi_speed_heat_pump.airLoopHVAC.get.availabilitySchedule.is_initialized
    heat_pump_avail_sch = multi_speed_heat_pump.airLoopHVAC.get.availabilitySchedule.get
  else
    heat_pump_avail_sch = OpenStudio::Model::ScheduleConstant.new(model)
    heat_pump_avail_sch.setValue(1.0)
  end
  heat_pump_avail_sch_var = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
  heat_pump_avail_sch_var.setKeyValue(heat_pump_avail_sch.name.to_s)
  heat_pump_avail_sch_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, heat_pump_avail_sch_var)
  updated_heat_pump_avail_sch = OpenStudio::Model::ScheduleConstant.new(model)
  multi_speed_heat_pump.setAvailabilitySchedule(updated_heat_pump_avail_sch)
  # This method will seem like an error in number of args..but this is due to swig voodoo.
  heat_pump_avail_sch_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(updated_heat_pump_avail_sch, 'Schedule:Constant', 'Schedule Value')
  heat_pump_avail_sch_prog = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
  heat_pump_avail_sch_prog.setName("#{multi_speed_heat_pump.name.to_s.gsub(/[ +-.]/, '_')} Availability Schedule Program by Line")
  heat_pump_avail_sch_prog_body = <<-EMS
      IF #{heat_pump_avail_sch_sensor.handle} > 0.0
        SET #{heat_pump_avail_sch_actuator.handle} = #{heat_pump_avail_sch_sensor.handle}
      ELSEIF #{night_cycle_sensor.handle} == 2.0
        SET #{heat_pump_avail_sch_actuator.handle} = 1.0
      ELSE
        SET #{heat_pump_avail_sch_actuator.handle} = 0.0
      ENDIF
  EMS
  heat_pump_avail_sch_prog.setBody(heat_pump_avail_sch_prog_body)
  pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
  pcm.setName("#{heat_pump_avail_sch_prog.name.to_s.gsub(/[ +-.]/, '_')} Calling Manager")
  pcm.setCallingPoint('InsideHVACSystemIterationLoop')
  pcm.addProgram(heat_pump_avail_sch_prog)
end

#create_heating_cooling_on_off_availability_schedule(model) ⇒ Object



2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2134

def create_heating_cooling_on_off_availability_schedule(model)
  # @todo Create a feature to derive start and end heating and cooling seasons from weather file.
  avail_data = [{ start_month: 1, start_day: 1, end_month: 6, end_day: 30, htg_value: 1, clg_value: 0 },
                { start_month: 7, start_day: 1, end_month: 10, end_day: 31, htg_value: 0, clg_value: 1 },
                { start_month: 11, start_day: 1, end_month: 12, end_day: 31, htg_value: 1, clg_value: 0 }]

  # Heating coil availability schedule for tpfc
  htg_availability_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  htg_availability_sch.setName('tpfc_htg_availability')
  # Cooling coil availability schedule for tpfc
  clg_availability_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  clg_availability_sch.setName('tpfc_clg_availability')
  avail_data.each do |data|
    htg_availability_sch_rule = OpenStudio::Model::ScheduleRule.new(htg_availability_sch)
    htg_availability_sch_rule.setName('tpfc_htg_availability_sch_rule')
    htg_availability_sch_rule.setStartDate(model.getYearDescription.makeDate(data[:start_month], data[:start_day]))
    htg_availability_sch_rule.setEndDate(model.getYearDescription.makeDate(data[:end_month], data[:end_day]))
    htg_availability_sch_rule.setApplySunday(true)
    htg_availability_sch_rule.setApplyMonday(true)
    htg_availability_sch_rule.setApplyTuesday(true)
    htg_availability_sch_rule.setApplyWednesday(true)
    htg_availability_sch_rule.setApplyThursday(true)
    htg_availability_sch_rule.setApplyFriday(true)
    htg_availability_sch_rule.setApplySaturday(true)
    day_schedule = htg_availability_sch_rule.daySchedule
    day_schedule.setName('tpfc_htg_availability_sch_rule_day')
    day_schedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), data[:htg_value])

    clg_availability_sch_rule = OpenStudio::Model::ScheduleRule.new(clg_availability_sch)
    clg_availability_sch_rule.setName('tpfc_clg_availability_sch_rule')
    clg_availability_sch_rule.setStartDate(model.getYearDescription.makeDate(data[:start_month], data[:start_day]))
    clg_availability_sch_rule.setEndDate(model.getYearDescription.makeDate(data[:end_month], data[:end_day]))
    clg_availability_sch_rule.setApplySunday(true)
    clg_availability_sch_rule.setApplyMonday(true)
    clg_availability_sch_rule.setApplyTuesday(true)
    clg_availability_sch_rule.setApplyWednesday(true)
    clg_availability_sch_rule.setApplyThursday(true)
    clg_availability_sch_rule.setApplyFriday(true)
    clg_availability_sch_rule.setApplySaturday(true)
    day_schedule = clg_availability_sch_rule.daySchedule
    day_schedule.setName('tpfc_clg_availability_sch_rule_day')
    day_schedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), data[:clg_value])
  end
  return clg_availability_sch, htg_availability_sch
end

#create_hw_loop_if_required(baseboard_type, boiler_fueltype, mau_heating_coil_type, model) ⇒ Object

Method will create a hot water loop if systems default fuel and medium sources require it.



826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 826

def create_hw_loop_if_required(baseboard_type, boiler_fueltype, mau_heating_coil_type, model)
  # get systems that will be used in the model based on the space types to determine if a hw_loop is required.
  systems_used = []
  model.getSpaces.sort.each do |space|
    systems_used << get_necb_spacetype_system_selection(space)
    systems_used.uniq!
  end

  # See if we need to create a hot water loop based on fueltype and systems used.
  hw_loop_needed = false
  systems_used.each do |system|
    case system.to_s
    when '2', '5', '7'
      hw_loop_needed = true
    when '1', '6'
      if (mau_heating_coil_type == 'Hot Water') || (baseboard_type == 'Hot Water')
        hw_loop_needed = true
      end
    when '3', '4'
      if (mau_heating_coil_type == 'Hot Water') || (baseboard_type == 'Hot Water')
        hw_loop_needed = true if baseboard_type == 'Hot Water'
      end
    end
    if hw_loop_needed
      # just need one true condition to need a boiler.
      break
    end
    # each
  end
  # create hw_loop as needed.. Assuming one loop per model.
  if hw_loop_needed
    @hw_loop = OpenStudio::Model::PlantLoop.new(model)
    always_on = model.alwaysOnDiscreteSchedule
    setup_hw_loop_with_components(model, @hw_loop, boiler_fueltype, always_on)
  end
  return @hw_loop
end

#create_necb_system(baseboard_type:, boiler_fueltype:, chiller_type:, fan_type:, heating_coil_type_sys3:, heating_coil_type_sys4:, heating_coil_type_sys6:, hw_loop:, mau_cooling_type:, mau_heating_coil_type:, mau_type:, model:, zones:, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel') ⇒ Object

Default method to create a necb system and assign array of zones to be supported by it. It will try to bring zones with similar loads on the same airloops and set control zones where possible for single zone systems and will create monolithic system 6 multizones where possible.



867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 867

def create_necb_system(baseboard_type:,
                       boiler_fueltype:,
                       chiller_type:,
                       fan_type:,
                       heating_coil_type_sys3:,
                       heating_coil_type_sys4:,
                       heating_coil_type_sys6:,
                       hw_loop:,
                       mau_cooling_type:,
                       mau_heating_coil_type:,
                       mau_type:,
                       model:,
                       zones:,
                       necb_reference_hp:false,
                       necb_reference_hp_supp_fuel:'DefaultFuel')
  # The goal is to minimize the number of system when possible.
  system_zones_hash = {}
  zones.each do |zone|
    system_zones_hash[get_necb_thermal_zone_system_selection(zone)] = [] if system_zones_hash[get_necb_thermal_zone_system_selection(zone)].nil?
    system_zones_hash[get_necb_thermal_zone_system_selection(zone)] << zone
  end
  # puts JSON.pretty_generate(system_zones_hash)
  # go through each system and zones pairs to
  system_zones_hash.each_pair do |system, sys_zones|
    case system
    when 0, nil
      # Do nothing no system assigned to zone. Used for Unconditioned spaces
    when 1
      group_similar_zones_together(sys_zones).each do |curr_zones|
        mau_air_loop = add_sys1_unitary_ac_baseboard_heating(model: model,
                                                             necb_reference_hp: necb_reference_hp,
                                                             necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                             zones: curr_zones,
                                                             mau_type: mau_type,
                                                             mau_heating_coil_type: mau_heating_coil_type,
                                                             baseboard_type: baseboard_type,
                                                             hw_loop: @hw_loop,
                                                             multispeed: false)
      end
    when 2
      group_similar_zones_together(sys_zones).each do |curr_zones|
        add_sys2_FPFC_sys5_TPFC(model: model,
                                zones: curr_zones,
                                chiller_type: chiller_type,
                                mau_cooling_type: mau_cooling_type,
                                fan_coil_type: 'FPFC',
                                hw_loop: @hw_loop)
      end
    when 3
      group_similar_zones_together(sys_zones).each do |curr_zones|
        add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
                                                                              necb_reference_hp: necb_reference_hp,
                                                                              necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                                              zones: curr_zones,
                                                                              heating_coil_type: heating_coil_type_sys3,
                                                                              baseboard_type: baseboard_type,
                                                                              hw_loop: @hw_loop,
                                                                              multispeed: false)
      end
    when 4
      group_similar_zones_together(sys_zones).each do |curr_zones|
        add_sys4_single_zone_make_up_air_unit_with_baseboard_heating(model: model,
                                                                     necb_reference_hp: necb_reference_hp,
                                                                     necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel,
                                                                     zones: curr_zones,
                                                                     heating_coil_type: heating_coil_type_sys4,
                                                                     baseboard_type: baseboard_type,
                                                                     hw_loop: @hw_loop)
        #          add_sys3and8_single_zone_packaged_rooftop_unit_with_baseboard_heating(model: model,
        #                                                                                zones: zones,
        #                                                                                heating_coil_type: heating_coil_type_sys4,
        #                                                                                baseboard_type: baseboard_type,
        #                                                                                hw_loop: @hw_loop,
        #                                                                                multispeed: false)
      end
    when 5
      group_similar_zones_together(sys_zones).each do |curr_zones|
        add_sys2_FPFC_sys5_TPFC(model: model,
                                zones: curr_zones,
                                chiller_type: chiller_type,
                                mau_cooling_type: mau_cooling_type,
                                fan_coil_type: 'TPFC',
                                hw_loop: @hw_loop)
      end
    when 6
      if necb_reference_hp
        add_sys6_multi_zone_reference_hp_with_baseboard_heating(model: model,
                                                                zones: sys_zones,
                                                                heating_coil_type: heating_coil_type_sys6,
                                                                baseboard_type: baseboard_type,
                                                                hw_loop:@hw_loop,
                                                                necb_reference_hp_supp_fuel: necb_reference_hp_supp_fuel)
      else
        add_sys6_multi_zone_built_up_system_with_baseboard_heating(model: model,
                                                                  zones: sys_zones,
                                                                  heating_coil_type: heating_coil_type_sys6,
                                                                  baseboard_type: baseboard_type,
                                                                  chiller_type: chiller_type,
                                                                  fan_type: fan_type,
                                                                  hw_loop: @hw_loop)
      end
    when 7
      group_similar_zones_together(sys_zones).each do |curr_zones|
        add_sys2_FPFC_sys5_TPFC(model: model,
                                zones: curr_zones,
                                chiller_type: chiller_type,
                                fan_coil_type: 'FPFC',
                                mau_cooling_type: mau_cooling_type,
                                hw_loop: @hw_loop)
      end
    end
  end
end

#determine_control_zone(zones) ⇒ Object

This method will determine the control zone from the last sizing run space loads.



1212
1213
1214
1215
1216
1217
1218
1219
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1212

def determine_control_zone(zones)
  # In this case the control zone is the load with the largest heating loads. This may cause overheating of some zones.
  # but this is preferred to unmet heating.
  # Iterate through zones.
  zone_heating_load_hash = {}
  zones.each { |zone| zone_heating_load_hash[zone] = stored_zone_heating_load(zone) }
  return zone_heating_load_hash.max_by(&:last).first
end

#determine_dominant_necb_schedule_type(model) ⇒ Object

This model determines the dominant NECB schedule type return s.each [String]

Parameters:

  • model (OpenStudio::Model::Model)

    A model object



806
807
808
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 806

def determine_dominant_necb_schedule_type(model)
  return determine_dominant_schedule(model.getSpaces)
end

#determine_dominant_schedule(spaces) ⇒ Object



765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 765

def determine_dominant_schedule(spaces)
  # lookup necb space type properties
  space_type_properties = @standards_data['space_types']
  # Here is a hash to keep track of the m2 running total of spacetypes for each
  # sched type.
  # 2018-04-11:  Not sure if this is still used but the list was expanded to incorporate additional existing or potential
  # future schedules.
  schedule_hash = Hash[
      'A', 0,
      'B', 0,
      'C', 0,
      'D', 0,
      'E', 0,
      'F', 0,
      'G', 0,
      'H', 0,
      'I', 0,
      'J', 0,
      'K', 0,
      'L', 0,
      'M', 0,
      'N', 0,
      'O', 0,
      'P', 0,
      'Q', 0
  ]
  # iterate through spaces in building.
  spaces.select { |space| !is_an_necb_wildcard_space?(space) && (space.spaceType.get.standardsSpaceType.get != '- undefined -') }.each do |space|
    # Ensure space floors are multiplied.
    mult = @space_multiplier_map[space.name.to_s].nil? ? 1.0 : @space_multiplier_map[space.name.to_s]
    # puts "this #{determine_necb_schedule_type(space)}"
    schedule_hash[determine_necb_schedule_type(space)] += space.floorArea * mult
  end
  # finds max value and returns NECB schedule letter.
  # determine dominant letter schedule.
  return schedule_hash.max_by(&:last).first
end

#determine_necb_schedule_type(space) ⇒ String

This method determines the spacetype schedule type. This will re

Parameters:

Returns:

  • (String)

    :[“A”,“B”,“C”,“D”,“E”,“F”,“G”,“H”,“I”] spacetype

Author:



814
815
816
817
818
819
820
821
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 814

def determine_necb_schedule_type(space)
  spacetype_data = @standards_data['space_types']
  raise "Spacetype not defined for space #{space.get.name}) if space.spaceType.empty?" if space.spaceType.empty?
  raise "Undefined standardsSpaceType or StandardsBuildingType for space #{space.spaceType.get.name}) if space.spaceType.empty?" if space.spaceType.get.standardsSpaceType.empty? | space.spaceType.get.standardsBuildingType.empty?

  space_type_properties = spacetype_data.detect { |st| (st['space_type'] == space.spaceType.get.standardsSpaceType.get) && (st['building_type'] == space.spaceType.get.standardsBuildingType.get) }
  return space_type_properties['necb_schedule_type'].strip
end

#determine_spacetype_vintage(model) ⇒ Object

this method will determine the vintage of NECB spacetypes the model contains. It will return nil if it can’t determine it.



1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1108

def determine_spacetype_vintage(model)
  #this code is the list of available vintages
  space_type_vintage_list = ['NECB2011', 'NECB2015', 'NECB2017', 'NECB2020', 'BTAPPRE1980', 'BTAP1980TO2010']
  #this reorders the list to do the current class first.
  space_type_vintage_list.insert(0, space_type_vintage_list.delete(self.class.name))
  # Set the space_type
  space_type_vintage = nil
  # get list of space types used in model and a mapped string.
  model_space_type_names = model.getSpaceTypes.map do |spacetype|
    [spacetype.standardsBuildingType.get.to_s + '-' + spacetype.standardsSpaceType.get.to_s]
  end
  # Now iterate though each vintage
  space_type_vintage_list.each do |template|
    # Create the standard object and get a list of all the spacetypes available for that vintage.
    standard_space_type_list = Standard.build(template).get_all_spacetype_names.map { |spacetype| [spacetype[0].to_s + '-' + spacetype[1].to_s] }
    # set array to contain unknown spacetypes.
    unknown_spacetypes = []
    # iterate though all space types that the model is using
    model_space_type_names.each do |space_type_name|
      # push unknown spacetypes into the array.
      unknown_spacetypes << space_type_name unless standard_space_type_list.include?(space_type_name)
    end
    if unknown_spacetypes.empty?
      # No unknowns, so return the template and don't bother looking for others.
      return template
    end
  end
  return space_type_vintage
end

#distance(loc1, loc2) ⇒ Object

Enter in [latitude, longitude] for each loc and this method will return the distance.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 150

def distance(loc1, loc2)
  rad_per_deg = Math::PI / 180 # PI / 180
  rkm = 6371 # Earth radius in kilometers
  rm = rkm * 1000 # Radius in meters

  dlat_rad = (loc2[0] - loc1[0]) * rad_per_deg # Delta, converted to rad
  dlon_rad = (loc2[1] - loc1[1]) * rad_per_deg

  lat1_rad, lon1_rad = loc1.map { |i| i * rad_per_deg }
  lat2_rad, lon2_rad = loc2.map { |i| i * rad_per_deg }

  a = Math.sin(dlat_rad / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2)**2
  c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
  rm * c # Delta in meters
end

#download_and_save_file(weather_list_url:, weather_loc:, git_folder:) ⇒ Object

Arguments: weather_list_url (string): the web address of the json file containing the list of weather files on the repository weather_loc (string): the name of the epw file we are looking for without the .epw extension git_folder (string): the url of the folder containing the weather files. As of 2023-07-07 this this is either the

url of the historical weather data folder or the future weather data folder.


2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2276

def download_and_save_file(weather_list_url:, weather_loc:, git_folder:)
  status = false
  attempt = 1
  # Try to download the list of weather files 5 times, waiting 5 seconds between each attempt.
  until attempt > 5
    begin
      puts "Beginning attempt #{attempt} to download #{weather_list_url}"
      # Download the list of weather files on the repository
      URI.open(weather_list_url) do |web_data|
        # Convert the weather file list to an array
        if web_data.size <= 100
          raise("Could not read #{weather_list_url}!")
        end
        weather_files = (JSON.parse(web_data.read)).to_a
        # Check to see if the requested weather file is on the list
        zip_name = weather_files.find{ |weather_file| weather_file.match(weather_loc) }
        # If the weather file is on the list proceed, otherwise report that it could not be found
        unless zip_name.nil?
          # Found the weather file on the list
          # Define the full url of the weather zip file we want to download
          save_file_url = git_folder + zip_name
          # Define the local location of where the weather zip file will be saved
          weather_dir = File.absolute_path(File.join(__FILE__, '..', '..', '..', '..', '..' , '..', "data/weather"))
          save_file = File.join(weather_dir, zip_name)
          attemptb = 1
          # Try to download the weather file up to 5 times, waiting 5 seconds between each attempt.
          until attemptb > 5
            begin
              puts "Beginning attempt #{attemptb} to download #{save_file_url}"
              # Download the weather zip file from the repository
              URI.open(save_file_url) do |file_url|
                if file_url.size <= 100
                  raise("Could not read #{save_file_url}!")
                end
                # Save the zip file in the /data/weather folder
                File.open(save_file, 'wb') { |f| f.write(file_url.read) }
                puts "Downloaded #{save_file_url} to #{save_file}"
                # Extract the individual weother files from the zip file
                status = extract_weather_data(zipped_file: save_file, weather_dir: weather_dir)
              end
              attemptb = 10
            rescue
              sleep(30)
              attemptb += 1
            end
          end
        end
      end
      attempt = 10
    rescue
      sleep(30)
      attempt += 1
    end
  end
  return status
end

#extract_weather_data(zipped_file:, weather_dir:) ⇒ Object

This method extracts data from a zip file. The name implies it is to be used for zip files containing weather data but really it can be used to extract any zip file. Arguments: zipped_file (string): As the name implies, this is the name and path to the zip file we want to expand. weather_dir (string): This is the folder where the zipped files will be extracted to. Don’t let the name fool you.

it can be any folder not just a weather directory.


2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2369

def extract_weather_data(zipped_file:, weather_dir:)
  # Set a flag to check if the weather data is for future weather.
  future_file = false
  # Start expanding the data.
  Zip::File.open(zipped_file) do |zip_file|
    puts "Expanding #{zipped_file}"
    # Cycle through each file in the zip file
    zip_file.each do |entry|
      # Define the location of where the file will be saved locally
      curr_save_file = File.join(weather_dir, entry.name.to_s)
      puts "Extracting #{entry.name} to #{curr_save_file}"
      # Check if the file includes a '_ASHRAE.ddy' term.  If there is, later on we will rename the '_ASHRAE.ddy' file
      # to just '.ddy'. This is so the rest of BTAP uses the _ASHRAE.ddy file.
      entry_name = entry.name.to_s
      if entry_name.length > 11
        future_file = true if entry_name[-11..-1] == '_ASHRAE.ddy'
      end
      # entry.extract # This was required before but now it isn't.  I'm confused so am saving this comment to
      # remind me if there are problems later.
      # Read the data from the file
      content = entry.get_input_stream.read
      # Save the data locally
      File.open(curr_save_file, 'wb') { |save_f| save_f.write(content) }
    end
  end
  if future_file
    # Rename the non ASHRAE.ddy as '_non_ASHRAE.ddy' and save the '_ASHRAE.ddy' as the regular '.ddy' file.  This is
    # because the ASHRAE .ddy file includes sizing information not included in the regular .ddy file for future
    # weather data files.  Unfortunately, openstudio-standards just looks for the regular .ddy file for sizing
    # information which is why the switch is done.
    weather_loc = (File.basename(zipped_file).to_s)[0..-5]
    puts "Renaming #{weather_loc}.ddy as #{weather_loc}_orig.ddy and #{weather_loc}_ASHRAE.ddy as #{weather_loc}.ddy."
    puts "This is so that the design weather information in the #{weather_loc}_ASHRAE.ddy file is used."
    orig_ddy_name = File.join(weather_dir, (weather_loc + ".ddy"))
    ashrae_ddy_name = File.join(weather_dir, (weather_loc + "_ASHRAE.ddy"))
    rev_ddy_name = File.join(weather_dir, (weather_loc + "_orig.ddy"))
    FileUtils.cp(orig_ddy_name, rev_ddy_name)
    FileUtils.cp(ashrae_ddy_name, orig_ddy_name)
  end
  # Return true if everything worked out
  return true
end

#fan_baseline_impeller_efficiency(fan) ⇒ Double

TODO:

Add fan type to data model and modify this method

Determines the baseline fan impeller efficiency based on the specified fan type.

Returns:

  • (Double)

    impeller efficiency (0.0 to 1.0)



1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1323

def fan_baseline_impeller_efficiency(fan)
  # Assume that the fan efficiency is 65% for normal fans
  # @todo add fan type to fan data model
  # and infer impeller efficiency from that?
  # or do we always assume a certain type of
  # fan impeller for the baseline system?
  # @todo check COMNET and T24 ACM and PNNL 90.1 doc
  fan_impeller_eff = 0.65

  return fan_impeller_eff
end

#fan_constant_volume_apply_prototype_fan_pressure_rise(fan_constant_volume) ⇒ Object



1522
1523
1524
1525
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1522

def fan_constant_volume_apply_prototype_fan_pressure_rise(fan_constant_volume)
  fan_constant_volume.setPressureRise(get_standards_constant('fan_constant_volume_pressure_rise_value'))
  return true
end

#fan_standard_minimum_motor_efficiency_and_size(fan, motor_bhp) ⇒ Array<Double>

Determines the minimum fan motor efficiency and nominal size for a given motor bhp. This should be the total brake horsepower with any desired safety factor already included. This method picks the next nominal motor catgory larger than the required brake horsepower, and the efficiency is based on that size. For example, if the bhp = 6.3, the nominal size will be 7.5HP and the efficiency for 90.1-2010 will be 91.7% from Table 10.8B. This method assumes 4-pole, 1800rpm totally-enclosed fan-cooled motors.

Parameters:

  • motor_bhp (Double)

    motor brake horsepower (hp)

Returns:

  • (Array<Double>)

    minimum motor efficiency (0.0 to 1.0), nominal horsepower



1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1346

def fan_standard_minimum_motor_efficiency_and_size(fan, motor_bhp)
  fan_motor_eff = 0.85
  nominal_hp = motor_bhp

  # Don't attempt to look up motor efficiency
  # for zero-hp fans, which may occur when there is no
  # airflow required for a particular system, typically
  # heated-only spaces with high internal gains
  # and no OA requirements such as elevator shafts.
  return [fan_motor_eff, 0] if motor_bhp == 0.0

  # Lookup the minimum motor efficiency
  motors_table = @standards_data['motors']

  # Assuming all fan motors are 4-pole ODP
  motor_use = 'FAN'
  motor_type = ''
  if (fan.class.name == 'OpenStudio::Model::FanConstantVolume') || (fan.class.name == 'OpenStudio::Model::FanOnOff')
    motor_type = 'CONSTANT'
  elsif fan.class.name == 'OpenStudio::Model::FanVariableVolume'
    # Is this a return or supply fan
    if fan.name.to_s.include?('Supply')
      motor_type += 'VARIABLE-SUPPLY'
    elsif fan.name.to_s.include?('Return')
      motor_type += 'VARIABLE-RETURN'
    end
    # 0.909 corrects for 10% over sizing implemented upstream
    # 0.7457 is to convert from bhp to kW
    fan_power_kw = 0.909 * 0.7457 * motor_bhp
    power_vs_flow_curve_name = if fan_power_kw >= 25.0
                                 'VarVolFan-FCInletVanes-NECB2011-FPLR'
                               elsif fan_power_kw >= 7.5 && fan_power_kw < 25
                                 'VarVolFan-AFBIInletVanes-NECB2011-FPLR'
                               else
                                 'VarVolFan-AFBIFanCurve-NECB2011-FPLR'
                               end
    power_vs_flow_curve = model_add_curve(fan.model, power_vs_flow_curve_name)
    fan.setFanPowerMinimumFlowRateInputMethod('Fraction')
    fan.setFanPowerCoefficient5(0.0)
    fan.setFanPowerMinimumFlowFraction(power_vs_flow_curve.minimumValueofx)
    fan.setFanPowerCoefficient1(power_vs_flow_curve.coefficient1Constant)
    fan.setFanPowerCoefficient2(power_vs_flow_curve.coefficient2x)
    fan.setFanPowerCoefficient3(power_vs_flow_curve.coefficient3xPOW2)
    fan.setFanPowerCoefficient4(power_vs_flow_curve.coefficient4xPOW3)
  elsif fan.class.name == 'OpenStudio::Model::FanZoneExhaust'
    motor_type = 'CONSTANT-RETURN'
  else
    raise('')
  end

  search_criteria = {
    'motor_use' => motor_use,
    'motor_type' => motor_type,
    'number_of_poles' => 4.0,
    'type' => 'Enclosed'
  }

  # Exception for small fans, including
  # zone exhaust, fan coil, and fan powered terminals.
  # In this case, use the 0.5 HP for the lookup.
  if fan_small_fan?(fan)
    nominal_hp = 0.5
  else
    motor_properties = model_find_object(motors_table, search_criteria, motor_bhp)
    if motor_properties.nil?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Fan', "For #{fan.name}, could not find motor properties using search criteria: #{search_criteria}, motor_bhp = #{motor_bhp} hp.")
      return [fan_motor_eff, nominal_hp]
    end

    nominal_hp = motor_properties['maximum_capacity'].to_f.round(1)
    # If the biggest fan motor size is hit, use the highest category efficiency
    if nominal_hp == 9999.0
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Fan', "For #{fan.name}, there is no greater nominal HP.  Use the efficiency of the largest motor category.")
      nominal_hp = motor_bhp
    end

    # Round to nearest whole HP for niceness
    if nominal_hp >= 2
      nominal_hp = nominal_hp.round
    end
  end

  # Get the efficiency based on the nominal horsepower
  # Add 0.01 hp to avoid search errors.
  motor_properties = model_find_object(motors_table, search_criteria, nominal_hp + 0.01)

  if motor_properties.nil?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Fan', "For #{fan.name}, could not find nominal motor properties using search criteria: #{search_criteria}, motor_hp = #{nominal_hp} hp.")
    return [fan_motor_eff, nominal_hp]
  end
  fan_motor_eff = motor_properties['nominal_full_load_efficiency']

  return [fan_motor_eff, nominal_hp]
end

#fan_variable_volume_apply_prototype_fan_pressure_rise(fan_variable_volume) ⇒ Object

Sets the fan pressure rise based on the Prototype buildings inputs which are governed by the flow rate coming through the fan and whether the fan lives inside a unit heater, PTAC, etc.



1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1539

def fan_variable_volume_apply_prototype_fan_pressure_rise(fan_variable_volume)
  # 1000 Pa for supply fan and 458.33 Pa for return fan (accounts for efficiency differences between two fans)
  if fan_variable_volume.name.to_s.include?('Supply')
    sfan_deltaP = get_standards_constant('supply_fan_variable_volume_pressure_rise_value')
    fan_variable_volume.setPressureRise(sfan_deltaP)
  elsif fan_variable_volume.name.to_s.include?('Return')
    rfan_deltaP = get_standards_constant('return_fan_variable_volume_pressure_rise_value')
    fan_variable_volume.setPressureRise(rfan_deltaP)
  end
  return true
end

#fan_variable_volume_part_load_fan_power_limitation?(fan_variable_volume) ⇒ Boolean

Determines whether there is a requirement to have a VSD or some other method to reduce fan power at low part load ratios.

Returns:

  • (Boolean)


1499
1500
1501
1502
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1499

def fan_variable_volume_part_load_fan_power_limitation?(fan_variable_volume)
  part_load_control_required = false
  return part_load_control_required
end

#find_mech_room(model) ⇒ Object

This method determines where the mechanical room is in a building. Mechanical rooms are assumed to be conditioned spaces that are not plenums. It goes through all of the spaces in a model ignoring unconditioned spaces and plenums. It then determines the floor of the space, and the centroid of the floor. It accumulates space floor area*floor centroid information and total floor area. It uses this to determine the centroid of the building. It also notes if the space has a space type with ‘Electrical/Mechanical’ in it. If it does then it assumes that this is a mechanical room and tracks it separately. Once it has gone through all of the spaces it determines if any mechanical rooms were found. If some were found it finds the lowest one in the building (if more than one) and returns that along with all of the conditioned, non-plenum, spaces. If none were found then it searches for the lowest space in the building closest to the building centroid and returns that as the centroid (along with eth conditioned, non-plenum, spaces).



582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 582

def find_mech_room(model)
  cond_spaces = []
  total_peak_flow = 0
  mech_rooms = []
  check_spaces = nil
  building_centre = Array.new(3, 0)
  total_peak_flow = 0
  lowest_space = 1000000000000000
  sp_func_regex = Regexp.new('Space Function')
  mech_regex = Regexp.new('Electrical/Mechanical')
  mech_flag = false
  index = 0
  model.getSpaces.sort.each do |space|
    spaceType_name = space.spaceType.get.nameString
    sp_type = spaceType_name[15..-1]
    # Including regular expressions in the following match for cases where extra characters, which do not belong, are
    # added to either the space type in the model or the space type reference file.
    sp_type_info = @standards_data['tables']['space_types']['table'].detect do |data|
      (Regexp.new(data['space_type'].to_s.upcase).match(sp_type.upcase) || Regexp.new(sp_type.upcase).match(data['space_type'].to_s.upcase) || (data['space_type'].to_s.upcase == sp_type.upcase)) &&
        (data['building_type'].to_s == 'Space Function')
    end
    if sp_type_info.nil?
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.model_add_swh', "The space type called #{sp_type} could not be found.  Please check that the schedules.json file is available and that the space types are spelled correctly")
      next
    end
    # Determine if space is heated or cooled via spacetype heating or cooling setpoints also checking if the space is
    # a plenum by checking if there is a hvac system associtated with it
    if sp_type_info['heating_setpoint_schedule'].nil?
      heated = false
    else
      heated = true
    end
    if sp_type_info['cooling_setpoint_schedule'].nil?
      cooled = false
    else
      cooled = true
    end
    if (sp_type_info['necb_hvac_system_selection_type'] == '- undefined -') || /undefined/.match(sp_type_info['necb_hvac_system_selection_type'])
      not_plenum = false
    else
      not_plenum = true
    end

    # Determine the bottom surface of the space and calculate it's centroid.  Although the mech room is assumed to
    # be in a space that conditioned and is not a plenum (or attic space) all spaces in the building may not be.
    # Doing the following to determine the centroid for the entire building (including unconditioned or plenum
    # spaces).

    # Get the coordinates of the origin for the space (the coordinates of points in the space are relative to this).
    xOrigin = space.xOrigin
    yOrigin = space.yOrigin
    zOrigin = space.zOrigin
    # Get the surfaces for the space.
    space_surfaces = space.surfaces
    # Find the floor (aka the surface with the lowest centroid).
    min_surf = space_surfaces.min_by { |sp_surface| sp_surface.centroid.z.to_f }
    # The following is added to determine the overall floor centroid because some spaces have floors composed of more than one surface.
    floor_centroid = [0, 0, 0]
    space_surfaces.each do |sp_surface|
      if min_surf.centroid.z.to_f == sp_surface.centroid.z.to_f
        floor_centroid[0] = floor_centroid[0] + sp_surface.centroid.x.to_f * sp_surface.grossArea.to_f
        floor_centroid[1] = floor_centroid[1] + sp_surface.centroid.y.to_f * sp_surface.grossArea.to_f
        floor_centroid[2] = floor_centroid[2] + sp_surface.grossArea
      end
    end

    floor_centroid[0] = floor_centroid[0] / floor_centroid[2]
    floor_centroid[1] = floor_centroid[1] / floor_centroid[2]

    if lowest_space > (min_surf.centroid.z.to_f + zOrigin)
      lowest_space = min_surf.centroid.z.to_f + zOrigin
    end
    # This part is used to determine the overall x, y centre of the building.  This is determined by summing the x
    # and y components times the floor area and diving by the total floor area.  This is only for conditioned spaces.
    building_centre[0] += (floor_centroid[0] + xOrigin) * floor_centroid[2]
    building_centre[1] += (floor_centroid[1] + yOrigin) * floor_centroid[2]
    building_centre[2] += (floor_centroid[2])

    # Check if the space is conditioned and not a plenum.  If it is then add it to the list of conditioned, non-plenum,
    # spaces and check if it has a 'Mechanical/Electrical' space type.  Note that the mech room is assumed to
    # be in a space that conditioned and is not a plenum (or attic space).

    if (heated == true || cooled == true) && (not_plenum == true)
      if mech_regex.match(spaceType_name)
        mech_rooms << index
      end
      cond_space = {
        'space_name' => space.nameString,
        'space' => space,
        'space_centroid' => [floor_centroid[0] + xOrigin, floor_centroid[1] + yOrigin, min_surf.centroid.z.to_f + zOrigin],
        'building_cent_dist' => 0
      }
      cond_spaces << cond_space
      index += 1
    end
  end

  return [cond_spaces[mech_rooms[0]], cond_spaces] if mech_rooms.size == 1

  if mech_rooms.size > 1
    check_spaces = []
    lowest_space = 10000000000000
    mech_rooms.each do |mech_room|
      check_spaces << cond_spaces[mech_room]
      if cond_spaces[mech_room]['space_centroid'][2].to_f < lowest_space
        lowest_space = cond_spaces[mech_room]['space_centroid'][2].to_f
      end
    end
  else
    check_spaces = cond_spaces
  end

  # If no heated or cooled spaces were found then return false
  if cond_spaces.empty?
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.model_add_swh', 'No heated or cooled spaces types could be found which were not also a plenum.')
    return false
  end

  # This is where the average happens
  building_centre[0] /= building_centre[2]
  building_centre[1] /= building_centre[2]
  # Go through each space on the lowest floor of the building and determine the distance between the centroid of the
  # space's floors and the center of the building I calculated just above.
  centre_spaces = []
  check_spaces.each do |check_space|
    if check_space['space_centroid'][2] == lowest_space
      check_space['building_cent_dist'] = Math.sqrt(((check_space['space_centroid'][0] - building_centre[0])**2) + ((check_space['space_centroid'][1] - building_centre[1])**2))
      centre_spaces << check_space
    end
  end
  # Determine which of the floor spaces is closest to the centre of the building and that one becomes the location of
  # the mechanical room.
  centre_space = centre_spaces.min_by { |dist| dist['building_cent_dist'].round(1) }
  return [centre_space, cond_spaces]
end

#friction_factor(re_pipe, relative_rough) ⇒ Object



543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 543

def friction_factor(re_pipe, relative_rough)
  # This method determines the Darcy-Weisbach friction factor assuming the pipe is circular and filled.
  if re_pipe <= 2100
    # Laminar flow use the Uagen-Poiseuille equation.  https://neutrium.net/fluid_flow/pressure-loss-in-pipe
    # accessed 2018-07-25.
    f = 64.to_f / re_pipe.to_f
  elsif re_pipe > 2100 && re_pipe <= 4000
    # In the transition flow region I interpolate by Reynolds number between laminar and turbulent regimes.  Yeah, that's
    # crap but if you can come up with something better you are welcome to replace what I have below.
    flam = 64.to_f / 2100.to_f
    pipe_rough_fact = relative_rough / 3.7
    factor_A = -2 * Math.log10(pipe_rough_fact + (12.to_f / 4000.to_f))
    factor_B = -2 * Math.log10(pipe_rough_fact + ((2.51 * factor_A) / 4000))
    factor_C = -2 * Math.log10(pipe_rough_fact + ((2.51 * factor_B) / 4000))
    fturb = 1 / ((factor_A - (((factor_B - factor_A)**2) / (factor_C - 2 * factor_B + factor_A)))**2)
    re_int = (re_pipe - 2100.to_f) / 1900.to_f
    f = ((fturb - flam) * re_int) + flam
  elsif re_pipe > 4000
    # Turbulent flow use Serghide's Equation which I got from https://neutrium.net/fluid_flow/pressure-loss-in-pipe
    # accessed 2018-07-25.  Apparently it is good for 4000 < Re < 1x10^10 and relative roughness between 1x10-7 and 1.
    pipe_rough_fact = relative_rough / 3.7
    factor_A = -2 * Math.log10(pipe_rough_fact + (12 / re_pipe))
    factor_B = -2 * Math.log10(pipe_rough_fact + ((2.51 * factor_A) / re_pipe))
    factor_C = -2 * Math.log10(pipe_rough_fact + ((2.51 * factor_B) / re_pipe))
    f = 1 / ((factor_A - (((factor_B - factor_A)**2) / (factor_C - 2 * factor_B + factor_A)))**2)
  end
  return f
end

#get_all_spacetype_namesObject



145
146
147
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 145

def get_all_spacetype_names
  return @standards_data['space_types'].map { |space_types| [space_types['building_type'], space_types['space_type']] }
end

#get_any_number_ppm(model) ⇒ Object

TODO:

(upon other BTAP tasks) This function can be added to btap/schedules.rb > module StandardScheduleTypeLimits

Define ScheduleTypeLimits for Any_Number_ppm



1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1715

def get_any_number_ppm(model)
  name = 'Any_Number_ppm'
  any_number_ppm_schedule_type_limits = model.getScheduleTypeLimitsByName(name)
  return any_number_ppm_schedule_type_limits.get unless any_number_ppm_schedule_type_limits.empty?

  any_number_ppm_schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
  any_number_ppm_schedule_type_limits.setName(name)
  any_number_ppm_schedule_type_limits.setNumericType('CONTINUOUS')
  any_number_ppm_schedule_type_limits.setUnitType('Dimensionless')
  any_number_ppm_schedule_type_limits.setLowerLimitValue(400.0)
  any_number_ppm_schedule_type_limits.setUpperLimitValue(1000.0)
  return any_number_ppm_schedule_type_limits
end

#get_climate_zone_index(hdd) ⇒ Fixnum

This model gets the climate zone column index from tables 3.2.2.x

Parameters:

  • hdd (Float)

Returns:

  • (Fixnum)

    climate zone 4-8

Author:



1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1307

def get_climate_zone_index(hdd)
  # check for climate zone index from NECB 3.2.2.X
  case hdd
  when 0..2999 then
    return 0 # climate zone 4
  when 3000..3999 then
    return 1 # climate zone 5
  when 4000..4999 then
    return 2 # climate zone 6
  when 5000..5999 then
    return 3 # climate zone 7a
  when 6000..6999 then
    return 4 # climate zone 7b
  when 7000..1000000 then
    return 5 # climate zone 8
  end
end

#get_climate_zone_name(hdd) ⇒ Fixnum

This model gets the climate zone name and returns the climate zone string.

Parameters:

  • hdd (Float)

Returns:

  • (Fixnum)

    climate zone 4-8

Author:



1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1329

def get_climate_zone_name(hdd)
  case get_climate_zone_index(hdd)
  when 0 then
    return '4'
  when 1 then
    return '5' # climate zone 5
  when 2 then
    return '6' # climate zone 6
  when 3 then
    return '7a' # climate zone 7a
  when 4 then
    return '7b' # climate zone 7b
  when 5 then
    return '8' # climate zone 8
  end
end

#get_max_space_height_for_space_type(space_type:) ⇒ Object

This method is needed to the space_type_apply_internal_loads space needed for the calculation of atriums’ LPD when LED lighting is used in atriums. ***END***



1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1926

def get_max_space_height_for_space_type(space_type:)
  # initialize return value to zero.
  max_space_height_for_space_type = 0.0
  # Interate through all spaces in model.. not just ones that have space type defined.. Is this right sara?
  space_type.spaces.sort.each do |space|
    # Get only the wall type surfaces and iterate throught them.
    space.surfaces.sort.select(&:surfaceType == 'Wall').each do |wall_surface|
      # Find the vertex with the max z value.
      vertex_with_max_height = wall_surface.vertices.max_by(&:z)
      # replace max if this surface has something bigger.
      max_space_height_for_space_type = vertex_with_max_height.z if vertex_with_max_height.z > max_space_height_for_space_type
    end
  end
  return max_space_height_for_space_type
end

#get_necb_hdd18(model:, necb_hdd: true) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 166

def get_necb_hdd18(model:, necb_hdd: true)
  max_distance_tolerance = 500000
  min_distance = 100000000000000.0
  necb_closest = nil
  weather_file_path = model.weatherFile.get.path.get.to_s
  epw_file = model.weatherFile.get.file.get
  stat_file_path = weather_file_path.gsub('.epw', '.stat')
  stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
  # If necb_hdd is false use the information in the .stat file associated with the.epw file.
  unless necb_hdd
    return stat_file.hdd18
  end
  # this extracts the table from the json database.
  necb_2015_table_c1 = @standards_data['tables']['necb_2015_table_c1']['table']
  necb_2015_table_c1.each do |necb|
    next if necb['lat_long'].nil? # Need this until Tyson cleans up table.

    dist = distance([epw_file.latitude, epw_file.longitude], necb['lat_long'])
    if min_distance > dist
      min_distance = dist
      necb_closest = necb
    end
  end
  if ((min_distance / 1000.0) > max_distance_tolerance) && !stat_file.hdd18.nil?
    puts "Could not find close NECB HDD from Table C1 < #{max_distance_tolerance}km. Closest city is #{min_distance / 1000.0}km away. Using epw hdd18 instead."
    return stat_file.hdd18
  else
    dist_clause = "%.2f % #{(min_distance / 1000.0)}"
    puts "INFO:NECB HDD18 of #{necb_closest['degree_days_below_18_c'].to_f}  at nearest city #{necb_closest['city']},#{necb_closest['province']}, at a distance of " + dist_clause + 'km from epw location. Ref: nbc_2015_table_c1'
    return necb_closest['degree_days_below_18_c'].to_f
  end
end

#get_necb_spacetype_system_selection(space) ⇒ Object

Determines what system index number is required for the space’s spacetype by NECB rules.



682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 682

def get_necb_spacetype_system_selection(space)
  space_type_table = @standards_data['space_types']
  space_type_data = model_find_object(space_type_table, 'space_type' => space.spaceType.get.standardsSpaceType.get,
                                                        'building_type' => space.spaceType.get.standardsBuildingType.get)
  if space_type_data.nil?
    raise("Could not find space_type_data for #{{ 'space_type' => space.spaceType.get.standardsSpaceType.get,
                                                  'building_type' => space.spaceType.get.standardsBuildingType.get }} ")
  end

  # identify space-system_index and assign the right NECB system type 1-7.
  necb_hvac_system_selection_table = @standards_data['necb_hvac_system_selection_type']
  necb_hvac_system_select = necb_hvac_system_selection_table.detect do |curr_necb_hvac_system_select|
    curr_necb_hvac_system_select['necb_hvac_system_selection_type'] == space_type_data['necb_hvac_system_selection_type'] &&
      curr_necb_hvac_system_select['min_stories'] <= space.model.getBuilding.standardsNumberOfAboveGroundStories.get &&
      curr_necb_hvac_system_select['max_stories'] >= space.model.getBuilding.standardsNumberOfAboveGroundStories.get &&
      curr_necb_hvac_system_select['min_cooling_capacity_kw'] <= stored_space_cooling_load(space) &&
      curr_necb_hvac_system_select['max_cooling_capacity_kw'] >= stored_space_cooling_load(space)
  end
  raise('could not find system for given spacetype') if necb_hvac_system_select.nil?

  return necb_hvac_system_select['system_type']
end

#get_necb_thermal_zone_system_selection(tz) ⇒ Object

Determines what system index number is required for the thermal zone based on the spacetypes it contains



706
707
708
709
710
711
712
713
714
715
716
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 706

def get_necb_thermal_zone_system_selection(tz)
  systems = []
  tz.spaces.each do |space|
    systems << get_necb_spacetype_system_selection(space)
  end
  systems.uniq!
  systems.compact!
  raise('This thermal zone spaces require different systems.') if systems.size > 1

  return systems.first
end

#get_parameters_sidelighting(daylight_space:, floor_surface:, floor_vertices:, floor_area:, primary_sidelighted_area:, area_weighted_vt_handle:, window_area_sum:) ⇒ Object

The below method is for the ‘model_add_daylighting_controls’ method



1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1943

def get_parameters_sidelighting(daylight_space:, floor_surface:, floor_vertices:, floor_area:, primary_sidelighted_area:, area_weighted_vt_handle:, window_area_sum:)
  daylight_space.surfaces.sort.each do |surface|
    ##### Get the vertices of each exterior wall of the daylight_space on the floor
    ##### (these vertices will be used to calculate daylight_space depth in relation to the exterior wall, and
    ##### the distance of the window to vertical walls on each side of the window)
    if surface.outsideBoundaryCondition == 'Outdoors' && surface.surfaceType == 'Wall'
      wall_vertices_x_on_floor = []
      wall_vertices_y_on_floor = []
      surface_z_min = [surface.vertices[0].z, surface.vertices[1].z, surface.vertices[2].z, surface.vertices[3].z].min
      surface.vertices.each do |vertex|
        # puts vertex.z
        if vertex.z == surface_z_min && surface_z_min == floor_vertices[0][0].z
          wall_vertices_x_on_floor << vertex.x
          wall_vertices_y_on_floor << vertex.y
        end
      end
    end

    if surface.outsideBoundaryCondition == 'Outdoors' && surface.surfaceType == 'Wall' && surface_z_min == floor_vertices[0][0].z

      ##### Calculate the daylight_space depth in relation to the considered exterior wall.
      ##### To calculate daylight_space depth, first get the floor vertices which are on the opposite side of the considered exterior wall.
      floor_vertices_x_wall_opposite = []
      floor_vertices_y_wall_opposite = []
      floor_vertices[0].each do |floor_vertex|
        if (floor_vertex.x != wall_vertices_x_on_floor[0] && floor_vertex.y != wall_vertices_y_on_floor[0]) || (floor_vertex.x != wall_vertices_x_on_floor[1] && floor_vertex.y != wall_vertices_y_on_floor[1])
          floor_vertices_x_wall_opposite << floor_vertex.x
          floor_vertices_y_wall_opposite << floor_vertex.y
        end
      end

      ##### To calculate daylight_space depth, second calculate floor length on both sides: (1) exterior wall side, (2) on the opposite side of the exterior wall
      floor_width_wall_side = Math.sqrt((wall_vertices_x_on_floor[0] - wall_vertices_x_on_floor[1])**2 + (wall_vertices_y_on_floor[0] - wall_vertices_y_on_floor[1])**2)
      floor_width_wall_opposite = Math.sqrt((floor_vertices_x_wall_opposite[0] - floor_vertices_x_wall_opposite[1])**2 + (floor_vertices_y_wall_opposite[0] - floor_vertices_y_wall_opposite[1])**2)

      ##### Now, daylight_space depth can be calculated using the floor area and two lengths of the floor (note that these two lengths are in parallel to each other).
      daylight_space_depth = 2 * floor_area / (floor_width_wall_side + floor_width_wall_opposite)

      ##### Loop through the windows (including fixed and operable ones) to get window specification (width, height, area, visible transmittance (VT)), and area-weighted VT
      surface.subSurfaces.sort.each do |subsurface|
        # puts subsurface.name.to_s
        if subsurface.subSurfaceType == 'FixedWindow' || subsurface.subSurfaceType == 'OperableWindow'
          window_vt = subsurface.visibleTransmittance
          window_vt = window_vt.get
          window_area = subsurface.netArea
          window_area_sum += window_area
          area_weighted_vt_handle += window_area * window_vt
          window_vertices = subsurface.vertices
          if window_vertices[0].z.round(2) == window_vertices[1].z.round(2)
            window_width = Math.sqrt((window_vertices[0].x - window_vertices[1].x)**2.0 + (window_vertices[0].y - window_vertices[1].y)**2.0)
          else
            window_width = Math.sqrt((window_vertices[1].x - window_vertices[2].x)**2.0 + (window_vertices[1].y - window_vertices[2].y)**2.0)
          end
          window_head_height = [window_vertices[0].z, window_vertices[1].z, window_vertices[2].z, window_vertices[3].z].max.round(2)
          primary_sidelighted_area_depth = [window_head_height, daylight_space_depth].min # as per NECB2011: 4.2.2.9.

          ##### Calculate the  distance of the window to vertical walls on each side of the window (this is used to determine the sidelighted area's width).
          window_vertices_on_floor = []
          window_vertices.each do |vertex|
            window_vertices_on_floor << floor_surface.plane.project(vertex)
          end
          window_wall_distance_side1 = [Math.sqrt((wall_vertices_x_on_floor[0] - window_vertices_on_floor[0].x)**2.0 + (wall_vertices_y_on_floor[0] - window_vertices_on_floor[0].y)**2.0),
                                        Math.sqrt((wall_vertices_x_on_floor[0] - window_vertices_on_floor[2].x)**2.0 + (wall_vertices_y_on_floor[0] - window_vertices_on_floor[2].y)**2.0),
                                        0.6].min # 0.6 m as per NECB2011: 4.2.2.9.
          window_wall_distance_side2 = [Math.sqrt((wall_vertices_x_on_floor[1] - window_vertices_on_floor[0].x)**2.0 + (wall_vertices_y_on_floor[1] - window_vertices_on_floor[0].y)**2.0),
                                        Math.sqrt((wall_vertices_x_on_floor[1] - window_vertices_on_floor[2].x)**2.0 + (wall_vertices_y_on_floor[1] - window_vertices_on_floor[2].y)**2.0),
                                        0.6].min # 0.6 m as per NECB2011: 4.2.2.9.
          primary_sidelighted_area_width = window_wall_distance_side1 + window_width + window_wall_distance_side2
          primary_sidelighted_area += primary_sidelighted_area_depth * primary_sidelighted_area_width
          # if subsurface.subSurfaceType == "FixedWindow" || subsurface.subSurfaceType == "OperableWindow"
        end
        # surface.subSurfaces.each do |subsurface|
      end
      # if surface.outsideBoundaryCondition == "Outdoors" && surface.surfaceType == "Wall" && surface_z_min == floor_vertices[0][0].z
    end
    # daylight_space.surfaces.each do |surface|
  end

  return primary_sidelighted_area, area_weighted_vt_handle, window_area_sum
end

#get_parameters_skylight(daylight_space:, skylight_area_weighted_vt_handle:, skylight_area_sum:, daylighted_under_skylight_area:) ⇒ Object

The below method is for the ‘model_add_daylighting_controls’ method



2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2025

def get_parameters_skylight(daylight_space:, skylight_area_weighted_vt_handle:, skylight_area_sum:, daylighted_under_skylight_area:)
  daylight_space.surfaces.sort.each do |surface|
    ##### Get roof vertices
    if surface.outsideBoundaryCondition == 'Outdoors' && surface.surfaceType == 'RoofCeiling'
      roof_vertices = surface.vertices
    end

    ##### Loop through each subsurface to calculate daylighted_area_under_skylights and skylight_effective_aperture for each daylight_space
    surface.subSurfaces.sort.each do |subsurface|
      if subsurface.subSurfaceType == 'Skylight'
        skylight_vt = subsurface.visibleTransmittance
        skylight_vt = skylight_vt.get
        skylight_area = subsurface.netArea
        skylight_area_sum += skylight_area
        skylight_area_weighted_vt_handle += skylight_area * skylight_vt

        ##### Get skylight vertices
        skylight_vertices = subsurface.vertices

        ##### Calculate skylight width and height
        skylight_width = Math.sqrt((skylight_vertices[0].x - skylight_vertices[1].x)**2.0 + (skylight_vertices[0].y - skylight_vertices[1].y)**2.0)
        skylight_length = Math.sqrt((skylight_vertices[0].x - skylight_vertices[3].x)**2.0 + (skylight_vertices[0].y - skylight_vertices[3].y)**2.0)

        ##### Get ceiling height assuming the skylight is flush with the ceiling
        ceiling_height = skylight_vertices[0].z

        ##### Calculate roof lengths
        ##### (Note: used OpenStudio BCL measure called "assign_ashrae_9012010_daylighting_controls" with some changes/correcctions)
        roof_length_0 = Math.sqrt((roof_vertices[0].x - roof_vertices[1].x)**2.0 + (roof_vertices[0].y - roof_vertices[1].y)**2.0)
        roof_length_1 = Math.sqrt((roof_vertices[1].x - roof_vertices[2].x)**2.0 + (roof_vertices[1].y - roof_vertices[2].y)**2.0)
        roof_length_2 = Math.sqrt((roof_vertices[2].x - roof_vertices[3].x)**2.0 + (roof_vertices[2].y - roof_vertices[3].y)**2.0)
        roof_length_3 = Math.sqrt((roof_vertices[3].x - roof_vertices[0].x)**2.0 + (roof_vertices[3].y - roof_vertices[0].y)**2.0)

        ##### Find the skylight point that is the closest one to roof_vertex_0
        ##### (Note: used OpenStudio BCL measure called "assign_ashrae_9012010_daylighting_controls" with some changes/correcctions)
        roof_vertex_0_skylight_vertex_0 = Math.sqrt((roof_vertices[0].x - skylight_vertices[0].x)**2.0 + (roof_vertices[0].y - skylight_vertices[0].y)**2.0)
        roof_vertex_0_skylight_vertex_1 = Math.sqrt((roof_vertices[0].x - skylight_vertices[1].x)**2.0 + (roof_vertices[0].y - skylight_vertices[1].y)**2.0)
        roof_vertex_0_skylight_vertex_2 = Math.sqrt((roof_vertices[0].x - skylight_vertices[2].x)**2.0 + (roof_vertices[0].y - skylight_vertices[2].y)**2.0)
        roof_vertex_0_skylight_vertex_3 = Math.sqrt((roof_vertices[0].x - skylight_vertices[3].x)**2.0 + (roof_vertices[0].y - skylight_vertices[3].y)**2.0)
        roof_vertex_0_closest_distance = [roof_vertex_0_skylight_vertex_0, roof_vertex_0_skylight_vertex_1, roof_vertex_0_skylight_vertex_2, roof_vertex_0_skylight_vertex_3].min
        if roof_vertex_0_closest_distance == roof_vertex_0_skylight_vertex_0
          roof_vertex_0_closest_point = skylight_vertices[0]
        elsif roof_vertex_0_closest_distance == roof_vertex_0_skylight_vertex_1
          roof_vertex_0_closest_point = skylight_vertices[1]
        elsif roof_vertex_0_closest_distance == roof_vertex_0_skylight_vertex_2
          roof_vertex_0_closest_point = skylight_vertices[2]
        elsif roof_vertex_0_closest_distance == roof_vertex_0_skylight_vertex_3
          roof_vertex_0_closest_point = skylight_vertices[3]
        end

        ##### Find the skylight point that is the closest one to roof_vertex_2
        ##### (Note: used OpenStudio BCL measure called "assign_ashrae_9012010_daylighting_controls" with some changes/correcctions)
        roof_vertex_2_skylight_vertex_0 = Math.sqrt((roof_vertices[2].x - skylight_vertices[0].x)**2.0 + (roof_vertices[2].y - skylight_vertices[0].y)**2.0)
        roof_vertex_2_skylight_vertex_1 = Math.sqrt((roof_vertices[2].x - skylight_vertices[1].x)**2.0 + (roof_vertices[2].y - skylight_vertices[1].y)**2.0)
        roof_vertex_2_skylight_vertex_2 = Math.sqrt((roof_vertices[2].x - skylight_vertices[2].x)**2.0 + (roof_vertices[2].y - skylight_vertices[2].y)**2.0)
        roof_vertex_2_skylight_vertex_3 = Math.sqrt((roof_vertices[2].x - skylight_vertices[3].x)**2.0 + (roof_vertices[2].y - skylight_vertices[3].y)**2.0)
        roof_vertex_2_closest_distance = [roof_vertex_2_skylight_vertex_0, roof_vertex_2_skylight_vertex_1, roof_vertex_2_skylight_vertex_2, roof_vertex_2_skylight_vertex_3].min
        if roof_vertex_2_closest_distance == roof_vertex_2_skylight_vertex_0
          roof_vertex_2_closest_point = skylight_vertices[0]
        elsif roof_vertex_2_closest_distance == roof_vertex_2_skylight_vertex_1
          roof_vertex_2_closest_point = skylight_vertices[1]
        elsif roof_vertex_2_closest_distance == roof_vertex_2_skylight_vertex_2
          roof_vertex_2_closest_point = skylight_vertices[2]
        elsif roof_vertex_2_closest_distance == roof_vertex_2_skylight_vertex_3
          roof_vertex_2_closest_point = skylight_vertices[3]
        end

        ##### Calculate the vertical distance from the closest skylight points (projection onto the roof) to the wall (projection onto the roof) for roof_vertex_0 and roof_vertex_2
        ##### (Note: used OpenStudio BCL measure called "assign_ashrae_9012010_daylighting_controls" with some changes/correcctions)
        ##### For the calculation of each vertical distance: (1) first the area of the triangle is calculated knowing the cooridantes of its three corners;
        ##### (2) the vertical distance (i.e. triangle height) is calculated knowing the triangle area and the associated roof length.
        rv_0_triangle_0_area = 0.5 * (((roof_vertex_0_closest_point.x - roof_vertices[1].x) * (roof_vertex_0_closest_point.y - roof_vertices[0].y)) -
            ((roof_vertex_0_closest_point.x - roof_vertices[0].x) * (roof_vertex_0_closest_point.y - roof_vertices[1].y))).abs
        rv_0_distance_0 = (2.0 * rv_0_triangle_0_area) / roof_length_0
        rv_0_triangle_3_area = 0.5 * (((roof_vertex_0_closest_point.x - roof_vertices[3].x) * (roof_vertex_0_closest_point.y - roof_vertices[0].y)) -
            ((roof_vertex_0_closest_point.x - roof_vertices[0].x) * (roof_vertex_0_closest_point.y - roof_vertices[3].y))).abs
        rv_0_distance_3 = (2.0 * rv_0_triangle_3_area) / roof_length_3

        rv_2_triangle_1_area = 0.5 * (((roof_vertex_2_closest_point.x - roof_vertices[1].x) * (roof_vertex_2_closest_point.y - roof_vertices[2].y)) -
            ((roof_vertex_2_closest_point.x - roof_vertices[2].x) * (roof_vertex_2_closest_point.y - roof_vertices[1].y))).abs
        rv_2_distance_1 = (2.0 * rv_2_triangle_1_area) / roof_length_1
        rv_2_triangle_2_area = 0.5 * (((roof_vertex_2_closest_point.x - roof_vertices[3].x) * (roof_vertex_2_closest_point.y - roof_vertices[2].y)) -
            ((roof_vertex_2_closest_point.x - roof_vertices[2].x) * (roof_vertex_2_closest_point.y - roof_vertices[3].y))).abs
        rv_2_distance_2 = (2.0 * rv_2_triangle_2_area) / roof_length_2

        ##### Set the vertical distances from the closest skylight points (projection onto the roof) to the wall (projection onto the roof) for roof_vertex_0 and roof_vertex_2
        distance_1 = rv_0_distance_0
        distance_2 = rv_0_distance_3
        distance_3 = rv_2_distance_1
        distance_4 = rv_2_distance_2

        ##### Calculate the width and length of the daylighted area under the skylight as per NECB2011: 4.2.2.5.
        ##### Note: In the below loops, if any exterior walls has window(s), the width and length of the daylighted area under the skylight are re-calculated as per NECB2011: 4.2.2.5.
        daylighted_under_skylight_width = skylight_width + [0.7 * ceiling_height, distance_1].min + [0.7 * ceiling_height, distance_4].min
        daylighted_under_skylight_length = skylight_length + [0.7 * ceiling_height, distance_2].min + [0.7 * ceiling_height, distance_3].min

        ##### As noted above, the width and length of the daylighted area under the skylight are re-calculated (as per NECB2011: 4.2.2.5.), if any exterior walls has window(s).
        ##### To this end, the window_head_height should be calculated, as below:
        daylight_space.surfaces.sort.each do |curr_surface|
          if curr_surface.outsideBoundaryCondition == 'Outdoors' && curr_surface.surfaceType == 'Wall'
            wall_vertices_on_floor_x = []
            wall_vertices_on_floor_y = []
            wall_vertices = curr_surface.vertices
            if wall_vertices[0].z == wall_vertices[1].z
              wall_vertices_on_floor_x << wall_vertices[0].x
              wall_vertices_on_floor_x << wall_vertices[1].x
              wall_vertices_on_floor_y << wall_vertices[0].y
              wall_vertices_on_floor_y << wall_vertices[1].y
            elsif wall_vertices[0].z == wall_vertices[3].z
              wall_vertices_on_floor_x << wall_vertices[0].x
              wall_vertices_on_floor_x << wall_vertices[3].x
              wall_vertices_on_floor_y << wall_vertices[0].y
              wall_vertices_on_floor_y << wall_vertices[3].y
            end
            window_vertices = subsurface.vertices
            window_head_height = [window_vertices[0].z, window_vertices[1].z, window_vertices[2].z, window_vertices[3].z].max.round(2)

            ##### Calculate the exterior wall length (on the floor)
            exterior_wall_length = Math.sqrt((wall_vertices_on_floor_x[0] - wall_vertices_on_floor_x[1])**2.0 + (wall_vertices_on_floor_y[0] - wall_vertices_on_floor_y[1])**2.0)

            ##### Calculate the vertical distance of skylight_vertices[0] projection onto the roof/floor to the exterior wall
            skylight_vertex_0_triangle_area = 0.5 * (((wall_vertices_on_floor_x[0] - wall_vertices_on_floor_x[1]) * (wall_vertices_on_floor_y[0] - skylight_vertices[0].y)) -
                ((wall_vertices_on_floor_x[0] - skylight_vertices[0].x) * (wall_vertices_on_floor_y[0] - wall_vertices_on_floor_y[1]))).abs
            skylight_vertex_0_distance = (2.0 * skylight_vertex_0_triangle_area) / exterior_wall_length

            ##### Calculate the vertical distance of skylight_vertices[1] projection onto the roof/floor to the exterior wall
            skylight_vertex_1_triangle_area = 0.5 * (((wall_vertices_on_floor_x[0] - wall_vertices_on_floor_x[1]) * (wall_vertices_on_floor_y[0] - skylight_vertices[1].y)) -
                ((wall_vertices_on_floor_x[0] - skylight_vertices[1].x) * (wall_vertices_on_floor_y[0] - wall_vertices_on_floor_y[1]))).abs
            skylight_vertex_1_distance = (2.0 * skylight_vertex_1_triangle_area) / exterior_wall_length

            ##### Calculate the vertical distance of skylight_vertices[3] projection onto the roof/floor to the exterior wall
            skylight_vertex_3_triangle_area = 0.5 * (((wall_vertices_on_floor_x[0] - wall_vertices_on_floor_x[1]) * (wall_vertices_on_floor_y[0] - skylight_vertices[3].y)) -
                ((wall_vertices_on_floor_x[0] - skylight_vertices[3].x) * (wall_vertices_on_floor_y[0] - wall_vertices_on_floor_y[1]))).abs
            skylight_vertex_3_distance = (2.0 * skylight_vertex_3_triangle_area) / exterior_wall_length

            ##### Loop through the subsurfaces that has exterior windows to re-calculate the width and length of the daylighted area under the skylight
            curr_surface.subSurfaces.sort.each do |curr_subsurface|
              if curr_subsurface.subSurfaceType == 'FixedWindow' || curr_subsurface.subSurfaceType == 'OperableWindow'

                if skylight_vertex_0_distance == skylight_vertex_1_distance # skylight_01 is in parellel to the exterior wall
                  if skylight_vertex_0_distance.round(2) == distance_2.round(2)
                    daylighted_under_skylight_length = skylight_length + [0.7 * ceiling_height, distance_2, distance_2 - window_head_height].min + [0.7 * ceiling_height, distance_3].min
                  elsif skylight_vertex_0_distance.round(2) == distance_3.round(2)
                    daylighted_under_skylight_length = skylight_length + [0.7 * ceiling_height, distance_2].min + [0.7 * ceiling_height, distance_3, distance_3 - window_head_height].min
                  end
                elsif skylight_vertex_0_distance == skylight_vertex_3_distance # skylight_03 is in parellel to the exterior wall
                  if skylight_vertex_0_distance.round(2) == distance_1.round(2)
                    daylighted_under_skylight_width = skylight_width + [0.7 * ceiling_height, distance_1, distance_1 - window_head_height].min + [0.7 * ceiling_height, distance_4].min
                  elsif skylight_vertex_0_distance.round(2) == distance_4.round(2)
                    daylighted_under_skylight_width = skylight_width + [0.7 * ceiling_height, distance_1].min + [0.7 * ceiling_height, distance_4, distance_4 - window_head_height].min
                  end
                  # if skylight_vertex_0_distance == skylight_vertex_1_distance
                end

                daylighted_under_skylight_area += daylighted_under_skylight_length * daylighted_under_skylight_width
                # if subsurface.subSurfaceType == "FixedWindow" || subsurface.subSurfaceType == "OperableWindow"
              end
              # surface.subSurfaces.each do |subsurface|
            end
            # if surface.outsideBoundaryCondition == "Outdoors" && surface.surfaceType == "Wall"
          end
          # daylight_space.surfaces.each do |surface|
        end
        # if subsurface.subSurfaceType == "Skylight"
      end
      # surface.subSurfaces.each do |subsurface|
    end
    # daylight_space.surfaces.each do |surface|
  end

  return daylighted_under_skylight_area, skylight_area_weighted_vt_handle, skylight_area_sum
end

#get_qaqc_table(table_name:, search_criteria: nil) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 140

def get_qaqc_table(table_name:, search_criteria: nil)
  return_objects = nil
  table = @qaqc_data['tables'][table_name]
  raise("could not find #{table_name} in qaqc table database. ") if table.nil?

  return table if search_criteria.nil? # removed table beause need to use the object['refs']

  rows = table['table']
  search_criteria.each do |key, value|
    rows = rows.select { |row| row[key] == value }
  end
  return rows
end

#get_sql_table_to_json(model, report_name, report_for_string, table_name) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 4

def get_sql_table_to_json(model, report_name, report_for_string, table_name)
  table = []
  query_row_names = "
   SELECT DISTINCT
      RowName
   FROM
      tabulardatawithstrings
    WHERE
      ReportName='#{report_name}'
    AND
      ReportForString='#{report_for_string}'
    AND
      TableName='#{table_name}'"
  row_names = model.sqlFile.get.execAndReturnVectorOfString(query_row_names).get

  # get Columns
  query_col_names = "
   SELECT DISTINCT
      ColumnName
   FROM tabulardatawithstrings
    WHERE ReportName='#{report_name}'
    AND ReportForString='#{report_for_string}'
    AND TableName='#{table_name}'"
  col_names = model.sqlFile.get.execAndReturnVectorOfString(query_col_names).get

  # get units
  query_unit_names = "
   SELECT DISTINCT
      Units
   FROM tabulardatawithstrings
    WHERE ReportName='#{report_name}'
    AND ReportForString='#{report_for_string}'
    AND TableName='#{table_name}'"
  unit_names = model.sqlFile.get.execAndReturnVectorOfString(query_unit_names).get

  row_names.each do |row|
    next if row.nil? || row == ''

    row_hash = {}
    row_hash[:name] = row
    col_names.each do |col|
      unit_names.each do |unit|
        query = "
      SELECT
        Value
      FROM
        tabulardatawithstrings
      WHERE
        ReportName='#{report_name}'
      AND
        ReportForString='#{report_for_string}'
      AND
        TableName='#{table_name}'
      AND
        RowName='#{row}'
      AND
        ColumnName='#{col}'
      AND
        Units='#{unit}'
"
        column_name = col.to_s.gsub(/\s+/, '_').downcase
        # If the column name is "additional_fuel" and the file contains a boiler with a FuelOilNo2 fuel type assume
        # the column name should be "fueloilno2".
        if column_name.include? 'additional_fuel'
          model.getPlantLoops.sort.each do |iplantloop|
            boilers = iplantloop.components.select { |icomponent| icomponent.to_BoilerHotWater.is_initialized }
            column_name = 'fueloilno2' unless boilers.select { |boiler| boiler.to_BoilerHotWater.get.fuelType.to_s == 'FuelOilNo2' }.empty?
          end
        end
        column_name += "_#{unit}" if unit != ''
        value = model.sqlFile.get.execAndReturnFirstString(query)
        next if value.empty? || value.get.nil?

        value = value.get.strip
        # check is value is a number
        if (begin
              Float(value)
            rescue StandardError
              false
            end) && value.to_f != 0
          row_hash[column_name] = value.to_f
          # Check if value is a date
        elsif unit == '' && value =~ /\d\d-\D\D\D-\d\d:\d\d/
          row_hash[column_name] = DateTime.parse(value)
          # skip if value in an empty string or a zero value
        elsif value != '' && value != '0.00'
          row_hash[column_name] = value
        end
      end
    end
    if row_hash.size > 1
      table << row_hash
    end
  end
  result = { report_name: report_name, report_for_string: report_for_string, table_name: table_name, table: table }
  return result
end

#get_sql_tables_to_json(model) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 106

def get_sql_tables_to_json(model)
  sql_data = []
  sql_data << get_sql_table_to_json(model, 'AnnualBuildingUtilityPerformanceSummary', 'Entire Facility', 'End Uses')
  sql_data << get_sql_table_to_json(model, 'AnnualBuildingUtilityPerformanceSummary', 'Entire Facility', 'Site and Source Energy')
  # sql_data << get_sql_table_to_json(model, "AnnualBuildingUtilityPerformanceSummary", "Entire Facility", "On-Site Thermal Sources")
  # sql_data << get_sql_table_to_json(model, "AnnualBuildingUtilityPerformanceSummary", "Entire Facility", "Comfort and Setpoint Not Met Summary")
  # sql_data << get_sql_table_to_json(model, "InputVerificationandResultsSummary", "Entire Facility", "Window-Wall Ratio")
  # sql_data << get_sql_table_to_json(model, "InputVerificationandResultsSummary", "Entire Facility", "Conditioned Window-Wall Ratio")
  # sql_data << get_sql_table_to_json(model, "InputVerificationandResultsSummary", "Entire Facility", "Skylight-Roof Ratio")
  # sql_data << get_sql_table_to_json(model, "DemandEndUseComponentsSummary", "Entire Facility", "End Uses")
  # sql_data << get_sql_table_to_json(model, "ComponentSizingSummary", "Entire Facility", "AirLoopHVAC")
  return sql_data
end

#get_standard_constant_value(constant_name:) ⇒ Object



46
47
48
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 46

def get_standard_constant_value(constant_name:)
  puts 'do nothing'
end

#get_standards_constant(name) ⇒ Object



117
118
119
120
121
122
123
124
125
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 117

def get_standards_constant(name)
  object = @standards_data['constants'][name]

  if object.nil? || object['value'].nil?
    raise("could not find #{name} in standards constants database. ")
  end

  return object['value']
end

#get_standards_formula(name) ⇒ Object



127
128
129
130
131
132
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 127

def get_standards_formula(name)
  object = @standards_data['formulas'][name]
  raise("could not find #{name} in standards formual database. ") if object.nil? || object['value'].nil?

  return object['value']
end

#get_standards_table(table_name:) ⇒ Object



38
39
40
41
42
43
44
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 38

def get_standards_table(table_name:)
  if @standards_data['tables'][table_name].nil?
    message = "Could not find table #{table_name} in database."
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.Standards.NECB', message)
  end
  @standards_data['tables'][table_name]
end

#get_surface_exp_per(floor, walls) ⇒ Object

Find the exposed perimeter of a floor surface. For each side of the floor loop through the walls and find the walls that share sides with the floor. Then sum the lengths of the sides of the walls that come in contact with sides of the floor. created by: Kamel Haddad ([email protected])



870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 870

def get_surface_exp_per(floor,walls)
  floor_exp_per = 0.0
  vert1 = floor.vertices[0]
  # loop through the indices of the floor surface
  for index in 1..floor.vertices.size
    if index < floor.vertices.size
      vert2 = floor.vertices[index]
    else
      vert2 = floor.vertices[0]
    end
    side_length = ((vert2.x-vert1.x)**2+(vert2.y-vert1.y)**2+(vert2.z-vert1.z)**2)**0.5
    walls_exp_per = 0.0
    walls.each do |wall|
      vert3 = wall.vertices[0]
      # loop through the indices of the wall surface
      for index2 in 1..wall.vertices.size-1
        if index2 < wall.vertices.size
          vert4 = wall.vertices[index2]
        else
          vert4 = wall.vertices[0]
        end
        vert1_2_3_on_same_line = three_vertices_same_line_and_dir?(vert1,vert2,vert3)
        if vert1_2_3_on_same_line
          vert1_2_4_on_same_line = three_vertices_same_line_and_dir?(vert1,vert2,vert4)
          if vert1_2_4_on_same_line
            wall_width = ((vert4.x-vert3.x)**2+(vert4.y-vert3.y)**2+(vert4.z-vert3.z)**2)**0.5
            walls_exp_per += wall_width
          end
        end
        vert3 = vert4
      end
    end
    # increment the exposed perimeter of the floor. Limit the length of the walls in contact with the
    # side of the floor to the length of the side of the floor.
    floor_exp_per += [walls_exp_per,side_length].min
    vert1 = vert2
  end

  return floor_exp_per
end

#get_weather_file_from_repo(epw_file:) ⇒ Object

This method handles looking for the epw_file in the github.com/canmet-energy/btap_weather repository. It checks for the epw_file in the historical data first. If it is not there then it looks in the future weather data. If it is not there either, it throws an error. epw_file (String): The name of the epw file. The different weather files all share the same name as the epw file,

only the extension changes.


2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2246

def get_weather_file_from_repo(epw_file:)
  # Get just the weather file name without the extension
  weather_loc = epw_file[0..-5]
  # Get the url of the file containing the historical weather data file names in the repository and the repository
  # folder containing the files
  historic_weather_files_loc = @standards_data['constants']['historic_weather_file_list']['value'].to_s
  historic_git_folder = @standards_data['constants']['historic_weather_folder_url']['value'].to_s
  # Get the files from the repository
  success_flag = download_and_save_file(weather_list_url: historic_weather_files_loc, weather_loc: weather_loc, git_folder: historic_git_folder)
  return if success_flag
  # If the file could not be found in the historical data look for it with the future weather data.
  puts "Could not find #{epw_file} in historical weather data files, looking in future weather data files."
  future_weather_files_loc = @standards_data['constants']['future_weather_file_list']['value'].to_s
  future_git_folder = @standards_data['constants']['future_weather_folder_url']['value'].to_s
  success_flag = download_and_save_file(weather_list_url: future_weather_files_loc, weather_loc: weather_loc, git_folder: future_git_folder)
  return if success_flag
  raise("Could not locate the following file in the canmet/btap_weather repository or could not extract the data: #{epw_file}.  Please check the spelling of the file or visit https://github.com/canmet-energy/btap_weather to see if the file exists.")
end

#group_similar_zones_together(zones) ⇒ Object

This method is used to determine if there are single zones that can be grouped with zones of similar loads.



1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1222

def group_similar_zones_together(zones)
  total_zones_input = zones.size
  array_of_array_of_zones = []
  accounted_for = []
  # Go through other zones to see if there are similar zones with similar loads on the same floor that can be grouped.
  zones.each do |zone|
    similar_array_of_zones = []
    next if accounted_for.include?(zone.name.to_s)

    similar_array_of_zones << zone
    accounted_for << zone.name.to_s
    zones.each do |zone_target|
      unless accounted_for.include?(zone_target.name.to_s)
        if are_zone_loads_similar?(zone_1: zone,
                                   zone_2: zone_target)
          similar_array_of_zones << zone_target
          accounted_for << zone_target.name.to_s
        end
      end
    end
    array_of_array_of_zones << similar_array_of_zones
  end
  total_zones_output = 0
  array_of_array_of_zones.each do |curr_zones|
    total_zones_output += curr_zones.size
  end
  # puts total_zones_output
  # puts accounted_for.sort
  # sanity check.
  if total_zones_output != total_zones_input
    # puts JSON.pretty_generate(array_of_array_of_zones)
    # puts JSON.pretty_generate(accounted_for.sort)
    raise('')
  end

  return array_of_array_of_zones
end

#heat_exchanger_air_to_air_sensible_and_latent_apply_effectiveness(heat_exchanger_air_to_air_sensible_and_latent, erv_name = nil) ⇒ Object

Sets the minimum effectiveness of the heat exchanger per the standard.



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 376

def heat_exchanger_air_to_air_sensible_and_latent_apply_effectiveness(heat_exchanger_air_to_air_sensible_and_latent, erv_name = nil)
  # Assumed to be sensible and latent at all flow
  # This will now get data of the erv from the json file instead of hardcoding it. Defaults to NECB2011 erv we have been using.
  erv_name = 'NECB_Default' if erv_name.nil?
  erv_info = @standards_data['tables']['erv']['table'].detect { |item| item['erv_name'] == erv_name }
  raise("Could not find #{erv_name} in #{self.class.name} class' erv.json file or it's parents. The available ervs are #{@standards_data['tables']['erv']['table'].map { |item| item['erv_name'] }}") if erv_info.nil?

  heat_exchanger_air_to_air_sensible_and_latent.setHeatExchangerType(erv_info['HeatExchangerType'])
  heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat100HeatingAirFlow(erv_info['SensibleEffectivenessat100HeatingAirFlow'])
  heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat100HeatingAirFlow(erv_info['LatentEffectivenessat100HeatingAirFlow'])
  heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat100CoolingAirFlow(erv_info['SensibleEffectivenessat100CoolingAirFlow'])
  heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat100CoolingAirFlow(erv_info['LatentEffectivenessat100CoolingAirFlow'])
  if heat_exchanger_air_to_air_sensible_and_latent.model.version < OpenStudio::VersionString.new('3.8.0')
    heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat75HeatingAirFlow(erv_info['SensibleEffectivenessat75HeatingAirFlow'])
    heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat75HeatingAirFlow(erv_info['LatentEffectivenessat75HeatingAirFlow'])
    heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat75CoolingAirFlow(erv_info['SensibleEffectivenessat75CoolingAirFlow'])
    heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat75CoolingAirFlow(erv_info['LatentEffectivenessat75CoolingAirFlow'])
  else
    heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat75HeatingAirFlow(erv_info['SensibleEffectivenessat75HeatingAirFlow']) unless erv_info['SensibleEffectivenessat75HeatingAirFlow'].zero?
    heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat75HeatingAirFlow(erv_info['LatentEffectivenessat75HeatingAirFlow']) unless erv_info['LatentEffectivenessat75HeatingAirFlow'].zero?
    heat_exchanger_air_to_air_sensible_and_latent.setSensibleEffectivenessat75CoolingAirFlow(erv_info['SensibleEffectivenessat75CoolingAirFlow']) unless erv_info['SensibleEffectivenessat75CoolingAirFlow'].zero?
    heat_exchanger_air_to_air_sensible_and_latent.setLatentEffectivenessat75CoolingAirFlow(erv_info['LatentEffectivenessat75CoolingAirFlow']) unless erv_info['LatentEffectivenessat75CoolingAirFlow'].zero?
  end
  heat_exchanger_air_to_air_sensible_and_latent.setSupplyAirOutletTemperatureControl(erv_info['SupplyAirOutletTemperatureControl'])
  heat_exchanger_air_to_air_sensible_and_latent.setFrostControlType(erv_info['FrostControlType'])
  heat_exchanger_air_to_air_sensible_and_latent.setEconomizerLockout(erv_info['EconomizerLockout'])
  heat_exchanger_air_to_air_sensible_and_latent.setThresholdTemperature(erv_info['ThresholdTemperature'])
  heat_exchanger_air_to_air_sensible_and_latent.setInitialDefrostTimeFraction(erv_info['InitialDefrostTimeFraction'])
  update_sys_name(heat_exchanger_air_to_air_sensible_and_latent.airLoopHVAC.get, sys_hr: 'erv')

  return true
end

#init_qaqc(model) ⇒ Object

generates full qaqc.json



155
156
157
158
159
160
161
162
163
164
165
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 155

def init_qaqc(model)
  # load the qaqc.json files
  # This is currently disabled as most tests are now done using regression and unit tests.. but we may bring this back.
  # @qaqc_data = self.load_qaqc_database_new()

  # generate base qaqc hash
  qaqc = create_base_data(model)
  # performs the qaqc on the given base qaqc hash
  # necb_qaqc(qaqc, model)
  return qaqc
end

#is_a_necb_dwelling_unit?(space) ⇒ Boolean

Check if the space spactype is a dwelling unit as per NECB.

Returns:

  • (Boolean)


665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 665

def is_a_necb_dwelling_unit?(space)
  space_type_table = @standards_data['space_types']
  space_type_data = model_find_object(space_type_table,
                                      'template' => self.class.name,
                                      'space_type' => space.spaceType.get.standardsSpaceType.get,
                                      'building_type' => space.spaceType.get.standardsBuildingType.get)

  necb_hvac_system_selection_table = @standards_data['necb_hvac_system_selection_type']
  necb_hvac_system_select = necb_hvac_system_selection_table.detect do |curr_necb_hvac_system_select|
    curr_necb_hvac_system_select['necb_hvac_system_selection_type'] == space_type_data['necb_hvac_system_selection_type'] &&
      curr_necb_hvac_system_select['min_stories'] <= space.model.getBuilding.standardsNumberOfAboveGroundStories.get &&
      curr_necb_hvac_system_select['max_stories'] >= space.model.getBuilding.standardsNumberOfAboveGroundStories.get
  end
  return necb_hvac_system_select['dwelling'] == true
end

#is_an_necb_storage_space?(space) ⇒ Boolean

Check to see if this is a wet space that the NECB does not have a specified schedule or system for. Currently hardcoded to Locker room and washroom.

Returns:

  • (Boolean)


659
660
661
662
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 659

def is_an_necb_storage_space?(space)
  # Hack! Should replace this with a proper table lookup.
  return space.spaceType.get.standardsSpaceType.get.include?('Storage')
end

#is_an_necb_wet_space?(space) ⇒ Boolean

Check to see if this is a wet space that the NECB does not have a specified schedule or system for. Currently hardcoded to Locker room and washroom.

Returns:

  • (Boolean)


652
653
654
655
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 652

def is_an_necb_wet_space?(space)
  # Hack! Should replace this with a proper table lookup.
  return space.spaceType.get.standardsSpaceType.get.include?('Locker room') || space.spaceType.get.standardsSpaceType.get.include?('Washroom')
end

#is_an_necb_wildcard_space?(space) ⇒ Boolean

Check to see if this is a wildcard space that the NECB does not have a specified schedule or system for.

Returns:

  • (Boolean)


639
640
641
642
643
644
645
646
647
648
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 639

def is_an_necb_wildcard_space?(space)
  space_type_table = @standards_data['space_types']
  space_type_data = model_find_object(space_type_table,
                                      'template' => self.class.name,
                                      'space_type' => space.spaceType.get.standardsSpaceType.get,
                                      'building_type' => space.spaceType.get.standardsBuildingType.get)
  raise(space.to_s) if space_type_data.nil?

  return space_type_data['necb_hvac_system_selection_type'] == 'Wildcard'
end

#load_building_type_from_library(building_type:) ⇒ Object



323
324
325
326
327
328
329
330
331
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 323

def load_building_type_from_library(building_type:)
  osm_model_path = File.absolute_path(File.join(__FILE__, '..', '..', '..', "necb/NECB2011/data/geometry/#{building_type}.osm"))
  model = false
  if File.file?(osm_model_path)
    model = BTAP::FileIO.load_osm(osm_model_path)
    model.getBuilding.setName(building_type)
  end
  return model
end

#load_qaqc_database_newObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 120

def load_qaqc_database_new
  # Combine the data from the JSON files into a single hash
  files = Dir.glob("#{File.dirname(__FILE__)}/qaqc_data/*.json").select { |e| File.file? e }
  @qaqc_data = {}
  @qaqc_data['tables'] = {}
  files.each do |file|
    # puts "loading qaqc data from #{file}"
    data = JSON.parse(File.read(file))
    if !data['tables'].nil?
      @qaqc_data['tables'] = [*@qaqc_data['tables'], *data['tables']].to_h
    else
      @qaqc_data[data.keys.first] = data[data.keys.first]
    end
  end
  # Write test report file.
  test_result_file = File.join(File.dirname(__FILE__), '..', 'NECB2011_QAQC.json')
  File.open(test_result_file, 'w') { |f| f.write(JSON.pretty_generate(@qaqc_data)) }
  return @qaqc_data
end

#load_standards_database_newObject

Combine the data from the JSON files into a single hash Load JSON files differently depending on whether loading from the OpenStudio CLI embedded filesystem or from typical gem installation



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 53

def load_standards_database_new
  @standards_data = {}
  @standards_data['tables'] = {}

  if __dir__[0] == ':' # Running from OpenStudio CLI
    embedded_files_relative('../common', /.*\.json/).each do |file|
      data = JSON.parse(EmbeddedScripting.getFileAsString(file))
      if !data['tables'].nil?
        @standards_data['tables'] = [*@standards_data['tables'], *data['tables']].to_h
      else
        @standards_data[data.keys.first] = data[data.keys.first]
      end
    end
  else
    path = "#{File.dirname(__FILE__)}/../common/"
    raise 'Could not find common folder' unless Dir.exist?(path)

    files = Dir.glob("#{path}/*.json").select { |e| File.file? e }
    files.each do |file|
      data = JSON.parse(File.read(file))
      if !data['tables'].nil?
        @standards_data['tables'] = [*@standards_data['tables'], *data['tables']].to_h
      else
        @standards_data[data.keys.first] = data[data.keys.first]
      end
    end
  end

  if __dir__[0] == ':' # Running from OpenStudio CLI
    embedded_files_relative('data/', /.*\.json/).each do |file|
      data = JSON.parse(EmbeddedScripting.getFileAsString(file))
      if !data['tables'].nil?
        @standards_data['tables'] = [*@standards_data['tables'], *data['tables']].to_h
      else
        @standards_data[data.keys.first] = data[data.keys.first]
      end
    end
  else
    files = Dir.glob("#{File.dirname(__FILE__)}/data/*.json").select { |e| File.file? e }
    files.each do |file|
      data = JSON.parse(File.read(file))
      if !data['tables'].nil?
        @standards_data['tables'] = [*@standards_data['tables'], *data['tables']].to_h
      else
        @standards_data[data.keys.first] = data[data.keys.first]
      end
    end
  end
  # Write database to file.
  # File.open(File.join(File.dirname(__FILE__), '..', 'NECB2011.json'), 'w') {|f| f.write(JSON.pretty_generate(@standards_data))}

  return @standards_data
end

#look_up_csv_data(csv_fname, search_criteria) ⇒ Object



1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1911

def look_up_csv_data(csv_fname, search_criteria)
  options = { headers: :first_row,
              converters: [:numeric] }
  unless File.exist?(csv_fname)
    raise "File: [#{csv_fname}] Does not exist"
  end

  # we'll save the matches here
  matches = nil
  # save a copy of the headers
  headers = nil
  CSV.open(csv_fname, 'r', options) do |csv|
    # Since CSV includes Enumerable we can use 'find_all'
    # which will return all the elements of the Enumerble for
    # which the block returns true

    matches = csv.find_all do |row|
      match = true
      search_criteria.keys.each do |key|
        match &&= (row[key].strip == search_criteria[key].strip)
      end
      match
    end
    headers = csv.headers
  end
  # puts matches
  raise('More than one match') if matches.size > 1

  puts "Zero matches found for [#{search_criteria}]" if matches.empty?
  # return matches[0]
  return matches[0]
end

#max_fwdr(hdd) ⇒ Double

Returns a constant float.

Parameters:

  • hdd (Float)

Returns:

  • (Double)

    a constant float

Author:



286
287
288
289
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 286

def max_fwdr(hdd)
  #  get formula from json database.
  return eval(get_standards_formula('fdwr_formula'))
end

#merge_recursively(a, b) ⇒ Object



102
103
104
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 102

def merge_recursively(a, b)
  a.merge(b) { |key, a_item, b_item| merge_recursively(a_item, b_item) }
end

#model_add_construction_set_from_osm(model:, construction_set_name: 'BTAP-Mass', osm_path: File.absolute_path(File.join(__FILE__, '..', '..', 'common/construction_defaults.osm'))) ⇒ Object



524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 524

def model_add_construction_set_from_osm(model:,
                                        construction_set_name: 'BTAP-Mass',
                                        osm_path: File.absolute_path(File.join(__FILE__, '..', '..', 'common/construction_defaults.osm')))
  # load resources model
  construction_library = BTAP::FileIO.load_osm(osm_path)

  if !construction_library.getDefaultConstructionSetByName(construction_set_name.to_s).is_initialized
    runner.registerError('Did not find the expected construction in library.')
    return false
  end
  selected_construction_set = construction_library.getDefaultConstructionSetByName(construction_set_name.to_s).get
  new_construction_set = selected_construction_set.clone(model).to_DefaultConstructionSet.get
  return new_construction_set
end

#model_add_constructions(model) ⇒ Boolean

Adds code-minimum constructions based on the building type as defined in the OpenStudio_Standards_construction_sets.json file. Where there is a separate construction set specified for the individual space type, this construction set will be created and applied to this space type, overriding the whole-building construction set.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Boolean)

    returns true if successful, false if not



471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 471

def model_add_constructions(model)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started applying constructions')

  # Assign construction to adiabatic construction
  # Assign a material to all internal mass objects
  assign_contruction_to_adiabatic_surfaces(model)
  # The constructions lookup table uses a slightly different list of
  # building types.
  apply_building_default_constructionset(model)
  # Make a construction set for each space type, if one is specified
  # apply_default_constructionsets_to_spacetypes(climate_zone, model)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished applying constructions')
  return true
end

#model_add_daylighting_controls(model:, daylighting_type:) ⇒ Object



1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1346

def model_add_daylighting_controls(model:, daylighting_type:)

  return if daylighting_type == 'none'
  ##### Find spaces with exterior fenestration including fixed window, operable window, and skylight.
  daylight_spaces = []
  daylight_spaces_target_illuminance_setpoint_hash = {}
  model.getSpaces.sort.each do |space|
    space.surfaces.sort.each do |surface|
      surface.subSurfaces.sort.each do |subsurface|
        if subsurface.outsideBoundaryCondition == 'Outdoors' &&
          (subsurface.subSurfaceType == 'FixedWindow' ||
            subsurface.subSurfaceType == 'OperableWindow' ||
            subsurface.subSurfaceType == 'Skylight')
          daylight_spaces << space
          space_type = space.spaceType.get
          space_type_name = space.spaceType.get.name.to_s
          space_type_name = space_type_name.gsub('Space Function', '')
          # puts "space_type_name is #{space_type_name}"

          # Gather minimum illuminance level as per NECB
          lux_spacetype_data = @standards_data['tables']['space_types']['table']
          standards_building_type = space_type.standardsBuildingType.is_initialized ? space_type.standardsBuildingType.get : nil
          standards_space_type = space_type.standardsSpaceType.is_initialized ? space_type.standardsSpaceType.get : nil
          lux_space_type_properties = lux_spacetype_data.detect { |s| (s['building_type'] == standards_building_type) && (s['space_type'] == standards_space_type) }
          if lux_space_type_properties.nil?
            raise("#{standards_building_type} for #{standards_space_type} was not found please verify the target_illuminance_setpoint database names match the space type names.")
          end

          target_illuminance_setpoint = lux_space_type_properties['target_illuminance_setpoint'].to_f
          daylight_spaces_target_illuminance_setpoint_hash[space.name.to_s] = target_illuminance_setpoint

          # subsurface.outsideBoundaryCondition == "Outdoors" && (subsurface.subSurfaceType == "FixedWindow" || "OperableWindow")
        end
        # surface.subSurfaces.each do |subsurface|
      end
      # space.surfaces.each do |surface|
    end
    # model.getSpaces.sort.each do |space|
  end

  ##### Remove duplicate spaces from the "daylight_spaces" array, as a daylighted space may have various fenestration types.
  daylight_spaces = daylight_spaces.uniq
  # puts "daylight_spaces are #{daylight_spaces}"

  if daylighting_type.nil? || daylighting_type == false || daylighting_type == 'none' || daylighting_type == 'NECB_Default' # puts daylighting sensors in the spaces as per NECB requirements; so some spaces may not have sensors

    ##### Create hashes for "Primary Sidelighted Areas", "Sidelighting Effective Aperture", "Daylighted Area Under Skylights",
    ##### and "Skylight Effective Aperture" for the whole model.
    ##### Each of these hashes will be used later in this function (i.e. model_add_daylighting_controls)
    ##### to provide a dictionary of daylighted space names and the associated value (i.e. daylighted area or effective aperture).
    primary_sidelighted_area_hash = {}
    sidelighting_effective_aperture_hash = {}
    daylighted_area_under_skylights_hash = {}
    skylight_effective_aperture_hash = {}

    ##### Calculate "Primary Sidelighted Areas" AND "Sidelighting Effective Aperture" as per NECB2011. # @todo consider removing overlapped sidelighted area
    daylight_spaces.sort.each do |daylight_space|
      primary_sidelighted_area = 0.0
      area_weighted_vt_handle = 0.0
      area_weighted_vt = 0.0
      window_area_sum = 0.0

      ##### Calculate floor area of the daylight_space and get floor vertices of the daylight_space (to be used for the calculation of daylight_space depth)
      floor_surface = nil
      floor_area = 0.0
      floor_vertices = []
      daylight_space.surfaces.sort.each do |surface|
        if surface.surfaceType == 'Floor'
          floor_surface = surface
          floor_area += surface.netArea
          floor_vertices << surface.vertices
        end
      end

      ##### Loop through the surfaces of each daylight_space to calculate primary_sidelighted_area and
      ##### area-weighted visible transmittance and window_area_sum which are used to calculate sidelighting_effective_aperture
      primary_sidelighted_area, area_weighted_vt_handle, window_area_sum =
        get_parameters_sidelighting(daylight_space: daylight_space,
                                    floor_surface: floor_surface,
                                    floor_vertices: floor_vertices,
                                    floor_area: floor_area,
                                    primary_sidelighted_area: primary_sidelighted_area,
                                    area_weighted_vt_handle: area_weighted_vt_handle,
                                    window_area_sum: window_area_sum)

      primary_sidelighted_area_hash[daylight_space.name.to_s] = primary_sidelighted_area

      ##### Calculate area-weighted VT of glazing (this is used to calculate sidelighting effective aperture; see NECB2011: 4.2.2.10.).
      area_weighted_vt = area_weighted_vt_handle / window_area_sum
      sidelighting_effective_aperture_hash[daylight_space.name.to_s] = window_area_sum * area_weighted_vt / primary_sidelighted_area
      # daylight_spaces.each do |daylight_space|
    end

    ##### Calculate "Daylighted Area Under Skylights" AND "Skylight Effective Aperture"
    daylight_spaces.sort.each do |daylight_space|
      # puts daylight_space.name.to_s
      skylight_area = 0.0
      skylight_area_weighted_vt_handle = 0.0
      skylight_area_weighted_vt = 0.0
      skylight_area_sum = 0.0
      daylighted_under_skylight_area = 0.0

      ##### Loop through the surfaces of each daylight_space to calculate daylighted_area_under_skylights and skylight_effective_aperture for each daylight_space
      daylighted_under_skylight_area, skylight_area_weighted_vt_handle, skylight_area_sum =
        get_parameters_skylight(daylight_space: daylight_space,
                                skylight_area_weighted_vt_handle: skylight_area_weighted_vt_handle,
                                skylight_area_sum: skylight_area_sum,
                                daylighted_under_skylight_area: daylighted_under_skylight_area)

      daylighted_area_under_skylights_hash[daylight_space.name.to_s] = daylighted_under_skylight_area

      ##### Calculate skylight_effective_aperture as per NECB2011: 4.2.2.7.
      ##### Note that it was assumed that the skylight is flush with the ceiling. Therefore, area-weighted average well factor (WF) was set to 0.9 in the below Equation.
      skylight_area_weighted_vt = skylight_area_weighted_vt_handle / skylight_area_sum
      skylight_effective_aperture_hash[daylight_space.name.to_s] = 0.85 * skylight_area_sum * skylight_area_weighted_vt * 0.9 / daylighted_under_skylight_area
      # daylight_spaces.each do |daylight_space|
    end
    # puts "primary_sidelighted_area_hash is #{primary_sidelighted_area_hash}"
    # puts sidelighting_effective_aperture_hash
    # puts daylighted_area_under_skylights_hash
    # puts skylight_effective_aperture_hash

    ##### Find office spaces >= 25m2 among daylight_spaces
    offices_larger_25m2 = []
    daylight_spaces.sort.each do |daylight_space|
      ## The following steps are for in case an office has multiple floors at various heights
      ## 1. Calculate number of floors of each daylight_space
      ## 2. Find the lowest z among all floors of each daylight_space
      ## 3. Find lowest floors of each daylight_space (these floors are at the same level)
      ## 4. Calculate 'daylight_space_area' as sum of area of all the lowest floors of each daylight_space, and gather the vertices of all the lowest floors of each daylight_space

      ## 1. Calculate number of floors of daylight_space
      floor_vertices = []
      number_floor = 0
      daylight_space.surfaces.sort.each do |surface|
        if surface.surfaceType == 'Floor'
          floor_vertices << surface.vertices
          number_floor += 1
        end
      end

      ## 2. Loop through all floors of daylight_space, and find the lowest z among all floors of daylight_space
      lowest_floor_z = []
      highest_floor_z = []
      for i in 0..number_floor - 1
        if i == 0
          lowest_floor_z = floor_vertices[i][0].z
          highest_floor_z = floor_vertices[i][0].z
        else
          if lowest_floor_z > floor_vertices[i][0].z
            lowest_floor_z = floor_vertices[i][0].z
          else
            lowest_floor_z = lowest_floor_z
          end
          if highest_floor_z < floor_vertices[i][0].z
            highest_floor_z = floor_vertices[i][0].z
          else
            highest_floor_z = highest_floor_z
          end
        end
      end

      ## 3 and 4. Loop through all floors of daylight_space, and calculate the sum of area of all the lowest floors of daylight_space,
      ## and gather the vertices of all the lowest floors of daylight_space
      daylight_space_area = 0
      lowest_floors_vertices = []
      floor_vertices = []
      daylight_space.surfaces.sort.each do |surface|
        if surface.surfaceType == 'Floor'
          floor_vertices = surface.vertices
          if floor_vertices[0].z == lowest_floor_z
            lowest_floors_vertices << floor_vertices
            daylight_space_area += surface.netArea
          end
        end
      end

      if daylight_space.spaceType.get.standardsSpaceType.get.to_s == 'Office - enclosed' && daylight_space_area >= 25.0
        offices_larger_25m2 << daylight_space.name.to_s
      end
    end

    ##### find daylight_spaces which do not need daylight sensor controls based on the primary_sidelighted_area as per NECB2011: 4.2.2.8.
    ##### Note: Office spaces >= 25m2 are excluded (i.e. they should have daylighting controls even if their primary_sidelighted_area <= 100m2), as per NECB2011: 4.2.2.2.
    daylight_spaces_exception = []
    primary_sidelighted_area_hash.sort.each do |key_daylight_space_name, value_primary_sidelighted_area|
      if value_primary_sidelighted_area <= 100.0 && [key_daylight_space_name].any? { |word| offices_larger_25m2.include?(word) } == false
        daylight_spaces_exception << key_daylight_space_name
      end
    end

    ##### find daylight_spaces which do not need daylight sensor controls based on the sidelighting_effective_aperture as per NECB2011: 4.2.2.8.
    ##### Note: Office spaces >= 25m2 are excluded (i.e. they should have daylighting controls even if their sidelighting_effective_aperture <= 10%), as per NECB2011: 4.2.2.2.
    sidelighting_effective_aperture_hash.sort.each do |key_daylight_space_name, value_sidelighting_effective_aperture|
      if value_sidelighting_effective_aperture <= 0.1 && [key_daylight_space_name].any? { |word| offices_larger_25m2.include?(word) } == false
        daylight_spaces_exception << key_daylight_space_name
      end
    end

    ##### find daylight_spaces which do not need daylight sensor controls based on the daylighted_area_under_skylights as per NECB2011: 4.2.2.4.
    ##### Note: Office spaces >= 25m2 are excluded (i.e. they should have daylighting controls even if their daylighted_area_under_skylights <= 400m2), as per NECB2011: 4.2.2.2.
    daylighted_area_under_skylights_hash.sort.each do |key_daylight_space_name, value_daylighted_area_under_skylights|
      if value_daylighted_area_under_skylights <= 400.0 && [key_daylight_space_name].any? { |word| offices_larger_25m2.include?(word) } == false
        daylight_spaces_exception << key_daylight_space_name
      end
    end

    ##### find daylight_spaces which do not need daylight sensor controls based on the skylight_effective_aperture criterion as per NECB2011: 4.2.2.4.
    ##### Note: Office spaces >= 25m2 are excluded (i.e. they should have daylighting controls even if their skylight_effective_aperture <= 0.6%), as per NECB2011: 4.2.2.2.
    skylight_effective_aperture_hash.sort.each do |key_daylight_space_name, value_skylight_effective_aperture|
      if value_skylight_effective_aperture <= 0.006 && [key_daylight_space_name].any? { |word| offices_larger_25m2.include?(word) } == false
        daylight_spaces_exception << key_daylight_space_name
      end
    end
    # puts daylight_spaces_exception

    ##### Loop through the daylight_spaces and exclude the daylight_spaces that do not meet the criteria (see above) as per NECB2011: 4.2.2.4. and 4.2.2.8.
    daylight_spaces_exception.sort.each do |daylight_space_exception|
      daylight_spaces.sort.each do |daylight_space|
        if daylight_space.name.to_s == daylight_space_exception
          daylight_spaces.delete(daylight_space)
        end
      end
    end
    # puts daylight_spaces

    # elsif daylighting_type == 'add_daylighting_controls' # puts daylighting sensors in all spaces regardless of NECB requirements

  end  #if daylighting_type.nil? || daylighting_type == false || daylighting_type == 'none' || daylighting_type == 'NECB_Default'

  ##### Create one daylighting sensor and put it at the center of each daylight_space if the space area < 250m2;
  ##### otherwise, create two daylight sensors, divide the space into two parts and put each of the daylight sensors at the center of each part of the space.
  daylight_spaces.sort.each do |daylight_space|
    # puts daylight_space.name.to_s
    ##### 1. Calculate number of floors of each daylight_space
    ##### 2. Find the lowest z among all floors of each daylight_space
    ##### 3. Find lowest floors of each daylight_space (these floors are at the same level)
    ##### 4. Calculate 'daylight_space_area' as sum of area of all the lowest floors of each daylight_space, and gather the vertices of all the lowest floors of each daylight_space
    ##### 5. Find min and max of x and y among vertices of all the lowest floors of each daylight_space

    ##### Calculate number of floors of daylight_space
    floor_vertices = []
    number_floor = 0
    daylight_space.surfaces.sort.each do |surface|
      if surface.surfaceType == 'Floor'
        floor_vertices << surface.vertices
        number_floor += 1
      end
    end

    ##### Loop through all floors of daylight_space, and find the lowest z among all floors of daylight_space
    lowest_floor_z = []
    highest_floor_z = []
    for i in 0..number_floor - 1
      if i == 0
        lowest_floor_z = floor_vertices[i][0].z
        highest_floor_z = floor_vertices[i][0].z
      else
        if lowest_floor_z > floor_vertices[i][0].z
          lowest_floor_z = floor_vertices[i][0].z
        else
          lowest_floor_z = lowest_floor_z
        end
        if highest_floor_z < floor_vertices[i][0].z
          highest_floor_z = floor_vertices[i][0].z
        else
          highest_floor_z = highest_floor_z
        end
      end
    end
    # puts lowest_floor_z

    ##### Loop through all floors of daylight_space, and calculate the sum of area of all the lowest floors of daylight_space,
    ##### and gather the vertices of all the lowest floors of daylight_space
    daylight_space_area = 0
    lowest_floors_vertices = []
    floor_vertices = []
    daylight_space.surfaces.sort.each do |surface|
      if surface.surfaceType == 'Floor'
        floor_vertices = surface.vertices
        if floor_vertices[0].z == lowest_floor_z
          lowest_floors_vertices << floor_vertices
          daylight_space_area += surface.netArea
        end
      end
    end
    # puts daylight_space.name.to_s
    # puts number_floor
    # puts lowest_floors_vertices
    # puts daylight_space_area

    ##### Loop through all lowest floors of daylight_space and find the min and max of x and y among their vertices
    xmin = lowest_floors_vertices[0][0].x
    ymin = lowest_floors_vertices[0][0].y
    xmax = lowest_floors_vertices[0][0].x
    ymax = lowest_floors_vertices[0][0].y
    zmin = lowest_floor_z
    for i in 0..lowest_floors_vertices.count - 1 # this loops through each of the lowers floors of daylight_space
      for j in 0..lowest_floors_vertices[i].count - 1 # this loops through each of vertices of each of the lowers floors of daylight_space

        if xmin > lowest_floors_vertices[i][j].x
          xmin = lowest_floors_vertices[i][j].x
        end
        if ymin > lowest_floors_vertices[i][j].y
          ymin = lowest_floors_vertices[i][j].y
        end
        if xmax < lowest_floors_vertices[i][j].x
          xmax = lowest_floors_vertices[i][j].x
        end
        if ymax < lowest_floors_vertices[i][j].y
          ymax = lowest_floors_vertices[i][j].y
        end
      end
    end
    # puts daylight_space.name.to_s
    # puts xmin
    # puts xmax
    # puts ymin
    # puts ymax

    ##### Get the thermal zone of daylight_space (this is used later to assign daylighting sensor)
    zone = daylight_space.thermalZone
    # puts "zone name is #{zone}"
    if !zone.empty?
      zone = daylight_space.thermalZone.get
      ##### Get the floor of the daylight_space
      floors = []
      daylight_space.surfaces.sort.each do |surface|
        if surface.surfaceType == 'Floor'
          floors << surface
        end
      end

      ##### Set daylighting controls illuminance setpoint and number of stepped control steps
      number_of_stepped_control_steps = 2   ##### Note that the minimum number of stepped control steps is two steps as per NECB2011.
      illuminance_setpoint =  daylight_spaces_target_illuminance_setpoint_hash.select {|key| key == daylight_space.name.to_s }
      illuminance_setpoint = illuminance_setpoint[daylight_space.name.to_s]

      ##### Create daylighting sensor control
      ##### NOTE: NECB2011 has some requirements on the number of sensors in spaces based on the area of the spaces.
      ##### However, EnergyPlus/OpenStudio allows to put maximum two built-in sensors in each thermal zone rather than in each space.
      ##### Since a thermal zone may include several spaces which are not next to each other on the same floor, or
      ##### a thermal zone may include spaces on different floors, a simplified method has been used to create a daylighting sensor.
      ##### So, in each thermal zone, only one daylighting sensor has been created even if the area of that thermal zone requires more than one daylighting sensor.
      ##### Also, it has been assumed that a thermal zone includes spaces which are next to each other and are on the same floor.
      ##### Furthermore, the one daylighting sensor in each thermal zone (where the thermal zone needs daylighting sensor),
      ##### the sensor has been put at the intersection of the minimum and maximum x and y of the lowest floor of that thermal zones.
      sensor = OpenStudio::Model::DaylightingControl.new(daylight_space.model)
      sensor.setName("#{daylight_space.name} daylighting control")
      sensor.setSpace(daylight_space)
      sensor.setIlluminanceSetpoint(illuminance_setpoint)
      sensor.setLightingControlType('Stepped')
      sensor.setNumberofSteppedControlSteps(number_of_stepped_control_steps)
      x_pos = (xmin + xmax) / 2.0
      y_pos = (ymin + ymax) / 2.0
      z_pos = zmin + 0.8 # put it 0.8 meter above the floor
      sensor_vertex = OpenStudio::Point3d.new(x_pos, y_pos, z_pos)
      sensor.setPosition(sensor_vertex)
      zone.setPrimaryDaylightingControl(sensor)
      zone.setFractionofZoneControlledbyPrimaryDaylightingControl(1.0)
      # if !zone.empty?
    end
    # daylight_spaces.each do |daylight_space|
  end # END if daylighting_controls_type.nil? || daylighting_controls_type == false || daylighting_controls_type == 'none' || daylighting_controls_type == 'NECB_Default'

end

#model_add_hvac(model:) ⇒ Object



2
3
4
5
6
7
8
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2

def model_add_hvac(model:)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started Adding HVAC')
  system_fuel_defaults = get_canadian_system_defaults_by_weatherfile_name(model)
  necb_autozone_and_autosystem(model: model, runner: nil, use_ideal_air_loads: false, system_fuel_defaults: system_fuel_defaults)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished adding HVAC')
  return true
end

#model_add_loads(model, lights_type, lights_scale) ⇒ Boolean

Adds the loads and associated schedules for each space type as defined in the OpenStudio_Standards_space_types.json file. This includes lights, plug loads, occupants, ventilation rate requirements, infiltration, gas equipment (for kitchens, etc.) and typical schedules for each. Some loads are governed by the standard, others are typical values pulled from sources such as the DOE Reference and DOE Prototype Buildings.

Returns:

  • (Boolean)

    returns true if successful, false if not



1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1904

def model_add_loads(model, lights_type, lights_scale)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started applying space types (loads)')

  # Loop through all the space types currently in the model,
  # which are placeholders, and give them appropriate loads and schedules
  model.getSpaceTypes.sort.each do |space_type|
    # Rendering color
    space_type_apply_rendering_color(space_type)

    # Loads
    space_type_apply_internal_loads(space_type: space_type, lights_type: lights_type, lights_scale: lights_scale)

    # Schedules
    space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, true)
  end

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished applying space types (loads)')

  return true
end

#model_add_schedule(model, schedule_name) ⇒ ScheduleRuleset

TODO:

make return an OptionalScheduleRuleset

Create a schedule from the openstudio standards dataset and add it to the model.

Parameters:

  • schedule_name (String)

    name of the schedule

Returns:

  • (ScheduleRuleset)

    the resulting schedule ruleset



113
114
115
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 113

def model_add_schedule(model, schedule_name)
  super(model, schedule_name)
end

#model_add_swh(model:, swh_fueltype: 'DefaultFuel', shw_scale:) ⇒ Object



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 2

def model_add_swh(model:, swh_fueltype: 'DefaultFuel', shw_scale:)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started Adding Service Water Heating')
  # Get default fuel based on epw location province.
  if swh_fueltype == 'DefaultFuel'
    epw = OpenStudio::EpwFile.new(model.weatherFile.get.path.get)
    swh_fueltype = @standards_data['regional_fuel_use'].detect { |fuel_sources| fuel_sources['state_province_regions'].include?(epw.stateProvinceRegion) }['fueltype_set']
  end

  # Calculate the tank size and service water pump information
  shw_sizing = auto_size_shw_capacity(model: model, shw_scale: shw_scale)
  if shw_sizing['loop_peak_flow_rate_SI'] == 0
    # Only add a shw_loop if at least one space calls for shw.  If no space calls for shw put out a warning but do not
    # add a shw loop.
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'No Service Water Heating Added')
    return true
  else
    shw_pump_head = auto_size_shw_pump_head(model, default: false)
  end

  # Add the main service water heating loop
  shw_pump_motor_eff = 0.9

  main_swh_loop = model_add_swh_loop(model,
                                     'Main Service Water Loop',
                                     nil,
                                     shw_sizing['max_temp_SI'],
                                     shw_pump_head,
                                     shw_pump_motor_eff,
                                     shw_sizing['tank_capacity_SI'],
                                     shw_sizing['tank_volume_SI'],
                                     swh_fueltype,
                                     shw_sizing['parasitic_loss'],
                                     nil)

  # Note that when water use equipment is assigned to spaces then the water used by the equipment is multiplied by
  # the space (ultimately thermal zone) multiplier.  Note that there is a separate water use equipment multiplier
  # as well which is different than the space (ultimately thermal zone) multiplier.
  shw_sizing['spaces_w_dhw'].each { |space| model_add_swh_end_uses_by_spaceonly(model, space, main_swh_loop) }
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished adding Service Water Heating')

  return true
end

#model_add_swh_end_uses_by_spaceonly(model, space, swh_loop) ⇒ OpenStudio::Model::WaterUseEquipment

This method will add an swh water fixture to the model for the space. if the it will return a water fixture object, or NIL if there is no water load at all.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • space (Hash)

    hash of shw space information

  • swh_loop (OpenStudio::Model::PlantLoop)

    plant loop to add swh

Returns:

  • (OpenStudio::Model::WaterUseEquipment)

    water use equipment



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 52

def model_add_swh_end_uses_by_spaceonly(model, space, swh_loop)
  # Water use connection
  swh_connection = OpenStudio::Model::WaterUseConnections.new(model)

  # Water fixture definition
  water_fixture_def = OpenStudio::Model::WaterUseEquipmentDefinition.new(model)

  # water_use_sensible_frac_sch = OpenStudio::Model::ScheduleConstant.new(self)
  # water_use_sensible_frac_sch.setValue(0.2)
  # water_use_latent_frac_sch = OpenStudio::Model::ScheduleConstant.new(self)
  # water_use_latent_frac_sch.setValue(0.05)
  # Note that when water use equipment is assigned to spaces then the water used by the equipment is multiplied by the
  # space (ultimately thermal zone) multiplier.  Note that there is a separate water use equipment multiplier as well
  # which is different than the space (ultimately thermal zone) multiplier.
  rated_flow_rate_gal_per_min = OpenStudio.convert(space['shw_peakflow_ind_SI'], 'm^3/s', 'gal/min').get
  water_use_sensible_frac_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  water_use_sensible_frac_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0.2)
  water_use_latent_frac_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  water_use_latent_frac_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0.05)
  water_fixture_def.setSensibleFractionSchedule(water_use_sensible_frac_sch)
  water_fixture_def.setLatentFractionSchedule(water_use_latent_frac_sch)
  water_fixture_def.setPeakFlowRate(space['shw_peakflow_ind_SI'])
  water_fixture_def.setName("#{space['shw_spaces'].name.to_s.capitalize} Service Water Use Def #{rated_flow_rate_gal_per_min.round(2)}gal/min")
  # Target mixed water temperature
  mixed_water_temp_c = space['shw_temp_SI']
  mixed_water_temp_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  mixed_water_temp_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), mixed_water_temp_c)
  water_fixture_def.setTargetTemperatureSchedule(mixed_water_temp_sch)

  # Water use equipment
  water_fixture = OpenStudio::Model::WaterUseEquipment.new(water_fixture_def)
  schedule = model_add_schedule(model, space['shw_sched'])
  water_fixture.setFlowRateFractionSchedule(schedule)
  water_fixture.setName("#{space['shw_spaces'].name.to_s.capitalize} Service Water Use #{rated_flow_rate_gal_per_min.round(2)}gal/min")
  swh_connection.addWaterUseEquipment(water_fixture)
  # Assign water fixture to a space
  water_fixture.setSpace(space['shw_spaces']) if model_attach_water_fixtures_to_spaces?(model)

  # Connect the water use connection to the SWH loop
  swh_loop.addDemandBranchForComponent(swh_connection)
  return water_fixture
end

#model_apply_sizing_parameters(model) ⇒ Object



1516
1517
1518
1519
1520
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1516

def model_apply_sizing_parameters(model)
  model.getSizingParameters.setHeatingSizingFactor(get_standards_constant('sizing_factor_max_heating'))
  model.getSizingParameters.setCoolingSizingFactor(get_standards_constant('sizing_factor_max_cooling'))
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.prototype.Model', "Set sizing factors to #{get_standards_constant('sizing_factor_max_heating')} for heating and #{get_standards_constant('sizing_factor_max_heating')} for cooling.")
end

#model_apply_standard(model:, tbd_option: nil, tbd_interpolate: nil, epw_file:, custom_weather_folder: nil, sizing_run_dir: Dir.pwd, necb_reference_hp: false, necb_reference_hp_supp_fuel: 'DefaultFuel', primary_heating_fuel: 'DefaultFuel', dcv_type: 'NECB_Default', lights_type: 'NECB_Default', lights_scale: 'NECB_Default', daylighting_type: 'NECB_Default', ecm_system_name: 'NECB_Default', ecm_system_zones_map_option: 'NECB_Default', erv_package: 'NECB_Default', boiler_eff: nil, furnace_eff: nil, unitary_cop: nil, shw_eff: nil, ext_wall_cond: nil, ext_floor_cond: nil, ext_roof_cond: nil, ground_wall_cond: nil, ground_floor_cond: nil, ground_roof_cond: nil, door_construction_cond: nil, fixed_window_cond: nil, glass_door_cond: nil, overhead_door_cond: nil, skylight_cond: nil, glass_door_solar_trans: nil, fixed_wind_solar_trans: nil, skylight_solar_trans: nil, fdwr_set: nil, srr_set: nil, rotation_degrees: nil, scale_x: nil, scale_y: nil, scale_z: nil, nv_type: nil, nv_opening_fraction: nil, nv_temp_out_min: nil, nv_delta_temp_in_out: nil, pv_ground_type: nil, pv_ground_total_area_pv_panels_m2: nil, pv_ground_tilt_angle: nil, pv_ground_azimuth_angle: nil, pv_ground_module_description: nil, chiller_type: nil, occupancy_loads_scale: nil, electrical_loads_scale: nil, oa_scale: nil, infiltration_scale: nil, output_variables: nil, shw_scale: nil, output_meters: nil, airloop_economizer_type: nil, baseline_system_zones_map_option: nil, necb_hdd: true) ⇒ Object

Created this method so that additional methods can be addded for bulding the prototype model in later code versions without modifying the build_protoype_model method or copying it wholesale for a few changes.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 335

def model_apply_standard(model:,
                         tbd_option: nil,
                         tbd_interpolate: nil,
                         epw_file:,
                         custom_weather_folder: nil,
                         sizing_run_dir: Dir.pwd,
                         necb_reference_hp: false,
                         necb_reference_hp_supp_fuel: 'DefaultFuel',
                         primary_heating_fuel: 'DefaultFuel',
                         dcv_type: 'NECB_Default',
                         lights_type: 'NECB_Default',
                         lights_scale: 'NECB_Default',
                         daylighting_type: 'NECB_Default',
                         ecm_system_name: 'NECB_Default',
                         ecm_system_zones_map_option: 'NECB_Default',
                         erv_package: 'NECB_Default',
                         boiler_eff: nil,
                         furnace_eff: nil,
                         unitary_cop: nil,
                         shw_eff: nil,
                         ext_wall_cond: nil,
                         ext_floor_cond: nil,
                         ext_roof_cond: nil,
                         ground_wall_cond: nil,
                         ground_floor_cond: nil,
                         ground_roof_cond: nil,
                         door_construction_cond: nil,
                         fixed_window_cond: nil,
                         glass_door_cond: nil,
                         overhead_door_cond: nil,
                         skylight_cond: nil,
                         glass_door_solar_trans: nil,
                         fixed_wind_solar_trans: nil,
                         skylight_solar_trans: nil,
                         fdwr_set: nil,
                         srr_set: nil,
                         rotation_degrees: nil,
                         scale_x: nil,
                         scale_y: nil,
                         scale_z: nil,
                         nv_type: nil,
                         nv_opening_fraction: nil,
                         nv_temp_out_min: nil,
                         nv_delta_temp_in_out: nil,
                         pv_ground_type: nil,
                         pv_ground_total_area_pv_panels_m2: nil,
                         pv_ground_tilt_angle: nil,
                         pv_ground_azimuth_angle: nil,
                         pv_ground_module_description: nil,
                         chiller_type: nil,
                         occupancy_loads_scale: nil,
                         electrical_loads_scale: nil,
                         oa_scale: nil,
                         infiltration_scale: nil,
                         output_variables: nil,
                         shw_scale: nil,
                         output_meters: nil,
                         airloop_economizer_type: nil,
                         baseline_system_zones_map_option: nil,
                         necb_hdd: true)
  self.fuel_type_set = SystemFuels.new()
  self.fuel_type_set.set_defaults(standards_data: @standards_data, primary_heating_fuel: primary_heating_fuel)
  clean_and_scale_model(model: model, rotation_degrees: rotation_degrees, scale_x: scale_x, scale_y: scale_y, scale_z: scale_z)
  fdwr_set = convert_arg_to_f(variable: fdwr_set, default: -1)
  srr_set = convert_arg_to_f(variable: srr_set, default: -1)
  necb_hdd = convert_arg_to_bool(variable: necb_hdd, default: true)

  # Ensure the volume calculation in all spaces is done automatically
  model.getSpaces.sort.each do |space|
    space.autocalculateVolume
  end

  apply_weather_data(model: model, epw_file: epw_file, custom_weather_folder: custom_weather_folder)
  apply_loads(model: model,
              lights_type: lights_type,
              lights_scale: lights_scale,
              occupancy_loads_scale: occupancy_loads_scale,
              electrical_loads_scale: electrical_loads_scale,
              oa_scale: oa_scale)
  apply_envelope(model: model,
                 ext_wall_cond: ext_wall_cond,
                 ext_floor_cond: ext_floor_cond,
                 ext_roof_cond: ext_roof_cond,
                 ground_wall_cond: ground_wall_cond,
                 ground_floor_cond: ground_floor_cond,
                 ground_roof_cond: ground_roof_cond,
                 door_construction_cond: door_construction_cond,
                 fixed_window_cond: fixed_window_cond,
                 glass_door_cond: glass_door_cond,
                 overhead_door_cond: overhead_door_cond,
                 skylight_cond: skylight_cond,
                 glass_door_solar_trans: glass_door_solar_trans,
                 fixed_wind_solar_trans: fixed_wind_solar_trans,
                 skylight_solar_trans: skylight_solar_trans,
                 infiltration_scale: infiltration_scale,
                 necb_hdd: necb_hdd)
  apply_fdwr_srr_daylighting(model: model,
                             fdwr_set: fdwr_set,
                             srr_set: srr_set,
                             necb_hdd: necb_hdd)
  apply_thermal_bridging(model: model,
                         tbd_option: tbd_option,
                         tbd_interpolate: tbd_interpolate,
                         wall_U: ext_wall_cond,
                         floor_U: ext_floor_cond,
                         roof_U: ext_roof_cond)
  apply_auto_zoning(model: model,
                    sizing_run_dir: sizing_run_dir,
                    lights_type: lights_type,
                    lights_scale: lights_scale)
  apply_kiva_foundation(model)
  apply_systems_and_efficiencies(model: model,
                                 sizing_run_dir: sizing_run_dir,
                                 primary_heating_fuel: primary_heating_fuel,
                                 dcv_type: dcv_type,
                                 ecm_system_name: ecm_system_name,
                                 ecm_system_zones_map_option: ecm_system_zones_map_option,
                                 erv_package: erv_package,
                                 boiler_eff: boiler_eff,
                                 unitary_cop: unitary_cop,
                                 furnace_eff: furnace_eff,
                                 shw_eff: shw_eff,
                                 daylighting_type: daylighting_type,
                                 nv_type: nv_type,
                                 nv_opening_fraction: nv_opening_fraction,
                                 nv_temp_out_min: nv_temp_out_min,
                                 nv_delta_temp_in_out: nv_delta_temp_in_out,
                                 pv_ground_type: pv_ground_type,
                                 pv_ground_total_area_pv_panels_m2: pv_ground_total_area_pv_panels_m2,
                                 pv_ground_tilt_angle: pv_ground_tilt_angle,
                                 pv_ground_azimuth_angle: pv_ground_azimuth_angle,
                                 pv_ground_module_description: pv_ground_module_description,
                                 chiller_type: chiller_type,
                                 shw_scale: shw_scale,
                                 airloop_economizer_type: airloop_economizer_type,
                                 baseline_system_zones_map_option: baseline_system_zones_map_option)
  self.set_output_variables(model: model, output_variables: output_variables)
  self.set_output_meters(model: model, output_meters: output_meters)

  return model
end

#model_attach_water_fixtures_to_spaces?(model) ⇒ Boolean

Determine whether or not water fixtures are attached to spaces

Returns:

  • (Boolean)


1172
1173
1174
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1172

def model_attach_water_fixtures_to_spaces?(model)
  return true
end

#model_create_prototype_model(template:, building_type:, epw_file:, custom_weather_folder: nil, debug: false, sizing_run_dir: Dir.pwd, primary_heating_fuel: 'Electricity', dcv_type: 'NECB_Default', lights_type: 'NECB_Default', lights_scale: 1.0, daylighting_type: 'NECB_Default', ecm_system_name: 'NECB_Default', ecm_system_zones_map_option: 'NECB_Default', erv_package: 'NECB_Default', boiler_eff: nil, unitary_cop: nil, furnace_eff: nil, shw_eff: nil, ext_wall_cond: nil, ext_floor_cond: nil, ext_roof_cond: nil, ground_wall_cond: nil, ground_floor_cond: nil, ground_roof_cond: nil, door_construction_cond: nil, fixed_window_cond: nil, glass_door_cond: nil, overhead_door_cond: nil, skylight_cond: nil, glass_door_solar_trans: nil, fixed_wind_solar_trans: nil, skylight_solar_trans: nil, rotation_degrees: nil, fdwr_set: -1.0,, srr_set: -1.0,, nv_type: nil, nv_opening_fraction: nil, nv_temp_out_min: nil, nv_delta_temp_in_out: nil, scale_x: nil, scale_y: nil, scale_z: nil, pv_ground_type: nil, pv_ground_total_area_pv_panels_m2: nil, pv_ground_tilt_angle: nil, pv_ground_azimuth_angle: nil, pv_ground_module_description: nil, chiller_type: nil, occupancy_loads_scale: nil, electrical_loads_scale: nil, oa_scale: nil, infiltration_scale: nil, output_variables: nil, shw_scale: nil, output_meters: nil, airloop_economizer_type: nil, baseline_system_zones_map_option: nil, tbd_option: nil, tbd_interpolate: false, necb_hdd: true) ⇒ Object

This method is a wrapper to create the 16 archetypes easily. # 55 args



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 200

def model_create_prototype_model(template:,
                                 building_type:,
                                 epw_file:,
                                 custom_weather_folder: nil,
                                 debug: false,
                                 sizing_run_dir: Dir.pwd,
                                 primary_heating_fuel: 'Electricity',
                                 dcv_type: 'NECB_Default',
                                 lights_type: 'NECB_Default',
                                 lights_scale: 1.0,
                                 daylighting_type: 'NECB_Default',
                                 ecm_system_name: 'NECB_Default',
                                 ecm_system_zones_map_option: 'NECB_Default',
                                 erv_package: 'NECB_Default',
                                 boiler_eff: nil,
                                 unitary_cop: nil,
                                 furnace_eff: nil,
                                 shw_eff: nil,
                                 ext_wall_cond: nil,
                                 ext_floor_cond: nil,
                                 ext_roof_cond: nil,
                                 ground_wall_cond: nil,
                                 ground_floor_cond: nil,
                                 ground_roof_cond: nil,
                                 door_construction_cond: nil,
                                 fixed_window_cond: nil,
                                 glass_door_cond: nil,
                                 overhead_door_cond: nil,
                                 skylight_cond: nil,
                                 glass_door_solar_trans: nil,
                                 fixed_wind_solar_trans: nil,
                                 skylight_solar_trans: nil,
                                 rotation_degrees: nil,
                                 fdwr_set: -1.0,
                                 srr_set: -1.0,
                                 nv_type: nil,
                                 nv_opening_fraction: nil,
                                 nv_temp_out_min: nil,
                                 nv_delta_temp_in_out: nil,
                                 scale_x: nil,
                                 scale_y: nil,
                                 scale_z: nil,
                                 pv_ground_type: nil,
                                 pv_ground_total_area_pv_panels_m2: nil,
                                 pv_ground_tilt_angle: nil,
                                 pv_ground_azimuth_angle: nil,
                                 pv_ground_module_description: nil,
                                 chiller_type: nil,
                                 occupancy_loads_scale: nil,
                                 electrical_loads_scale: nil,
                                 oa_scale: nil,
                                 infiltration_scale: nil,
                                 output_variables: nil,
                                 shw_scale: nil,
                                 output_meters: nil,
                                 airloop_economizer_type: nil,
                                 baseline_system_zones_map_option: nil,
                                 tbd_option: nil,
                                 tbd_interpolate: false,
                                 necb_hdd: true)
  model = load_building_type_from_library(building_type: building_type)
  return model_apply_standard(model: model,
                              tbd_option: tbd_option,
                              tbd_interpolate: tbd_interpolate,
                              epw_file: epw_file,
                              custom_weather_folder: custom_weather_folder,
                              sizing_run_dir: sizing_run_dir,
                              primary_heating_fuel: primary_heating_fuel,
                              dcv_type: dcv_type, # Four options: (1) 'NECB_Default', (2) 'No_DCV', (3) 'Occupancy_based_DCV' , (4) 'CO2_based_DCV'
                              lights_type: lights_type, # Two options: (1) 'NECB_Default', (2) 'LED'
                              lights_scale: lights_scale,
                              daylighting_type: daylighting_type, # Two options: (1) nil/none/false/'NECB_Default' (Option #1 puts daylighting sensors in the spaces as per NECB requirements; so some spaces may not have sensors), (2) 'add_daylighting_controls' (Option #2 puts daylighting sensors in all spaces regardless of NECB requirements)
                              ecm_system_name: ecm_system_name,
                              ecm_system_zones_map_option: ecm_system_zones_map_option, # (1) 'NECB_Default' (2) 'one_sys_per_floor' (3) 'one_sys_per_bldg'
                              erv_package: erv_package,
                              boiler_eff: boiler_eff,
                              unitary_cop: unitary_cop,
                              furnace_eff: furnace_eff,
                              shw_eff: shw_eff,
                              ext_wall_cond: ext_wall_cond,
                              ext_floor_cond: ext_floor_cond,
                              ext_roof_cond: ext_roof_cond,
                              ground_wall_cond: ground_wall_cond,
                              ground_floor_cond: ground_floor_cond,
                              ground_roof_cond: ground_roof_cond,
                              door_construction_cond: door_construction_cond,
                              fixed_window_cond: fixed_window_cond,
                              glass_door_cond: glass_door_cond,
                              overhead_door_cond: overhead_door_cond,
                              skylight_cond: skylight_cond,
                              glass_door_solar_trans: glass_door_solar_trans,
                              fixed_wind_solar_trans: fixed_wind_solar_trans,
                              skylight_solar_trans: skylight_solar_trans,
                              rotation_degrees: rotation_degrees,
                              fdwr_set: fdwr_set,
                              srr_set: srr_set,
                              nv_type: nv_type, # Two options: (1) nil/none/false/'NECB_Default', (2) 'add_nv'
                              nv_opening_fraction: nv_opening_fraction, # options: (1) nil/none/false (2) 'NECB_Default' (i.e. 0.1), (3) opening fraction of windows, which can be a float number between 0.0 and 1.0
                              nv_temp_out_min: nv_temp_out_min, # options: (1) nil/none/false(2) 'NECB_Default' (i.e. 13.0 based on inputs from Michel Tardif re a real school in QC), (3) minimum outdoor air temperature (in Celsius) below which natural ventilation is shut down
                              nv_delta_temp_in_out: nv_delta_temp_in_out, # options: (1) nil/none/false (2) 'NECB_Default' (i.e. 1.0 based on inputs from Michel Tardif re a real school in QC), (3) temperature difference (in Celsius) between the indoor and outdoor air temperatures below which ventilation is shut down
                              scale_x: scale_x,
                              scale_y: scale_y,
                              scale_z: scale_z,
                              pv_ground_type: pv_ground_type, # Two options: (1) nil/none/false/'NECB_Default', (2) 'add_pv_ground'
                              pv_ground_total_area_pv_panels_m2: pv_ground_total_area_pv_panels_m2, # Options: (1) nil/none/false, (2) 'NECB_Default' (i.e. building footprint), (3) area value (e.g. 50)
                              pv_ground_tilt_angle: pv_ground_tilt_angle, # Options: (1) nil/none/false, (2) 'NECB_Default' (i.e. latitude), (3) tilt angle value (e.g. 20)
                              pv_ground_azimuth_angle: pv_ground_azimuth_angle, # Options: (1) nil/none/false, (2) 'NECB_Default' (i.e. south), (3) azimuth angle value (e.g. 90)
                              pv_ground_module_description: pv_ground_module_description, # Options: (1) nil/none/false, (2) 'NECB_Default' (i.e. Standard), (3) other options ('Standard', 'Premium', ThinFilm')
                              occupancy_loads_scale: occupancy_loads_scale,
                              electrical_loads_scale: electrical_loads_scale,
                              oa_scale: oa_scale,
                              infiltration_scale: infiltration_scale,
                              chiller_type: chiller_type, # Options: (1) 'NECB_Default'/nil/'none'/false (i.e. do nothing), (2) e.g. 'VSD'
                              output_variables: output_variables,
                              shw_scale: shw_scale,  # Options: (1) 'NECB_Default'/nil/'none'/false (i.e. do nothing), (2) a float number larger than 0.0
                              output_meters: output_meters,
                              airloop_economizer_type: airloop_economizer_type, # (1) 'NECB_Default'/nil/' (2) 'DifferentialEnthalpy' (3) 'DifferentialTemperature'
                              baseline_system_zones_map_option: baseline_system_zones_map_option,  # Three options: (1) 'NECB_Default'/'none'/nil (i.e. 'one_sys_per_bldg'), (2) 'one_sys_per_dwelling_unit', (3) 'one_sys_per_bldg'
                              necb_hdd: necb_hdd
                              )

end

#model_create_thermal_zones(model, space_multiplier_map = nil) ⇒ Boolean

Creates thermal zones to contain each space, as defined for each building in the system_to_space_map inside the Prototype.building_name e.g. (Prototype.secondary_school.rb) file.

Returns:

  • (Boolean)

    returns true if successful, false if not



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 8

def model_create_thermal_zones(model, space_multiplier_map = nil)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started creating thermal zones')
  space_multiplier_map = {} if space_multiplier_map.nil?

  # Remove any Thermal zones assigned
  model.getThermalZones.each(&:remove)

  # Create a thermal zone for each space in the self
  model.getSpaces.sort.each do |space|
    zone = OpenStudio::Model::ThermalZone.new(model)
    zone.setName("#{space.name} ZN")
    unless space_multiplier_map[space.name.to_s].nil? || (space_multiplier_map[space.name.to_s] == 1)
      zone.setMultiplier(space_multiplier_map[space.name.to_s])
    end
    space.setThermalZone(zone)

    # Skip thermostat for spaces with no space type
    next if space.spaceType.empty?

    # Add a thermostat
    space_type_name = space.spaceType.get.name.get
    thermostat_name = space_type_name + ' Thermostat'
    thermostat = model.getThermostatSetpointDualSetpointByName(thermostat_name)
    if thermostat.empty?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Thermostat #{thermostat_name} not found for space name: #{space.name}")
    else
      thermostat_clone = thermostat.get.clone(model).to_ThermostatSetpointDualSetpoint.get
      zone.setThermostatSetpointDualSetpoint(thermostat_clone)
      # Set Ideal loads to thermal zone for sizing for NECB needs. We need this for sizing.
      ideal_loads = OpenStudio::Model::ZoneHVACIdealLoadsAirSystem.new(model)
      ideal_loads.addToThermalZone(zone)
    end
  end

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished creating thermal zones')
end

#model_enable_demand_controlled_ventilation(model, dcv_type = 'No_DCV') ⇒ Object

Note: Values for dcv_type are: ‘Occupancy_based_DCV’, ‘CO2_based_DCV’, ‘No_DCV’, ‘NECB_Default’



1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1730

def model_enable_demand_controlled_ventilation(model, dcv_type = 'No_DCV')
  return if dcv_type == 'NECB_Defualt'

  if dcv_type == 'Occupancy_based_DCV' || dcv_type == 'CO2_based_DCV'
    # @todo IMPORTANT: (upon other BTAP tasks) Set a value for the "Outdoor Air Flow per Person" field of the "OS:DesignSpecification:OutdoorAir" object
    # Note: The "Outdoor Air Flow per Person" field is required for occupancy-based DCV.
    # Note: The "Outdoor Air Flow per Person" values should be based on ASHRAE 62.1: Article 6.2.2.1.
    # Note: The "Outdoor Air Flow per Person" should be entered for "ventilation_per_person" in "lib/openstudio-standards/standards/necb/NECB2011/data/space_types.json"

    ##### Define indoor CO2 availability schedule (required for CO2-based DCV)
    ##### Reference: see page B.13 of PNNL (2017), "Impacts of Commercial Building Controls on Energy Savings and Peak Load Reduction", available a: https://www.energy.gov/eere/buildings/downloads/impacts-commercial-building-controls-energy-savings-and-peak-load-reduction
    ##### Note: the defined schedule here is redundant as the schedule says it is always on AND
    ##### the "ZoneControl:ContaminantController" object says that "If this field is left blank, the schedule has a value of 1 for all time periods".
    indoor_co2_availability_schedule = OpenStudio::Model::ScheduleCompact.new(model)
    indoor_co2_availability_schedule.setName('indoor_co2_availability_schedule')
    indoor_co2_availability_schedule.setScheduleTypeLimits(BTAP::Resources::Schedules::StandardScheduleTypeLimits.get_fraction(model))
    indoor_co2_availability_schedule.to_ScheduleCompact.get
    # indoor_co2_availability_schedule.setString(1,"indoor_co2_availability_schedule")
    indoor_co2_availability_schedule.setString(3, 'Through: 12/31')
    indoor_co2_availability_schedule.setString(4, 'For: Weekdays SummerDesignDay')
    indoor_co2_availability_schedule.setString(5, 'Until: 07:00')
    indoor_co2_availability_schedule.setString(6, '0.0')
    indoor_co2_availability_schedule.setString(7, 'Until: 22:00')
    indoor_co2_availability_schedule.setString(8, '1.0')
    indoor_co2_availability_schedule.setString(9, 'Until: 24:00')
    indoor_co2_availability_schedule.setString(10, '0.0')
    indoor_co2_availability_schedule.setString(11, 'For: Saturday WinterDesignDay')
    indoor_co2_availability_schedule.setString(12, 'Until: 07:00')
    indoor_co2_availability_schedule.setString(13, '0.0')
    indoor_co2_availability_schedule.setString(14, 'Until: 18:00')
    indoor_co2_availability_schedule.setString(15, '1.0')
    indoor_co2_availability_schedule.setString(16, 'Until: 24:00')
    indoor_co2_availability_schedule.setString(17, '0.0')
    indoor_co2_availability_schedule.setString(18, 'For: AllOtherDays')
    indoor_co2_availability_schedule.setString(19, 'Until: 24:00')
    indoor_co2_availability_schedule.setString(20, '0.0')

    ##### Define indoor CO2 setpoint schedule (required for CO2-based DCV)
    ##### Reference: see page B.13 of PNNL (2017), "Impacts of Commercial Building Controls on Energy Savings and Peak Load Reduction", available a: https://www.energy.gov/eere/buildings/downloads/impacts-commercial-building-controls-energy-savings-and-peak-load-reduction
    indoor_co2_setpoint_schedule = OpenStudio::Model::ScheduleCompact.new(model)
    indoor_co2_setpoint_schedule.setName('indoor_co2_setpoint_schedule')
    indoor_co2_setpoint_schedule.setScheduleTypeLimits(get_any_number_ppm(model))
    indoor_co2_setpoint_schedule.to_ScheduleCompact.get
    indoor_co2_setpoint_schedule.setString(3, 'Through: 12/31')
    indoor_co2_setpoint_schedule.setString(4, 'For: AllDays')
    indoor_co2_setpoint_schedule.setString(5, 'Until: 24:00')
    indoor_co2_setpoint_schedule.setString(6, '1000.0')
    # indoor_co2_setpoint_schedule.setToConstantValue(1000.0) #1000 ppm

    ##### Define outdoor CO2 schedule (required for CO2-based DCV
    ##### Reference: see page B.13 of PNNL (2017), "Impacts of Commercial Building Controls on Energy Savings and Peak Load Reduction", available a: https://www.energy.gov/eere/buildings/downloads/impacts-commercial-building-controls-energy-savings-and-peak-load-reduction
    outdoor_co2_schedule = OpenStudio::Model::ScheduleCompact.new(model)
    outdoor_co2_schedule.setName('outdoor_co2_schedule')
    outdoor_co2_schedule.setScheduleTypeLimits(get_any_number_ppm(model))
    outdoor_co2_schedule.to_ScheduleCompact.get
    outdoor_co2_schedule.setString(3, 'Through: 12/31')
    outdoor_co2_schedule.setString(4, 'For: AllDays')
    outdoor_co2_schedule.setString(5, 'Until: 24:00')
    outdoor_co2_schedule.setString(6, '400.0')
    # outdoor_co2_schedule.setToConstantValue(400.0) #400 ppm

    ##### Define ZoneAirContaminantBalance (required for CO2-based DCV)
    zone_air_contaminant_balance = model.getZoneAirContaminantBalance
    zone_air_contaminant_balance.setCarbonDioxideConcentration(true)
    zone_air_contaminant_balance.setOutdoorCarbonDioxideSchedule(outdoor_co2_schedule)

    ##### Set CO2 controller in each space (required for CO2-based DCV)
    model.getSpaces.sort.each do |space|
      # puts space.name.to_s
      zone = space.thermalZone
      if !zone.empty?
        zone = space.thermalZone.get
      end
      zone_control_co2 = OpenStudio::Model::ZoneControlContaminantController.new(zone.model)
      zone_control_co2.setName("#{space.name} Zone Control Contaminant Controller")
      zone_control_co2.setCarbonDioxideControlAvailabilitySchedule(indoor_co2_availability_schedule)
      zone_control_co2.setCarbonDioxideSetpointSchedule(indoor_co2_setpoint_schedule)
      zone.setZoneControlContaminantController(zone_control_co2)
    end
    # if dcv_type == "Occupancy_based_DCV" || dcv_type == "CO2_based_DCV"
  end

  ##### Loop through AirLoopHVACs
  model.getAirLoopHVACs.sort.each do |air_loop|
    ##### Loop through AirLoopHVAC's supply nodes to:
    ##### (1) Find its AirLoopHVAC:OutdoorAirSystem using the supply node;
    ##### (2) Find Controller:OutdoorAir using AirLoopHVAC:OutdoorAirSystem;
    ##### (3) Get "Controller Mechanical Ventilation" from Controller:OutdoorAir.
    air_loop.supplyComponents.sort.each do |supply_component|
      ##### Find AirLoopHVAC:OutdoorAirSystem of AirLoopHVAC using the supply node.
      hvac_component = supply_component.to_AirLoopHVACOutdoorAirSystem

      if !hvac_component.empty?
        ##### Find Controller:OutdoorAir using AirLoopHVAC:OutdoorAirSystem.
        hvac_component = hvac_component.get
        controller_oa = hvac_component.getControllerOutdoorAir

        ##### Get "Controller Mechanical Ventilation" from Controller:OutdoorAir.
        controller_mv = controller_oa.controllerMechanicalVentilation

        ##### Set "Demand Controlled Ventilation" to "Yes" or "No" in Controller:MechanicalVentilation depending on dcv_type.
        if (dcv_type == 'CO2_based_DCV') || (dcv_type == 'Occupancy_based_DCV') # Occupancy
          controller_mv.setDemandControlledVentilation(true)
          ##### Set the "System Outdoor Air Method" field based on dcv_type in the Controller:MechanicalVentilation object
          if dcv_type == 'CO2_based_DCV'
            controller_mv.setSystemOutdoorAirMethod('IndoorAirQualityProcedure')
          else # dcv_type == 'Occupancy_based_DCV'
            controller_mv.setSystemOutdoorAirMethod('ZoneSum')
          end
        elsif dcv_type == 'No_DCV'
          controller_mv.setDemandControlledVentilation(false)
        end
        # puts controller_mv
        # if !hvac_component.empty?
      end
      # air_loop.supplyComponents.each do |supply_component|
    end
    # model.getAirLoopHVACs.each do |air_loop|
  end
end

#model_find_climate_zone_set(model, clim) ⇒ Object

Helper method to find out which climate zone set contains a specific climate zone. Returns climate zone set name as String if success, nil if not found.



1646
1647
1648
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1646

def model_find_climate_zone_set(model, clim)
  return 'NECB-CNEB ClimatZone 4-8'
end

#necb_design_supply_temp_compliance(qaqc) ⇒ Object



1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1466

def necb_design_supply_temp_compliance(qaqc)
  necb_section_name = get_qaqc_table(table_name: 'design_supply_temp_compliance')['refs'].join(',')
  qaqc_table = get_qaqc_table(table_name: 'design_supply_temp_compliance')
  tolerance = 3
  qaqc[:thermal_zones].each do |zoneinfo|
    #    skipping undefined schedules
    if (qaqc_table['exclude']['exclude_string'].any? { |ex_string| zoneinfo[:name].to_s.include? ex_string }) && !qaqc_table['exclude']['exclude_string'].empty?
      puts "#{zoneinfo[:name]} was skipped in necb_zone_sizing_compliance because it contains #{qaqc_table['exclude']['exclude_string'].join(',')}"
      next
    end
    design_supply_temp_compliance = qaqc_table['table']

    design_supply_temp_compliance.each do |compliance|
      if compliance['var'] == 'heating_design_supply_air_temp'
        result_value = zoneinfo[:zone_heating_design_supply_air_temperature]
      elsif compliance['var'] == 'cooling_design_supply_temp'
        result_value = zoneinfo[:zone_cooling_design_supply_air_temperature]
      end

      next if result_value.nil?

      test_text = "[ZONE][#{zoneinfo[:name]}] #{compliance['var']}"
      # puts key
      necb_section_test(
        qaqc,
        result_value,
        compliance['bool_operator'],
        compliance['expected_value'],
        necb_section_name,
        test_text,
        tolerance
      )
    end
  end
  # Design supply temp test
  # necb_section_name = "NECB2011-?"
  # round_precision = 3
  # qaqc[:thermal_zones].each do |zoneinfo|
  #   #    skipping undefined schedules
  #   if zoneinfo[:name].to_s.include?"- undefined -"
  #     next
  #   end
  #   data = {}
  #   #data[:heating_sizing_factor] = [1.3 , zoneinfo[:heating_sizing_factor]]
  #   #data[:cooling_sizing_factor] = [1.1 ,zoneinfo[:cooling_sizing_factor]]
  #   data[:heating_design_supply_air_temp] =   [43.0, zoneinfo[:zone_heating_design_supply_air_temperature] ] #unless zoneinfo[:zone_heating_design_supply_air_temperature].nil?
  #   data[:cooling_design_supply_temp]   =   [13.0, zoneinfo[:zone_cooling_design_supply_air_temperature] ]
  #   data.each do |key,value|
  #     #puts key
  #     necb_section_test(
  #       qaqc,
  #       value[0],
  #       '==',
  #       value[1],
  #       necb_section_name,
  #       "[ZONE][#{zoneinfo[:name]}] #{key}",
  #       round_precision
  #     )
  #   end
  # end
end

#necb_economizer_compliance(qaqc) ⇒ Object



1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1528

def necb_economizer_compliance(qaqc)
  # determine correct economizer usage according to section 5.2.2.7 of NECB2011
  necb_section_name = get_qaqc_table(table_name: 'economizer_compliance')['refs'].join(',')
  qaqc_table = get_qaqc_table(table_name: 'economizer_compliance') # stores the full hash of qaqc for economizer_compliance
  # necb_section_name = "NECB2011-5.2.2.7"

  qaqc[:air_loops].each do |air_loop_info|
    capacity = -1.0
    if !air_loop_info[:cooling_coils][:dx_single_speed][0].nil?
      puts 'capacity = air_loop_info[:cooling_coils][:dx_single_speed][0][:nominal_total_capacity_w]'
      capacity = air_loop_info[:cooling_coils][:dx_single_speed][0][:nominal_total_capacity_w]
    elsif !air_loop_info[:cooling_coils][:dx_two_speed][0].nil?
      puts 'capacity = air_loop_info[:cooling_coils][:dx_two_speed][0][:cop_high]'
      capacity = air_loop_info[:cooling_coils][:dx_two_speed][0][:cop_high]
    elsif !air_loop_info[:cooling_coils][:coil_cooling_water][0].nil?
      puts 'capacity = air_loop_info[:cooling_coils][:coil_cooling_water][0][:nominal_total_capacity_w]'
      capacity = air_loop_info[:cooling_coils][:coil_cooling_water][0][:nominal_total_capacity_w]
    end
    puts capacity
    if capacity == -1.0
      # This should not happen
      qaqc[:errors] << "[necb_economizer_compliance] air_loop_info[:cooling_coils] for #{air_loop_info[:name]} does not have a capacity "
    else
      # check for correct economizer usage
      # puts "air_loop_info[:supply_fan][:max_air_flow_rate]: #{air_loop_info[:supply_fan][:max_air_flow_rate]}"
      unless air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] == -1.0
        # capacity should be in kW
        max_air_flow_rate_m3_per_s = air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s]
        necb_section_test(
          qaqc,
          eval(qaqc_table['table'][0]['expected_value']),
          '==',
          air_loop_info[:economizer][:control_type],
          necb_section_name,
          "[AIR LOOP][#{air_loop_info[:name]}][:economizer][:control_type]"
        )
      end
    end
  end
end

#necb_envelope_compliance(qaqc) ⇒ Object

checks envelope compliance fenestration_to_door_and_window_percentage, skylight_to_roof_percentage



1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1164

def necb_envelope_compliance(qaqc)
  # Envelope
  necb_section_name = 'NECB2011-Section 3.2.1.4'
  # store hdd in short form
  hdd = qaqc[:geography][:hdd]
  # calculate fdwr based on hdd.
  fdwr = 0
  if hdd < 4000
    fdwr = 0.40
  elsif (hdd >= 4000) && (hdd <= 7000)
    fdwr = (2000 - 0.2 * hdd) / 3000
  elsif hdd > 7000
    fdwr = 0.20
  end
  # hardset srr to 0.05
  srr = 0.05
  # create table of expected values and results.
  data = {}
  data[:fenestration_to_door_and_window_percentage] = [fdwr * 100, qaqc[:envelope][:fdwr].round(3)]
  data[:skylight_to_roof_percentage] = [srr * 100, qaqc[:envelope][:srr].round(3)]
  # perform test. result must be less than or equal to.
  data.each do |key, value|
    necb_section_test(
      qaqc,
      value[0],
      '>=',
      value[1],
      necb_section_name,
      "[ENVELOPE]#{key}",
      1 # padmassun added tollerance
    )
  end
end

#necb_exterior_fenestration_compliance(qaqc) ⇒ Object



1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1304

def necb_exterior_fenestration_compliance(qaqc)
  # Exterior Fenestration
  necb_section_name = get_qaqc_table(table_name: 'exterior_fenestration_compliance')['refs'].join(',')
  climate_index = NECB2011.new.get_climate_zone_index(qaqc[:geography][:hdd])
  tolerance = 3
  # puts "\n\n"
  # puts "climate_index: #{climate_index}"
  # puts get_qaqc_table("exterior_fenestration_compliance", {"var" => "ext_window_conductances", "climate_index" => 2})

  ['ext_window_conductances', 'ext_door_conductances', 'ext_overhead_door_conductances', 'ext_skylight_conductances'].each do |compliance_var|
    qaqc_table = get_qaqc_table(table_name: 'exterior_fenestration_compliance', search_criteria: { 'var' => compliance_var, 'climate_index' => climate_index }).first
    # puts "\n#{qaqc_table}\n"
    if compliance_var == 'ext_window_conductances'
      result_value = qaqc[:envelope][:windows_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ext_door_conductances'
      result_value = qaqc[:envelope][:doors_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ext_overhead_door_conductances'
      result_value = qaqc[:envelope][:overhead_doors_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ext_skylight_conductances'
      result_value = qaqc[:envelope][:skylights_average_conductance_w_per_m2_k]
    end
    test_text = "[ENVELOPE] #{compliance_var}"
    next if result_value.nil?

    necb_section_test(
      qaqc,
      result_value,
      qaqc_table['bool_operator'],
      qaqc_table['expected_value'],
      necb_section_name,
      test_text,
      tolerance
    )
  end
  # necb_section_name = "NECB2011-Section 3.2.2.3"
  # climate_index = BTAP::Compliance::NECB2011::get_climate_zone_index(qaqc[:geography][:hdd])
  # result_value_index = 6
  # round_precision = 3
  # data = {}
  # data[:ext_window_conductances]      =     [2.400,2.200,2.200,2.200,2.200,1.600,qaqc[:envelope][:windows_average_conductance_w_per_m2_k]] unless qaqc[:envelope][:windows_average_conductance_w_per_m2_k].nil?
  # data[:ext_door_conductances]        =     [2.400,2.200,2.200,2.200,2.200,1.600,qaqc[:envelope][:doors_average_conductance_w_per_m2_k]]   unless qaqc[:envelope][:doors_average_conductance_w_per_m2_k].nil?
  # data[:ext_overhead_door_conductances] =   [2.400,2.200,2.200,2.200,2.200,1.600,qaqc[:envelope][:overhead_doors_average_conductance_w_per_m2_k]] unless qaqc[:envelope][:overhead_doors_average_conductance_w_per_m2_k].nil?
  # data[:ext_skylight_conductances]  =       [2.400,2.200,2.200,2.200,2.200,1.600,qaqc[:envelope][:skylights_average_conductance_w_per_m2_k]] unless qaqc[:envelope][:skylights_average_conductance_w_per_m2_k].nil?

  # data.each do |key,value|
  #   #puts key
  #   necb_section_test(
  #     qaqc,
  #     value[result_value_index].round(round_precision),
  #     '==',
  #     value[climate_index].round(round_precision),
  #     necb_section_name,
  #     "[ENVELOPE]#{key}",
  #     round_precision
  #   )
  # end
end

#necb_exterior_ground_surfaces_compliance(qaqc) ⇒ Object



1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1362

def necb_exterior_ground_surfaces_compliance(qaqc)
  # Exterior Ground surfaces
  necb_section_name = get_qaqc_table(table_name: 'exterior_ground_surfaces_compliance')['refs'].join(',')
  climate_index = NECB2011.new.get_climate_zone_index(qaqc[:geography][:hdd])
  tolerance = 3
  # puts "\n\n"
  # puts "climate_index: #{climate_index}"
  # puts get_qaqc_table("exterior_ground_surfaces_compliance", {"var" => "ground_wall_conductances", "climate_index" => 2})

  ['ground_wall_conductances', 'ground_roof_conductances', 'ground_floor_conductances'].each do |compliance_var|
    qaqc_table = get_qaqc_table(table_name: 'exterior_ground_surfaces_compliance', search_criteria: { 'var' => compliance_var, 'climate_index' => climate_index }).first
    # puts "\n#{qaqc_table}\n"
    if compliance_var == 'ground_wall_conductances'
      result_value = qaqc[:envelope][:ground_walls_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ground_roof_conductances'
      result_value = qaqc[:envelope][:ground_roofs_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ground_floor_conductances'
      result_value = qaqc[:envelope][:ground_floors_average_conductance_w_per_m2_k]
    end
    test_text = "[ENVELOPE] #{compliance_var}"
    next if result_value.nil?

    necb_section_test(
      qaqc,
      result_value,
      qaqc_table['bool_operator'],
      qaqc_table['expected_value'],
      necb_section_name,
      test_text,
      tolerance
    )
  end
  # necb_section_name = "NECB2011-Section 3.2.3.1"
  # climate_index = BTAP::Compliance::NECB2011::get_climate_zone_index(qaqc[:geography][:hdd])
  # result_value_index = 6
  # round_precision = 3
  # data = {}
  # data[:ground_wall_conductances]  = [ 0.568,0.379,0.284,0.284,0.284,0.210, qaqc[:envelope][:ground_walls_average_conductance_w_per_m2_k] ]  unless qaqc[:envelope][:ground_walls_average_conductance_w_per_m2_k].nil?
  # data[:ground_roof_conductances]  = [ 0.568,0.379,0.284,0.284,0.284,0.210, qaqc[:envelope][:ground_roofs_average_conductance_w_per_m2_k] ]  unless qaqc[:envelope][:ground_roofs_average_conductance_w_per_m2_k].nil?
  # data[:ground_floor_conductances] = [ 0.757,0.757,0.757,0.757,0.757,0.379, qaqc[:envelope][:ground_floors_average_conductance_w_per_m2_k] ] unless qaqc[:envelope][:ground_floors_average_conductance_w_per_m2_k].nil?
  # data.each {|key,value| necb_section_test(
  #     qaqc,
  #     value[result_value_index],
  #     '==',
  #     value[climate_index],
  #     necb_section_name,
  #     "[ENVELOPE]#{key}",
  #     round_precision
  #   )
  # }
end

#necb_exterior_opaque_compliance(qaqc) ⇒ Object



1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1251

def necb_exterior_opaque_compliance(qaqc)
  # puts JSON.pretty_generate @qaqc_data
  # Exterior Opaque
  necb_section_name = get_qaqc_table(table_name: 'exterior_opaque_compliance')['refs'].join(',')
  climate_index = NECB2011.new.get_climate_zone_index(qaqc[:geography][:hdd])
  puts "HDD #{qaqc[:geography][:hdd]}"
  tolerance = 3
  # puts "\n\n"
  # puts "climate_index: #{climate_index}"
  # puts get_qaqc_table("exterior_opaque_compliance", {"var" => "ext_wall_conductances", "climate_index" => 2})
  ['ext_wall_conductances', 'ext_roof_conductances', 'ext_floor_conductances'].each do |compliance_var|
    qaqc_table = get_qaqc_table(table_name: 'exterior_opaque_compliance', search_criteria: { 'var' => compliance_var, 'climate_index' => climate_index }).first
    # puts "\n#{qaqc_table}\n"
    if compliance_var == 'ext_wall_conductances'
      result_value = qaqc[:envelope][:outdoor_walls_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ext_roof_conductances'
      result_value = qaqc[:envelope][:outdoor_floors_average_conductance_w_per_m2_k]
    elsif compliance_var == 'ext_floor_conductances'
      result_value = qaqc[:envelope][:outdoor_roofs_average_conductance_w_per_m2_k]
    end

    test_text = "[ENVELOPE] #{compliance_var}"
    next if result_value.nil?

    necb_section_test(
      qaqc,
      result_value,
      qaqc_table['bool_operator'],
      qaqc_table['expected_value'],
      necb_section_name,
      test_text,
      tolerance
    )
  end
  # result_value_index = 6
  # round_precision = 3
  # data = {}
  # data[:ext_wall_conductances]        =  [0.315,0.278,0.247,0.210,0.210,0.183,qaqc[:envelope][:outdoor_walls_average_conductance_w_per_m2_k]] unless qaqc[:envelope][:outdoor_walls_average_conductance_w_per_m2_k].nil?
  # data[:ext_roof_conductances]        =  [0.227,0.183,0.183,0.162,0.162,0.142,qaqc[:envelope][:outdoor_roofs_average_conductance_w_per_m2_k]] unless qaqc[:envelope][:outdoor_roofs_average_conductance_w_per_m2_k].nil?
  # data[:ext_floor_conductances]       =  [0.227,0.183,0.183,0.162,0.162,0.142,qaqc[:envelope][:outdoor_floors_average_conductance_w_per_m2_k]] unless qaqc[:envelope][:outdoor_floors_average_conductance_w_per_m2_k].nil?

  # data.each {|key,value| necb_section_test(
  #     qaqc,
  #     value[result_value_index],
  #     '==',
  #     value[climate_index],
  #     necb_section_name,
  #     "[ENVELOPE]#{key}",
  #     round_precision
  #   )
  # }
end

#necb_hrv_compliance(qaqc, model) ⇒ Object



1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1569

def necb_hrv_compliance(qaqc, model)
  # HRV check
  hrv_compliance = get_qaqc_table(table_name: 'hrv_compliance')['table']
  necb_section_name = get_qaqc_table(table_name: 'hrv_compliance')['refs'].join(',')
  qaqc[:air_loops].each do |air_loop_info|
    hrv_compliance.each do |compliance|
      data = {}

      # puts "\nspaceinfo[#{compliance['var']}]"
      result_value = !air_loop_info[:heat_exchanger].empty?
      # puts "#{compliance['test_text']}"
      test_text = "[AIR LOOP][:heat_exchanger] for [#{air_loop_info[:name]}] is present?"
      # puts "result_value: #{result_value}"
      # puts "test_text: #{test_text}\n"
      # data[:infiltration_method]    = [ "Flow/ExteriorArea", spaceinfo[:infiltration_method] , nil ]
      # data[:infiltration_flow_per_m2] = [ 0.00025,       spaceinfo[:infiltration_flow_per_m2], 5 ]
      # data.each do |key,value|
      # puts key
      outdoor_air_L_per_s = air_loop_info[:outdoor_air_L_per_s]
      weather_file_path = model.weatherFile.get.path.get.to_s
      stat_file_path = weather_file_path.gsub('.epw', '.stat')
      stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
      db990 = stat_file.heating_design_info[2]
      necb_section_test(
        qaqc,
        result_value,
        '==',
        eval(compliance['expected_value']),
        necb_section_name,
        test_text,
        compliance['tolerance']
      )
    end
  end
  # necb_section_name = "NECB2011-5.2.10.1"
  # qaqc[:air_loops].each do |air_loop_info|
  #   unless air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] == -1.0
  #     weather_file_path = model.weatherFile.get.path.get.to_s
  #     stat_file_path = weather_file_path.gsub('.epw', '.stat')
  #     stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
  #     db990 = stat_file.heating_design_info[2]
  #     hrv_calc = 0.00123 * air_loop_info[:outdoor_air_L_per_s] * (21 - db990) #=AP46*(21-O$1)
  #     hrv_reqd = hrv_calc > 150 ? true : false
  #     #qaqc[:information] << "[Info][TEST-PASS][#{necb_section_name}]:#{test_text} result value:#{result_value} #{bool_operator} expected value:#{expected_value}"
  #     hrv_present = false
  #     unless air_loop_info[:heat_exchanger].empty?
  #       hrv_present = true
  #     end
  #     necb_section_test(
  #       qaqc,
  #       hrv_reqd,
  #       '==',
  #       hrv_present,
  #       necb_section_name,
  #       "[AIR LOOP][:heat_exchanger] for [#{air_loop_info[:name]}] is present?"
  #     )
  #   else
  #     qaqc['warnings'] << "[hrv_compliance] air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] == -1.0 for [#{air_loop_info[:name]}]"
  #   end
  # end
end

#necb_hrv_compliance_for_single_airloop(qaqc, model, air_loop_info) ⇒ Object

This methos is not used as part of the QAQC process, because support for MURBS has not been implemented for HRV in NECB 2011 and 2015

This method will run the HRV compliance for a single air loop

Parameters:

  • qaqc (:hash)

    Hash that contains the base data with qaqc keys

  • model (:OS:Model)

    OpenStudio Model

  • air_loop_info (:hash)

    single air_loop object from the qaqc hash



1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1724

def necb_hrv_compliance_for_single_airloop(qaqc, model, air_loop_info)
  # HRV check
  hrv_compliance = get_qaqc_table('hrv_compliance')['table']
  necb_section_name = get_qaqc_table('hrv_compliance')['refs'].join(',')
  hrv_compliance.each do |compliance|
    data = {}

    # puts "\nspaceinfo[#{compliance['var']}]"
    result_value = !air_loop_info[:heat_exchanger].empty?
    # puts "#{compliance['test_text']}"
    test_text = "[AIR LOOP][:heat_exchanger] for [#{air_loop_info[:name]}] is present?"
    # puts "result_value: #{result_value}"
    # puts "test_text: #{test_text}\n"
    # data[:infiltration_method]    = [ "Flow/ExteriorArea", spaceinfo[:infiltration_method] , nil ]
    # data[:infiltration_flow_per_m2] = [ 0.00025,       spaceinfo[:infiltration_flow_per_m2], 5 ]
    # data.each do |key,value|
    # puts key
    outdoor_air_L_per_s = air_loop_info[:outdoor_air_L_per_s]
    weather_file_path = model.weatherFile.get.path.get.to_s
    stat_file_path = weather_file_path.gsub('.epw', '.stat')
    stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
    db990 = stat_file.heating_design_info[2]
    necb_section_test(
      qaqc,
      result_value,
      '==',
      eval(compliance['expected_value']),
      necb_section_name,
      test_text,
      compliance['tolerance']
    )
  end
  # necb_section_name = "NECB2011-5.2.10.1"
  # qaqc[:air_loops].each do |air_loop_info|
  #   unless air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] == -1.0
  #     weather_file_path = model.weatherFile.get.path.get.to_s
  #     stat_file_path = weather_file_path.gsub('.epw', '.stat')
  #     stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
  #     db990 = stat_file.heating_design_info[2]
  #     hrv_calc = 0.00123 * air_loop_info[:outdoor_air_L_per_s] * (21 - db990) #=AP46*(21-O$1)
  #     hrv_reqd = hrv_calc > 150 ? true : false
  #     #qaqc[:information] << "[Info][TEST-PASS][#{necb_section_name}]:#{test_text} result value:#{result_value} #{bool_operator} expected value:#{expected_value}"
  #     hrv_present = false
  #     unless air_loop_info[:heat_exchanger].empty?
  #       hrv_present = true
  #     end
  #     necb_section_test(
  #       qaqc,
  #       hrv_reqd,
  #       '==',
  #       hrv_present,
  #       necb_section_name,
  #       "[AIR LOOP][:heat_exchanger] for [#{air_loop_info[:name]}] is present?"
  #     )
  #   else
  #     qaqc['warnings'] << "[hrv_compliance] air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] == -1.0 for [#{air_loop_info[:name]}]"
  #   end
  # end
end

#necb_hrv_compliance_inc_murb(qaqc, model) ⇒ Object

This methos is not used as part of the QAQC process, because support for MURBS has not been implemented for HRV in NECB 2011 and 2015



1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1634

def necb_hrv_compliance_inc_murb(qaqc, model)
  murb_hrv_compliance = get_qaqc_table('murb_hrv_compliance')
  hrv_spacetpye_ignore_regex = murb_hrv_compliance['ignored_spacetypes_regex']
  hrv_dwelling_unit_spacetpye_regex = murb_hrv_compliance['dwelling_unit_spacetype_regex']
  necb_section_name = murb_hrv_compliance['refs'].join(',')

  model.getAirLoopHVACs.sort.each do |air_loop|
    air_loop_info = {}

    qaqc[:air_loops].each do |air_loop_i|
      next unless air_loop_i[:name] == air_loop.name.get

      air_loop_info = air_loop_i
    end

    zones = air_loop.thermalZones
    if zones.length == 1
      # here the Airloop is serving only one zone

      # So, next we need to determine if the zone has only
      # one dwelling unit and no other space types other than stairs, corridor, and lobby

      zone = zones.first
      # get the spaces and keep track of the number of spaces and dewlling units
      num_of_served_spaces = zone.spaces.length
      contains_dwelling_unit = false
      if num_of_served_spaces == 0
        qaqc[:warnings] << "[necb_murb_hrv_compliance] Thermal Zone [#{zone.name}] does not serve any Spaces"
      else
        spaces = zone.spaces()
        spaces.each do |z_space|
          spacetype = z_space.spaceType
          spacetype = validate_optional(spacetype, model, nil)
          if spacetype.nil?
            qaqc[:warnings] << "[necb_murb_hrv_compliance] Space [#{z_space.name}] does not have a SpaceType"
          else
            # reduce the number of spaces if the space served by the thermal zone is a
            # stairwell/staircase/lobby/corridor
            spacetype_name = spacetype.name.to_s
            ignored_spacetypes_regex = Regexp.new(hrv_spacetpye_ignore_regex, Regexp::IGNORECASE)
            if ignored_spacetypes_regex =~ spacetype_name
              num_of_served_spaces -= 1
            end
            # detect is the thermal zone serves a Dwelling Unit
            dwelling_unit_regex = Regexp.new(hrv_dwelling_unit_spacetpye_regex, Regexp::IGNORECASE)
            if dwelling_unit_regex =~ spacetype_name
              contains_dwelling_unit = true
            end
          end
        end
        if (num_of_served_spaces == 1) && contains_dwelling_unit
          # here the Thermal zone serves one space that is a dwelling unit
          # and other space types such as lobby, stairs, or corridors are ignored
          # So in this case, an HRV is required
          test_text = "[AIR LOOP][:heat_exchanger] (murb) for [#{air_loop_info[:name]}] is present?"
          result_value = murb_hrv_compliance['table']['expected_value']
          necb_section_test(
            qaqc,
            result_value,
            '==',
            true,
            necb_section_name,
            test_text,
            nil
          )
        else
          # Here either the number of served spaces exceed 1 or
          # does not contain a dwelling unit, So a regular HRV check has to be done for this air loop
          qaqc[:warnings] << "[necb_murb_hrv_compliance] Regular HRV compliance check for airloop: [#{air_loop.name}], because (it does not serve a single dwelling unit) OR (serves multiple spacetypes)"
          necb_hrv_compliance_for_single_airloop(qaqc, model, air_loop_info)
        end
      end
    else
      # here the Airloop does not serve any zones, or it serves more than one zone
      # So, a regular hrv compliance must be done for this air loop
      qaqc[:warnings] << "[necb_murb_hrv_compliance] Regular HRV compliance check for airloop: [#{air_loop.name}], because it serves multiple Thermal zones"
      necb_hrv_compliance_for_single_airloop(qaqc, model, air_loop_info)
    end
  end
end

#necb_infiltration_compliance(qaqc, model) ⇒ Object



1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1198

def necb_infiltration_compliance(qaqc, model)
  # Infiltration
  # puts "\n"
  # puts get_qaqc_table("infiltration_compliance")
  # puts "\n"
  # puts "\n"
  # puts get_qaqc_table("infiltration_compliance", {"var" => ":infiltration_method"} )
  # puts "\n"
  # puts "\n"
  infiltration_compliance = get_qaqc_table(table_name: 'infiltration_compliance')['table']
  necb_section_name = get_qaqc_table(table_name: 'infiltration_compliance')['refs'].join(',')
  qaqc[:spaces].each do |spaceinfo|
    model.getSpaces.sort.each do |space|
      next unless space.name.get == spaceinfo[:name]

      found = false
      space.surfaces.each do |surface|
        next unless surface.outsideBoundaryCondition == 'Outdoors'

        found = true
        # peform this infiltration qaqc if and only if the space's surface is in contact with outdoors
        infiltration_compliance.each do |compliance|
          # puts "\nspaceinfo[#{compliance['var']}]"
          eval_string = "spaceinfo[:#{compliance['var']}]"
          result_value = eval(eval_string)
          # puts "#{compliance['test_text']}"
          test_text = "[SPACE][#{spaceinfo[:name]}]-#{compliance['var']}"
          # puts "result_value: #{result_value}"
          # puts "test_text: #{test_text}\n"
          # data[:infiltration_method]    = [ "Flow/ExteriorArea", spaceinfo[:infiltration_method] , nil ]
          # data[:infiltration_flow_per_m2] = [ 0.00025,       spaceinfo[:infiltration_flow_per_m2], 5 ]
          # data.each do |key,value|
          # puts key
          necb_section_test(
            qaqc,
            result_value,
            compliance['bool_operator'],
            compliance['expected_value'],
            necb_section_name,
            test_text,
            compliance['tolerance']
          )
        end
        # peform qaqc only once per space
        break
      end
      if !found
        qaqc[:warnings] << "necb_infiltration_compliance for SPACE:[#{spaceinfo[:name]}] was skipped because it does not contain surfaces with 'Outside' boundary condition."
      end
    end
  end
end

#necb_plantloop_sanity(qaqc) ⇒ Object

checks the pump power using pressure, and flowrate



1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1031

def necb_plantloop_sanity(qaqc)
  necb_section_name = 'SANITY-??'
  qaqc[:plant_loops].each do |plant_loop_info|
    pump_head = plant_loop_info[:pumps][0][:head_pa]
    flow_rate = plant_loop_info[:pumps][0][:water_flow_m3_per_s] * 1000
    hp_check = ((flow_rate * 60 * 60) / 1000 * 1000 * 9.81 * pump_head * 0.000101997) / 3600000
    puts "\npump_head #{pump_head}"
    puts "name: #{qaqc[:building][:name]}"
    puts "name: #{plant_loop_info[:name]}"
    puts "flow_rate #{flow_rate}"
    puts "hp_check #{hp_check}\n"
    pump_power_hp = plant_loop_info[:pumps][0][:electric_power_w] / 1000 * 0.746
    percent_diff = (hp_check - pump_power_hp).to_f.abs / hp_check * 100

    if percent_diff.nan?
      qaqc[:ruby_warnings] << "(hp_check - pump_power_hp).to_f.abs/hp_check * 100 for #{plant_loop_info[:name]} is NaN"
      next
    end

    if pump_power_hp < 1.0
      qaqc[:warnings] << "necb_plantloop_sanity [SKIP] [PLANT LOOP][#{plant_loop_info[:name]}][:pumps][0][:electric_power_hp] because  pump_power_hp: [#{pump_power_hp}] < 1 hp"
      next
    end

    necb_section_test(
      qaqc,
      percent_diff,
      '<=',
      20, # diff of 20%
      necb_section_name,
      "[PLANT LOOP][#{plant_loop_info[:name]}][:pumps][0][:electric_power_hp] [#{pump_power_hp}]; NECB value [#{hp_check}]; Percent Diff"
    )
  end
end

#necb_qaqc(qaqc, model) ⇒ Object



1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1837

def necb_qaqc(qaqc, model)
  puts "\n\nin necb_qaqc 2011 now\n\n"
  # Now perform basic QA/QC on items for NECB2011
  qaqc[:information] = []
  qaqc[:warnings] = []
  qaqc[:errors] = []
  qaqc[:unique_errors] = []

  necb_space_compliance(qaqc)

  necb_envelope_compliance(qaqc)

  necb_infiltration_compliance(qaqc, model)

  necb_exterior_opaque_compliance(qaqc)

  necb_exterior_fenestration_compliance(qaqc)

  necb_exterior_ground_surfaces_compliance(qaqc)

  necb_zone_sizing_compliance(qaqc)

  necb_design_supply_temp_compliance(qaqc)

  necb_economizer_compliance(qaqc)

  necb_hrv_compliance(qaqc, model)

  necb_vav_fan_power_compliance(qaqc)

  sanity_check(qaqc)

  necb_plantloop_sanity(qaqc)

  qaqc[:information] = qaqc[:information].sort
  qaqc[:warnings] = qaqc[:warnings].sort
  qaqc[:errors] = qaqc[:errors].sort
  qaqc[:unique_errors] = qaqc[:unique_errors].sort
  return qaqc
end

#necb_section_test(qaqc, result_value, bool_operator, expected_value, necb_section_name, test_text, tolerance = nil) ⇒ Object



1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1878

def necb_section_test(qaqc, result_value, bool_operator, expected_value, necb_section_name, test_text, tolerance = nil)
  test = 'eval_failed'
  command = ''
  if tolerance.is_a?(Integer)
    command = "#{result_value}.round(#{tolerance}) #{bool_operator} #{expected_value}.round(#{tolerance})"
  elsif expected_value.is_a?(String) && result_value.is_a?(String)
    command = "'#{result_value}' #{bool_operator} '#{expected_value}'"
  else
    command = "#{result_value} #{bool_operator} #{expected_value}"
  end
  test = eval(command)
  test_res = nil
  test_res = true if test.to_s.downcase == 'true'
  test_res = false if test.to_s.downcase == 'false'
  raise "Eval command failed #{test}" if test_res.nil?

  if test_res
    qaqc[:information] << "[Info][TEST-PASS][#{necb_section_name}]:#{test_text} result value:#{result_value} #{bool_operator} expected value:#{expected_value}"
  else
    qaqc[:errors] << "[ERROR][TEST-FAIL][#{necb_section_name}]:#{test_text} expected value:#{expected_value} #{bool_operator} result value:#{result_value}"
    unless (expected_value == -1.0) || (expected_value == 'N/A')
      qaqc[:unique_errors] << "[ERROR][TEST-FAIL][#{necb_section_name}]:#{test_text} expected value:#{expected_value} #{bool_operator} result value:#{result_value}"
    end
  end
end

#necb_space_compliance(qaqc) ⇒ Object

checks space compliance Re: lighting_per_area, occupancy_per_area, occupancy_schedule, electric_equipment_per_area



1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1069

def necb_space_compliance(qaqc)
  #    #Padmassun's Code Start
  # csv_file_name ="#{File.dirname(__FILE__)}/necb_2011_spacetype_info.csv"
  qaqc[:spaces].each do |space|
    building_type = ''
    space_type = ''
    if space[:space_type_name].include? 'Space Function '
      space_type = space[:space_type_name].to_s.rpartition('Space Function ')[2].strip
      building_type = 'Space Function'
    elsif space[:space_type_name].include? ' WholeBuilding'
      space_type = space[:space_type_name].to_s.rpartition(' WholeBuilding')[0].strip
      building_type = 'WholeBuilding'
    end

    ['lighting_per_area_w_per_m2', 'occupancy_per_area_people_per_m2', 'occupancy_schedule', 'electric_equipment_per_area_w_per_m2'].each do |compliance_var|
      qaqc_table = get_qaqc_table(table_name: 'space_compliance', search_criteria: { 'building_type' => building_type, 'space_type' => space_type }).first
      puts "\n#{qaqc_table}\n"
      necb_section_name = get_qaqc_table(table_name: 'space_compliance')['refs'][compliance_var]
      tolerance = get_qaqc_table(table_name: 'space_compliance')['tolerance'][compliance_var]
      # puts "\ncompliance_var:#{compliance_var}\n\tnecb_section_name:#{necb_section_name}\n\texp Value:#{qaqc_table[compliance_var]}\n"
      if compliance_var == 'lighting_per_area_w_per_m2'
        if space[:lighting_w_per_m2].nil?
          result_value = 0
        else
          result_value = space[:lighting_w_per_m2] * qaqc_table['lpd_ratio']
        end
      elsif compliance_var == 'occupancy_per_area_people_per_m2'
        result_value = space[:occ_per_m2]
      elsif compliance_var == 'occupancy_schedule'
        result_value = space[:occupancy_schedule]
      elsif compliance_var == 'electric_equipment_per_area_w_per_m2'
        result_value = space[:electric_w_per_m2]
      end

      test_text = "[SPACE][#{space[:name]}]-[TYPE:][#{space_type}]-#{compliance_var}"
      next if result_value.nil?

      necb_section_test(
        qaqc,
        result_value,
        '==',
        qaqc_table[compliance_var],
        necb_section_name,
        test_text,
        tolerance
      )
    end

    # row = look_up_csv_data(csv_file_name,{2 => space_type, 1 => building_type})
    # if row.nil?
    #   #raise ("space type of [#{space_type}] and/or building type of [#{building_type}] was not found in the excel sheet for space: [#{space[:name]}]")
    #   qaqc[:ruby_warnings] << "space type of [#{space_type}] and/or building type of [#{building_type}] was not found in the excel sheet for space: [#{space[:name]}]"
    #   puts "space type of [#{space_type}] and/or building type of [#{building_type}] was not found in the excel sheet for space: [#{space[:name]}]"
    # else
    #   #correct the data from the csv file to include a multiplier of 0.9 for specific space types.

    #   reduceLPDSpaces = ["Classroom/lecture/training", "Conf./meet./multi-purpose", "Lounge/recreation",
    #     "Washroom-sch-A", "Washroom-sch-B", "Washroom-sch-C", "Washroom-sch-D", "Washroom-sch-E",
    #     "Washroom-sch-F", "Washroom-sch-G", "Washroom-sch-H", "Washroom-sch-I", "Dress./fitt. - performance arts",
    #     "Locker room", "Retail - dressing/fitting","Locker room-sch-A","Locker room-sch-B","Locker room-sch-C",
    #     "Locker room-sch-D","Locker room-sch-E","Locker room-sch-F","Locker room-sch-G","Locker room-sch-H",
    #     "Locker room-sch-I", "Office - open plan - occsens", "Office - enclosed - occsens", "Storage area - occsens",
    #     "Hospital - medical supply - occsens", "Storage area - refrigerated - occsens"]

    #   if reduceLPDSpaces.include?(space_type)
    #     row[3] = row[3]*0.9
    #     puts "\n============================\nspace_type: #{space_type}\n============================\n"
    #   end

    #   # Start of Space Compliance
    #   necb_section_name = "NECB2011-Section 8.4.3.6"
    #   data = {}
    #   data[:lighting_per_area]            = [ row[3],'==',space[:lighting_w_per_m2] , "Table 4.2.1.6"     ,1 ] unless space[:lighting_w_per_m2].nil?
    #   data[:occupancy_per_area]           = [ row[4],'==',space[:occ_per_m2]        , "Table A-8.4.3.3.1" ,3 ] unless space[:occ_per_m2].nil?
    #   data[:occupancy_schedule]           = [ row[5],'==',space[:occupancy_schedule], "Table A-8.4.3.3.1" ,nil ] unless space[:occupancy_schedule].nil?
    #   data[:electric_equipment_per_area]  = [ row[6],'==',space[:electric_w_per_m2] , "Table A-8.4.3.3.1" ,1 ] unless space[:electric_w_per_m2].nil?
    #   data.each do |key,value|
    #     #puts key
    #     necb_section_test(
    #       qaqc,
    #       value[0],
    #       value[1],
    #       value[2],
    #       value[3],
    #       "[SPACE][#{space[:name]}]-[TYPE:][#{space_type}]#{key}",
    #       value[4]
    #     )
    #   end
    # end#space Compliance
  end
  # Padmassun's Code End
end

#necb_vav_fan_power_compliance(qaqc) ⇒ Object



1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1784

def necb_vav_fan_power_compliance(qaqc)
  necb_section_name = get_qaqc_table(table_name: 'vav_fan_power_compliance')['refs'].join(',')
  qaqc_table = get_qaqc_table(table_name: 'vav_fan_power_compliance')
  # necb_section_name = "NECB2011-5.2.3.3"
  qaqc[:air_loops].each do |air_loop_info|
    # necb_clg_cop = air_loop_info[:cooling_coils][:dx_single_speed][:cop] #*assuming that the cop is defined correctly*
    if air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s].nil?
      qaqc[:warnings] << '[vav_fan_power_compliance] air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s] is nil'
      next
    end

    max_air_flow_rate_m3_per_s = air_loop_info[:supply_fan][:max_air_flow_rate_m3_per_s]
    necb_supply_fan_w = -1

    if air_loop_info[:name].include? 'PSZ'
      necb_supply_fan_w = eval(qaqc_table['formulas']['NECB PSZ fan power (W)']).round(2)
    elsif air_loop_info[:name].include? 'VAV'
      necb_supply_fan_w = eval(qaqc_table['formulas']['NECB VAV fan power (W)']).round(2)
    end

    if air_loop_info[:supply_fan][:rated_electric_power_w].nil?
      qaqc[:warnings] << '[vav_fan_power_compliance] air_loop_info[:supply_fan][:rated_electric_power_w] is nil'
      next
    end

    supply_fan_w = (air_loop_info[:supply_fan][:rated_electric_power_w]).round(3)
    absolute_diff = (necb_supply_fan_w - supply_fan_w).to_f.abs
    if absolute_diff < 10
      # This case should ALWAYS PASS
      necb_section_test(
        qaqc,
        10,
        '>=',
        absolute_diff,
        necb_section_name,
        "[AIR LOOP][#{air_loop_info[:name]}][:supply_fan][:rated_electric_power_w] [#{supply_fan_w}] Absolute Difference from NECB value [#{necb_supply_fan_w}]"
      )
      next
    else
      # The test should pass if and only if the percent difference is less than 10%
      percent_diff = ((necb_supply_fan_w - supply_fan_w).to_f.abs / necb_supply_fan_w * 100).round(3)
      necb_section_test(
        qaqc,
        10,
        '>=',
        percent_diff,
        necb_section_name,
        "[AIR LOOP][#{air_loop_info[:name]}][:supply_fan][:rated_electric_power_w] [#{supply_fan_w}] Percent Diff from NECB value [#{necb_supply_fan_w}]"
      )
    end
  end
end

#necb_zone_sizing_compliance(qaqc) ⇒ Object



1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 1414

def necb_zone_sizing_compliance(qaqc)
  # Zone Sizing test
  necb_section_name = get_qaqc_table(table_name: 'zone_sizing_compliance')['refs'].join(',')
  qaqc_table = get_qaqc_table(table_name: 'zone_sizing_compliance')
  tolerance = 3
  # necb_section_name = "NECB2011-?"
  # round_precision = 3
  qaqc[:thermal_zones].each do |zoneinfo|
    #    skipping undefined schedules
    if (qaqc_table['exclude']['exclude_string'].any? { |ex_string| zoneinfo[:name].to_s.include? ex_string }) && !qaqc_table['exclude']['exclude_string'].empty?
      # if zoneinfo[:name].to_s.include?"- undefined -"
      puts "#{zoneinfo[:name]} was skipped in necb_zone_sizing_compliance because it contains #{qaqc_table['exclude']['exclude_string'].join(',')}"
      next
    end
    zone_sizing_compliance = qaqc_table['table']
    zone_sizing_compliance.each do |compliance|
      eval_string = "zoneinfo[:#{compliance['var']}]"
      result_value = eval(eval_string)
      next if result_value.nil?

      test_text = "[ZONE][#{zoneinfo[:name]}] #{compliance['var']}"
      # puts key
      necb_section_test(
        qaqc,
        result_value,
        compliance['bool_operator'],
        compliance['expected_value'],
        necb_section_name,
        test_text,
        tolerance
      )
    end
    # data = {}
    # data[:heating_sizing_factor] = [1.3 , zoneinfo[:heating_sizing_factor]]
    # data[:cooling_sizing_factor] = [1.1 ,zoneinfo[:cooling_sizing_factor]]
    # #data[:heating_design_supply_air_temp] =   [43.0, zoneinfo[:zone_heating_design_supply_air_temperature] ] #unless zoneinfo[:zone_heating_design_supply_air_temperature].nil?
    # #data[:cooling_design_supply_temp]   =   [13.0, zoneinfo[:zone_cooling_design_supply_air_temperature] ]
    # data.each do |key,value|
    #   #puts key
    #   necb_section_test(
    #     qaqc,
    #     value[0],
    #     '==',
    #     value[1],
    #     necb_section_name,
    #     "[ZONE][#{zoneinfo[:name]}] #{key}",
    #     round_precision
    #   )
    # end
  end
end

#new_add_sys6_multi_zone_built_up_system_with_baseboard_heating(model:, zones:, heating_coil_type:, baseboard_type:, chiller_type:, fan_type:, hw_loop:) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_system_6.rb', line 163

def new_add_sys6_multi_zone_built_up_system_with_baseboard_heating(model:,
                                                                   zones:,
                                                                   heating_coil_type:,
                                                                   baseboard_type:,
                                                                   chiller_type:,
                                                                   fan_type:,
                                                                   hw_loop:)
  # System Type 6: VAV w/ Reheat
  # This measure creates:
  # a single hot water loop with a natural gas or electric boiler or for the building
  # a single chilled water loop with water cooled chiller for the building
  # a single condenser water loop for heat rejection from the chiller
  # a VAV system w/ hot water or electric heating, chilled water cooling, and
  # hot water or electric reheat for each story of the building
  # Arguments:
  # "boiler_fueltype" choices match OS choices for boiler fuel type:
  # "NaturalGas","Electricity","PropaneGas","FuelOilNo1","FuelOilNo2","Coal","Diesel","Gasoline","OtherFuel1"
  # "heating_coil_type": "Electric" or "Hot Water"
  # "baseboard_type": "Electric" and "Hot Water"
  # "chiller_type": "Scroll";"Centrifugal";""Screw";"Reciprocating"
  # "fan_type": "AF_or_BI_rdg_fancurve";"AF_or_BI_inletvanes";"fc_inletvanes";"var_speed_drive"
  system_6_data = {}
  system_6_data[:name] = 'Sys_6_VAV with Reheat'
  system_6_data[:CentralCoolingDesignSupplyAirTemperature] = 13.0
  system_6_data[:CentralHeatingDesignSupplyAirTemperature] = 43.0
  system_6_data[:AllOutdoorAirinCooling] = false
  system_6_data[:AllOutdoorAirinHeating] = false
  system_6_data[:MinimumSystemAirFlowRatio] = 0.03
  # zone data
  system_6_data[:system_supply_air_temperature] = 13.0
  system_6_data[:ZoneCoolingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_6_data[:ZoneCoolingDesignSupplyAirTemperatureDifference] = 11.0
  system_6_data[:ZoneHeatingDesignSupplyAirTemperatureInputMethod] = 'TemperatureDifference'
  system_6_data[:ZoneHeatingDesignSupplyAirTemperatureDifference] = 21.0
  system_6_data[:ZoneCoolingSizingFactor] = 1.1
  system_6_data[:ZoneHeatingSizingFactor] = 1.3
  system_6_data[:ZoneVAVMinFlowFactorPerFloorArea] = 0.002
  system_6_data[:ZoneVAVMaxReheatTemp] = 43.0
  system_6_data[:ZoneVAVDamperAction] = 'Normal'
  system_data = system_6_data

  always_on = model.alwaysOnDiscreteSchedule

  # Chilled Water Plant

  chw_loop = OpenStudio::Model::PlantLoop.new(model)
  chiller1, chiller2 = setup_chw_loop_with_components(model, chw_loop, chiller_type)

  # Condenser System

  cw_loop = OpenStudio::Model::PlantLoop.new(model)
  ctower = setup_cw_loop_with_components(model, cw_loop, chiller1, chiller2)

  # Make a Packaged VAV w/ PFP Boxes for each story of the building
  model.getBuildingStorys.sort.each do |story|
    unless (OpenstudioStandards::Geometry.building_story_get_thermal_zones(story) & zones).empty?
      air_loop = common_air_loop(model: model, system_data: system_data)
      air_loop.setName(system_data[:name])
      air_loop_sizing = air_loop.sizingSystem
      air_loop_sizing.setCentralCoolingDesignSupplyAirTemperature(system_data[:CentralCoolingDesignSupplyAirTemperature])
      air_loop_sizing.setCentralHeatingDesignSupplyAirTemperature(system_data[:CentralHeatingDesignSupplyAirTemperature])
      air_loop_sizing.setAllOutdoorAirinCooling(system_data[:AllOutdoorAirinCooling])
      air_loop_sizing.setAllOutdoorAirinHeating(system_data[:AllOutdoorAirinHeating])
      if model.version < OpenStudio::VersionString.new('2.7.0')
        air_loop_sizing.setMinimumSystemAirFlowRatio(system_data[:MinimumSystemAirFlowRatio])
      else
        air_loop_sizing.setCentralHeatingMaximumSystemAirFlowRatio(system_data[:MinimumSystemAirFlowRatio])
      end

      supply_fan = OpenStudio::Model::FanVariableVolume.new(model, always_on)
      supply_fan.setName('Sys6 Supply Fan')
      return_fan = OpenStudio::Model::FanVariableVolume.new(model, always_on)
      return_fan.setName('Sys6 Return Fan')

      if heating_coil_type == 'Hot Water'
        htg_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)
        hw_loop.addDemandBranchForComponent(htg_coil)
      end
      if heating_coil_type == 'Electric'
        htg_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
      end

      clg_coil = OpenStudio::Model::CoilCoolingWater.new(model, always_on)
      chw_loop.addDemandBranchForComponent(clg_coil)

      oa_controller = OpenStudio::Model::ControllerOutdoorAir.new(model)
      oa_controller.autosizeMinimumOutdoorAirFlowRate

      # Set mechanical ventilation controller outdoor air to ZoneSum (used to be defaulted to ZoneSum but now should be
      # set explicitly)
      oa_controller.controllerMechanicalVentilation.setSystemOutdoorAirMethod('ZoneSum')

      oa_system = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(model, oa_controller)

      # Add the components to the air loop
      # in order from closest to zone to furthest from zone
      supply_inlet_node = air_loop.supplyInletNode
      supply_outlet_node = air_loop.supplyOutletNode
      supply_fan.addToNode(supply_inlet_node)
      htg_coil.addToNode(supply_inlet_node)
      clg_coil.addToNode(supply_inlet_node)
      oa_system.addToNode(supply_inlet_node)
      returnAirNode = oa_system.returnAirModelObject.get.to_Node.get
      return_fan.addToNode(returnAirNode)

      # Add a setpoint manager to control the
      # supply air to a constant temperature

      sat_sch = OpenStudio::Model::ScheduleRuleset.new(model)
      sat_sch.setName('Supply Air Temp')
      sat_sch.defaultDaySchedule.setName('Supply Air Temp Default')
      sat_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), system_data[:system_supply_air_temperature])
      sat_stpt_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, sat_sch)
      sat_stpt_manager.addToNode(supply_outlet_node)

      # Make a VAV terminal with HW reheat for each zone on this story that is in intersection with the zones array.
      # and hook the reheat coil to the HW loop
      (OpenstudioStandards::Geometry.building_story_get_thermal_zones(story) & zones).each do |zone|
        # Zone sizing parameters
        sizing_zone = zone.sizingZone
        sizing_zone.setZoneCoolingDesignSupplyAirTemperature(system_data[:ZoneCoolingDesignSupplyAirTemperature])
        sizing_zone.setZoneHeatingDesignSupplyAirTemperature(system_data[:ZoneHeatingDesignSupplyAirTemperature])
        sizing_zone.setZoneCoolingSizingFactor(system_data[:ZoneCoolingSizingFactor])
        sizing_zone.setZoneHeatingSizingFactor(system_data[:ZoneHeatingSizingFactor])

        if heating_coil_type == 'Hot Water'
          reheat_coil = OpenStudio::Model::CoilHeatingWater.new(model, always_on)
          hw_loop.addDemandBranchForComponent(reheat_coil)
        elsif heating_coil_type == 'Electric'
          reheat_coil = OpenStudio::Model::CoilHeatingElectric.new(model, always_on)
        end

        vav_terminal = OpenStudio::Model::AirTerminalSingleDuctVAVReheat.new(model, always_on, reheat_coil)
        air_loop.addBranchForZone(zone, vav_terminal.to_StraightComponent)
        # NECB2011 minimum zone airflow setting
        vav_terminal.setFixedMinimumAirFlowRate(system_data[:ZoneVAVMinFlowFactorPerFloorArea] * zone.floorArea)
        vav_terminal.setMaximumReheatAirTemperature(system_data[:ZoneVAVMaxReheatTemp])
        vav_terminal.setDamperHeatingAction(system_data[:ZoneVAVDamperAction])

        # Set zone baseboards
        add_zone_baseboards(model: model,
                            zone: zone,
                            baseboard_type: baseboard_type,
                            hw_loop: hw_loop)
      end
    end
    # next story
  end
  # for debugging
  # puts "end add_sys6_multi_zone_built_up_with_baseboard_heating"
  return true
end

#percentage_difference(value_1, value_2) ⇒ Object

Math fundtion to determine percent difference.



719
720
721
722
723
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 719

def percentage_difference(value_1, value_2)
  return 0.0 if value_1 == value_2

  return ((value_1 - value_2).abs / ((value_1 + value_2) / 2) * 100)
end

#pump_standard_minimum_motor_efficiency_and_size(pump, motor_bhp) ⇒ Array<Double>

Determines the minimum pump motor efficiency and nominal size for a given motor bhp. This should be the total brake horsepower with any desired safety factor already included. This method picks the next nominal motor catgory larger than the required brake horsepower, and the efficiency is based on that size. For example, if the bhp = 6.3, the nominal size will be 7.5HP and the efficiency for 90.1-2010 will be 91.7% from Table 10.8B. This method assumes 4-pole, 1800rpm totally-enclosed fan-cooled motors.

Parameters:

  • motor_bhp (Double)

    motor brake horsepower (hp)

Returns:

  • (Array<Double>)

    minimum motor efficiency (0.0 to 1.0), nominal horsepower



1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1452

def pump_standard_minimum_motor_efficiency_and_size(pump, motor_bhp)
  motor_eff = 0.85
  nominal_hp = motor_bhp

  # Don't attempt to look up motor efficiency
  # for zero-hp pumps (required for circulation-pump-free
  # service water heating systems).
  return [1.0, 0] if motor_bhp == 0.0

  # Lookup the minimum motor efficiency
  motors = @standards_data['motors']

  # Assuming all pump motors are 4-pole ODP
  search_criteria = {
    'motor_use' => 'PUMP',
    'number_of_poles' => 4.0,
    'type' => 'Enclosed'
  }

  motor_properties = model_find_object(motors, search_criteria, motor_bhp)
  if motor_properties.nil?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Pump', "For #{pump.name}, could not find motor properties using search criteria: #{search_criteria}, motor_bhp = #{motor_bhp} hp.")
    return [motor_eff, nominal_hp]
  end

  motor_eff = motor_properties['nominal_full_load_efficiency']
  nominal_hp = motor_properties['maximum_capacity'].to_f.round(1)
  # Round to nearest whole HP for niceness
  if nominal_hp >= 2
    nominal_hp = nominal_hp.round
  end

  # Get the efficiency based on the nominal horsepower
  # Add 0.01 hp to avoid search errors.
  motor_properties = model_find_object(motors, search_criteria, nominal_hp + 0.01)
  if motor_properties.nil?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Fan', "For #{pump.name}, could not find nominal motor properties using search criteria: #{search_criteria}, motor_hp = #{nominal_hp} hp.")
    return [motor_eff, nominal_hp]
  end
  motor_eff = motor_properties['nominal_full_load_efficiency']

  return [motor_eff, nominal_hp]
end

#pump_variable_speed_control_type(pump) ⇒ Boolean

Determine and set type of part load control type for heating and chilled water variable speed pumps

Parameters:

  • pump (OpenStudio::Model::PumpVariableSpeed)

    OpenStudio pump object

Returns:

  • (Boolean)

    Returns true if applicable, false otherwise



1532
1533
1534
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1532

def pump_variable_speed_control_type(pump)
  return false
end

#qaqc_only(model) ⇒ Object

generates only qaqc component



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 168

def qaqc_only(model)
  # load the qaqc.json files
  @qaqc_data = load_qaqc_database_new

  # generate base qaqc hash
  qaqc = create_base_data(model)
  # performs the qaqc on the given base qaqc hash.
  # using `qaqc.clone` as an argument to pass in a shallow copy, so that the argument passed can stay unmodified.
  necb_qaqc_with_base = necb_qaqc(qaqc.clone, model)

  # subract base data from qaqc
  return (necb_qaqc_with_base.to_a - qaqc.to_a).to_h
end

#replace_massless_material_with_std_material(model, surf) ⇒ Object

Loop through the layers of the construction of the surface and replace any massless material with a standard one. The material used instead is from the EnergyPlus dataset file ‘ASHRAE_2005_HOF_Materials.idf’ with the name: ‘Insulation: Expanded polystyrene - extruded (smooth skin surface) (HCFC-142b exp.)’. The thickness of the new material is based on the thermal resistance of the massless material it replaces. created by: Kamel Haddad ([email protected])



830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 830

def replace_massless_material_with_std_material(model,surf)
  std_const_name = "#{surf.construction.get.name.to_s}_std"
  std_const = model.getLayeredConstructions.select {|const| const.name.to_s == std_const_name}
  new_const = nil
  if !std_const.empty?
    new_const = std_const[0]
  else
    new_layers = {}
    has_massless_mat = false
    layer_index = 0
    surf.construction.get.to_LayeredConstruction.get.layers.each do |layer|
      if layer.to_MasslessOpaqueMaterial.is_initialized then
        has_massless_mat = true
        new_mat = OpenStudio::Model::StandardOpaqueMaterial.new(model)
        new_mat.setName("Expanded Polystyrene")
        new_mat.setThermalConductivity(0.029)
        new_mat.setDensity(29.0)
        new_mat.setSpecificHeat(1210.0)
        new_mat.setRoughness('MediumSmooth')
        new_mat.setThickness(layer.to_MasslessOpaqueMaterial.get.thermalResistance.to_f * new_mat.thermalConductivity.to_f)
      else
        new_mat = layer
      end
      new_layers[layer_index] = new_mat
      layer_index += 1
    end
    if has_massless_mat
      new_const = OpenStudio::Model::Construction.new(model)
      new_layers.keys.sort.each {|layer_index| new_const.to_LayeredConstruction.get.insertLayer(layer_index,new_layers[layer_index])}
      new_const.setName("#{surf.construction.get.name.to_s}_std")
    end
  end
  surf.setConstruction(new_const) if !new_const.nil?

end

#sanity_check(qaqc) ⇒ Object

Checks if a space with a proper schedule is conditioned or not



998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
# File 'lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb', line 998

def sanity_check(qaqc)
  qaqc[:sanity_check] = {}
  qaqc[:sanity_check][:fail] = []
  qaqc[:sanity_check][:pass] = []
  # Padmassun's code for isConditioned start
  qaqc[:thermal_zones].each do |zoneinfo|
    zoneinfo[:spaces].each do |space|
      # skip plenums and undefined spaces/zones
      if zoneinfo[:name].to_s.include? '- undefined -'
        next
      end

      if zoneinfo[:space_type_name].to_s.include? 'Space Function - undefined -'
        if zoneinfo[:is_conditioned].to_s == 'No'
          qaqc[:sanity_check][:pass] << "[TEST-PASS][SANITY_CHECK-PASS] for [SPACE][#{space[:name]}] and [THERMAL ZONE] [#{zoneinfo[:name]}] where isConditioned is supposed to be [" 'No' "] and found as #{zoneinfo[:is_conditioned]}"
        else
          qaqc[:sanity_check][:fail] << "[ERROR][SANITY_CHECK-FAIL] for [SPACE][#{space[:name]}] and [THERMAL ZONE] [#{zoneinfo[:name]}] where isConditioned is supposed to be [" 'No' "] but found as #{zoneinfo[:is_conditioned]}"
        end
      else
        if zoneinfo[:is_conditioned].to_s == 'Yes'
          qaqc[:sanity_check][:pass] << "[TEST-PASS][SANITY_CHECK-PASS] for [SPACE][#{space[:name]}] and [THERMAL ZONE] [#{zoneinfo[:name]}] where isConditioned is supposed to be [" 'Yes' "] and found as #{zoneinfo[:is_conditioned]}"
        elsif zoneinfo[:name]
          qaqc[:sanity_check][:fail] << "[ERROR][SANITY_CHECK-FAIL] for [SPACE][#{space[:name]}] and [THERMAL ZONE] [#{zoneinfo[:name]}] where isConditioned is supposed to be [" 'Yes' "] but found as #{zoneinfo[:is_conditioned]}"
        end
      end
    end
  end
  qaqc[:sanity_check][:fail] = qaqc[:sanity_check][:fail].sort
  qaqc[:sanity_check][:pass] = qaqc[:sanity_check][:pass].sort
  # Padmassun's code for isConditioned end
end

#scale_model_geometry(model, x_scale, y_scale, z_scale) ⇒ Object



631
632
633
634
635
636
637
638
639
640
641
642
643
644
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 631

def scale_model_geometry(model, x_scale, y_scale, z_scale)
  # Identity matrix for setting space origins
  m = OpenStudio::Matrix.new(4, 4, 0)

  m[0, 0] = 1.0 / x_scale
  m[1, 1] = 1.0 / y_scale
  m[2, 2] = 1.0 / z_scale
  m[3, 3] = 1.0
  t = OpenStudio::Transformation.new(m)
  model.getPlanarSurfaceGroups.each do |planar_surface|
    planar_surface.changeTransformation(t)
  end
  return model
end

#set_lighting_per_area(space_type:, definition:, lighting_per_area:, lights_scale:) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/openstudio-standards/standards/necb/NECB2011/lighting.rb', line 146

def set_lighting_per_area(space_type:,
                          definition:,
                          lighting_per_area:,
                          lights_scale:)
  occ_sens_lpd_frac = 1.0
  # NECB2011 space types that require a reduction in the LPD to account for
  # the requirement of an occupancy sensor (8.4.4.6(3) and 4.2.2.2(2))
  reduce_lpd_spaces = ['Classroom/lecture/training', 'Conf./meet./multi-purpose', 'Lounge/recreation',
                       'Conf./meet./multi-purpose', 'Washroom-sch-A', 'Washroom-sch-B', 'Washroom-sch-C', 'Washroom-sch-D',
                       'Washroom-sch-E', 'Washroom-sch-F', 'Washroom-sch-G', 'Washroom-sch-H', 'Washroom-sch-I',
                       'Dress./fitt. - performance arts', 'Locker room', 'Locker room-sch-A', 'Locker room-sch-B',
                       'Locker room-sch-C', 'Locker room-sch-D', 'Locker room-sch-E', 'Locker room-sch-F', 'Locker room-sch-G',
                       'Locker room-sch-H', 'Locker room-sch-I', 'Retail - dressing/fitting']
  if reduce_lpd_spaces.include?(space_type.standardsSpaceType.get)
    # Note that "Storage area", "Storage area - refrigerated", "Hospital - medical supply" and "Office - enclosed"
    # LPD should only be reduced if their space areas are less than specific area values.
    # This is checked in a space loop after this function in the calling routine.
    occ_sens_lpd_frac = 0.9
  end
  definition.setWattsperSpaceFloorArea(OpenStudio.convert(lighting_per_area.to_f * occ_sens_lpd_frac * lights_scale, 'W/ft^2', 'W/m^2').get)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set LPD to #{lighting_per_area} W/ft^2.")
end

#set_lighting_per_area_led_lighting(space_type:, definition:, lighting_per_area_led_lighting:, lights_scale:) ⇒ Object



1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1851

def set_lighting_per_area_led_lighting(space_type:, definition:, lighting_per_area_led_lighting:, lights_scale:)
  # puts "#{space_type.name.to_s} - 'space_height' - #{space_height.to_s}"
  occ_sens_lpd_frac = 1.0
  # NECB2011 space types that require a reduction in the LPD to account for
  # the requirement of an occupancy sensor (8.4.4.6(3) and 4.2.2.2(2))
  reduce_lpd_spaces = ['Classroom/lecture/training', 'Conf./meet./multi-purpose', 'Lounge/recreation',
                       'Conf./meet./multi-purpose', 'Washroom-sch-A', 'Washroom-sch-B', 'Washroom-sch-C', 'Washroom-sch-D',
                       'Washroom-sch-E', 'Washroom-sch-F', 'Washroom-sch-G', 'Washroom-sch-H', 'Washroom-sch-I',
                       'Dress./fitt. - performance arts', 'Locker room', 'Locker room-sch-A', 'Locker room-sch-B',
                       'Locker room-sch-C', 'Locker room-sch-D', 'Locker room-sch-E', 'Locker room-sch-F', 'Locker room-sch-G',
                       'Locker room-sch-H', 'Locker room-sch-I', 'Retail - dressing/fitting']
  if reduce_lpd_spaces.include?(space_type.standardsSpaceType.get)
    # Note that "Storage area", "Storage area - refrigerated", "Hospital - medical supply" and "Office - enclosed"
    # LPD should only be reduced if their space areas are less than specific area values.
    # This is checked in a space loop after this function in the calling routine.
    occ_sens_lpd_frac = 0.9
  end

  # ##### Since Atrium's LPD for LED lighting depends on atrium's height, the height of the atrium (if applicable) should be found.
  standards_space_type = space_type.standardsSpaceType.is_initialized ? space_type.standardsSpaceType.get : nil
  if standards_space_type.include? 'Atrium' # @todo Note that since none of the archetypes has Atrium, this was tested for 'Dining'. #Atrium
    puts "#{standards_space_type} - has atrium" # space_type.name.to_s
    # Get the max height for the spacetype.
    max_space_height_for_spacetype = get_max_space_height_for_space_type(space_type: space_type)
    if max_space_height_for_spacetype < 12.0 # @todo Note that since none of the archetypes has Atrium, this was tested for 'Dining' with the threshold of 5.0 m for space_height.
      # @todo Regarding the below equations, identify which version of ASHRAE 90.1 was used in NECB2015.
      atrium_lpd_eq_smaller_12_intercept = 0
      atrium_lpd_eq_smaller_12_slope = 1.06
      atrium_lpd_eq_larger_12_intercept = 4.3
      atrium_lpd_eq_larger_12_slope = 1.06
      lighting_per_area_led_lighting_atrium = (atrium_lpd_eq_smaller_12_intercept + atrium_lpd_eq_smaller_12_slope * 12.0) * 0.092903 # W/ft2 @todo Note that for NECB2011, a constant LPD is used for atrium based on NECB2015's equations. NECB2011's threshold for height is 13.0 m.
    elsif max_space_height_for_spacetype >= 12.0 && max_space_height_for_spacetype < 13.0
      lighting_per_area_led_lighting_atrium = (atrium_lpd_eq_larger_12_intercept + atrium_lpd_eq_larger_12_slope * 12.5) * 0.092903 # W/ft2
    else # i.e. space_height >= 13.0
      lighting_per_area_led_lighting_atrium = (atrium_lpd_eq_larger_12_intercept + atrium_lpd_eq_larger_12_slope * 13.0) * 0.092903 # W/ft2
    end
    puts "#{standards_space_type} - has lighting_per_area_led_lighting_atrium - #{lighting_per_area_led_lighting_atrium}"
    lighting_per_area_led_lighting = lighting_per_area_led_lighting_atrium
  end
  lighting_per_area_led_lighting *= lights_scale
  definition.setWattsperSpaceFloorArea(OpenStudio.convert(lighting_per_area_led_lighting.to_f * occ_sens_lpd_frac, 'W/ft^2', 'W/m^2').get)

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set LPD to #{lighting_per_area_led_lighting} W/ft^2.")
end

#set_necb_external_subsurface_conductance(subsurface, hdd) ⇒ Object

Set all external subsurfaces (doors, windows, skylights) to NECB values.

Parameters:

  • subsurface (String)
  • hdd (Float)

Author:



450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 450

def set_necb_external_subsurface_conductance(subsurface, hdd)
  conductance_value = 0
  return unless subsurface.outsideBoundaryCondition.downcase.match('outdoors')

  case subsurface.subSurfaceType.downcase
  when /window/
    conductance_value = @standards_data['conductances']['Window'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
  when /door/
    conductance_value = @standards_data['conductances']['Door'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
  end
  subsurface.setRSI(1 / conductance_value)
end

#set_necb_external_surface_conductance(surface, hdd, is_radiant = false, scaling_factor = 1.0) ⇒ String

Set all external surface conductances to NECB values.

Parameters:

  • surface (String)
  • hdd (Float)
  • is_radiant (Boolian) (defaults to: false)
  • scaling_factor (Float) (defaults to: 1.0)

Returns:

Author:



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb', line 412

def set_necb_external_surface_conductance(surface, hdd, is_radiant = false, scaling_factor = 1.0)
  conductance_value = 0
  if surface.outsideBoundaryCondition.casecmp('outdoors').zero?

    case surface.surfaceType.downcase
    when 'wall'
      conductance_value = @standards_data['conductances']['Wall'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
    when 'floor'
      conductance_value = @standards_data['conductances']['Floor'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
    when 'roofceiling'
      conductance_value = @standards_data['conductances']['Roof'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
    end
    if is_radiant
      conductance_value *= 0.80
    end
    return BTAP::Geometry::Surfaces.set_surfaces_construction_conductance([surface], conductance_value)
  end

  return unless surface.outsideBoundaryCondition.downcase =~ /ground/

  case surface.surfaceType.downcase
  when 'wall'
    conductance_value = @standards_data['conductances']['GroundWall'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
  when 'floor'
    conductance_value = @standards_data['conductances']['GroundFloor'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
  when 'roofceiling'
    conductance_value = @standards_data['conductances']['GroundRoof'].find { |i| i['hdd'] > hdd }['thermal_transmittance'] * scaling_factor
  end
  if is_radiant
    conductance_value *= 0.80
  end
  return BTAP::Geometry::Surfaces.set_surfaces_construction_conductance([surface], conductance_value)
end

#set_occ_sensor_spacetypes(model, space_type_map) ⇒ Boolean

Returns true if successful, false if not

Returns:

  • (Boolean)

    returns true if successful, false if not



1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1246

def set_occ_sensor_spacetypes(model, space_type_map)
  building_type = 'Space Function'
  space_type_map.each do |space_type_name, space_names|
    space_names.sort.each do |space_name|
      space = model.getSpaceByName(space_name)
      next if space.empty?

      space = space.get

      # Check if space type for this space matches NECB2011 specific space type
      # for occupancy sensor that is area dependent. Note: space.floorArea in m2.

      # Evaluate the formula in the database.
      standard_space_type_name = space_type_name
      floor_area = space.floorArea
      if eval(@standards_data['formulas']['occupancy_sensors_space_types_formula']['value'])
        # If there is only one space assigned to this space type, then reassign this stub
        # to the @@template duplicate with appendage " - occsens", otherwise create a new stub
        # for this space. Required to use reduced LPD by NECB2011 0.9 factor.
        space_type_name_occsens = space_type_name + ' - occsens'
        stub_space_type_occsens = model.getSpaceTypeByName("#{building_type} #{space_type_name_occsens}")

        if stub_space_type_occsens.empty?
          # create a new space type just once for space_type_name appended with " - occsens"
          stub_space_type_occsens = OpenStudio::Model::SpaceType.new(model)
          stub_space_type_occsens.setStandardsBuildingType(building_type)
          stub_space_type_occsens.setStandardsSpaceType(space_type_name_occsens)
          stub_space_type_occsens.setName("#{building_type} #{space_type_name_occsens}")
          space_type_apply_rendering_color(stub_space_type_occsens)
          space.setSpaceType(stub_space_type_occsens)
        else
          # reassign occsens space type stub already created...
          stub_space_type_occsens = stub_space_type_occsens.get
          space.setSpaceType(stub_space_type_occsens)
        end
      end
    end
  end
  return true
end

#set_output_meters(model:, output_meters:) ⇒ Object



2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2212

def set_output_meters(model:,output_meters:)
  unless output_meters.nil? or output_meters.empty?
    # remove existing output meters
    existing_meters = model.getOutputMeters

    # OpenStudio doesn't seemt to like two meters of the same name, even if they have different reporting frequencies.
    output_meters.each do |new_meter|
      #check if meter already exists
      result = existing_meters.select { |e_m| e_m.name == new_meter['name'] }
      puts("More and one output meter named #{new_meter['name']}") if result.size > 1
      if result.size >= 1
        existing_meter = result[0]
        puts("A meter named #{new_meter['name']} already exists. One will not be added to the model.")
        if existing_meter.reportingFrequency != new_meter['frequency']
          existing_meter.setReportingFrequency(new_meter['frequency'])
          puts("Changing reporting frequency of existing meter to #{new_meter['frequency']}.")
        end
      end
      if result.size == 0
        meter = OpenStudio::Model::OutputMeter.new(model)
        meter.setName(new_meter['name'])
        meter.setReportingFrequency(new_meter['frequency'])
        puts("Adding meter for #{meter.name} reporting #{new_meter['frequency']}")
      end
    end
  end
  return model
end

#set_output_variables(model:, output_variables:) ⇒ Object



2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 2198

def set_output_variables(model:,output_variables:)
  unless output_variables.nil? or output_variables.empty?
    output_variables.each do |output_variable|
      puts output_variable
      puts output_variable['frequency']
      raise("Frequency is not valid. Must by \"hourly\" or \"timestep\" but got #{output_variable}.") unless ["timestep","hourly",'daily','monthly','annual'].include?(output_variable['frequency'])
      output = OpenStudio::Model::OutputVariable.new(output_variable['variable'],model)
      output.setKeyValue(output_variable['key'])
      output.setReportingFrequency(output_variable['frequency'])
    end
  end
  return model
end

#set_random_rendering_color(object, random) ⇒ Object

This method will create a color object used in SU, 3D Viewer and Floorspace.js



1261
1262
1263
1264
1265
1266
1267
1268
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 1261

def set_random_rendering_color(object, random)
  rendering_color = OpenStudio::Model::RenderingColor.new(object.model)
  rendering_color.setName(object.name.get)
  rendering_color.setRenderingRedValue(random.rand(255))
  rendering_color.setRenderingGreenValue(random.rand(255))
  rendering_color.setRenderingBlueValue(random.rand(255))
  return rendering_color
end

#set_wildcard_schedules_to_dominant_building_schedule(model, runner = nil) ⇒ Object



761
762
763
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 761

def set_wildcard_schedules_to_dominant_building_schedule(model, runner = nil)
  # Get rid of.
end

#set_zones_thermostat_schedule_based_on_space_type_schedules(model, runner = nil) ⇒ Object



1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1584

def set_zones_thermostat_schedule_based_on_space_type_schedules(model, runner = nil)
  puts 'in set_zones_thermostat_schedule_based_on_space_type_schedules'
  BTAP.runner_register('DEBUG', 'Start-set_zones_thermostat_schedule_based_on_space_type_schedules', runner)
  model.getThermalZones.sort.each do |zone|
    BTAP.runner_register('DEBUG', "Zone = #{zone.name} Spaces =#{zone.spaces.size} ", runner)
    array = []

    zone.spaces.sort.each do |space|
      schedule_type = determine_necb_schedule_type(space).to_s
      BTAP.runner_register('DEBUG', "space name/type:#{space.name}/#{schedule_type}", runner)

      # if wildcard space type, need to get dominant schedule type
      if '*'.to_s == schedule_type
        dominant_sched_type = determine_dominant_necb_schedule_type(model)
        schedule_type = dominant_sched_type
      end

      array << schedule_type
    end
    array.uniq!
    if array.size > 1
      BTAP.runner_register('Error', "#{zone.name} has spaces with different schedule types. Please ensure that all the spaces are of the same schedule type A to I.", runner)
      return false
    end

    htg_search_string = "NECB-#{array[0]}-Thermostat Setpoint-Heating"
    clg_search_string = "NECB-#{array[0]}-Thermostat Setpoint-Cooling"

    if model.getScheduleRulesetByName(htg_search_string).empty? == false
      htg_sched = model.getScheduleRulesetByName(htg_search_string).get
    else
      BTAP.runner_register('ERROR', "heating_thermostat_setpoint_schedule NECB-#{array[0]} does not exist", runner)
      return false
    end

    if model.getScheduleRulesetByName(clg_search_string).empty? == false
      clg_sched = model.getScheduleRulesetByName(clg_search_string).get
    else
      BTAP.runner_register('ERROR', "cooling_thermostat_setpoint_schedule NECB-#{array[0]} does not exist", runner)
      return false
    end

    name = "NECB-#{array[0]}-Thermostat Dual Setpoint Schedule"

    # If dual setpoint already exists, use that one, else create one
    ds = if model.getThermostatSetpointDualSetpointByName(name).empty? == false
           model.getThermostatSetpointDualSetpointByName(name).get
         else
           BTAP::Resources::Schedules.create_annual_thermostat_setpoint_dual_setpoint(model, name, htg_sched, clg_sched)
         end

    thermostat_clone = ds.clone.to_ThermostatSetpointDualSetpoint.get
    zone.setThermostatSetpointDualSetpoint(thermostat_clone)
    BTAP.runner_register('Info', "ThermalZone #{zone.name} set to DualSetpoint Schedule NECB-#{array[0]}", runner)
  end

  BTAP.runner_register('DEBUG', 'END-set_zones_thermostat_schedule_based_on_space_type_schedules', runner)
  return true
end

#setup_chw_loop_with_components(model, chw_loop, chiller_type) ⇒ Object

of setup_hw_loop_with_components



1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1707

def setup_chw_loop_with_components(model, chw_loop, chiller_type)
  chw_loop.setName('Chilled Water Loop')
  sizing_plant = chw_loop.sizingPlant
  sizing_plant.setLoopType('Cooling')
  sizing_plant.setDesignLoopExitTemperature(7.0)
  sizing_plant.setLoopDesignTemperatureDifference(6.0)

  # Note: pump of 'chilled water loop' has been changed to the variable one as the constant one caused fatal errors for LargeOffice-Yellowknife-NaturalGas for some ECMs and inputs.
  # Fatal error was: 'CheckForRunawayPlantTemps: Simulation terminated because of run away plant temperatures, too cold' OR '..., too hot' for the PlantLoop of 'Chilled Water Loop'.
  # Note that the variable speed pump has been already used for 'Hot Water Loop'.
  chw_pump = OpenStudio::Model::PumpVariableSpeed.new(model)

  chiller1 = OpenStudio::Model::ChillerElectricEIR.new(model)
  chiller2 = OpenStudio::Model::ChillerElectricEIR.new(model)
  chiller1.setCondenserType('WaterCooled')
  chiller2.setCondenserType('WaterCooled')
  chiller1_name = "Primary Chiller WaterCooled #{chiller_type}".strip
  chiller1.setName(chiller1_name)
  chiller2_name = "Secondary Chiller WaterCooled #{chiller_type}".strip
  chiller2.setName(chiller2_name)

  chiller_bypass_pipe = OpenStudio::Model::PipeAdiabatic.new(model)

  chw_supply_outlet_pipe = OpenStudio::Model::PipeAdiabatic.new(model)

  # Add the components to the chilled water loop
  chw_supply_inlet_node = chw_loop.supplyInletNode
  chw_supply_outlet_node = chw_loop.supplyOutletNode
  chw_pump.addToNode(chw_supply_inlet_node)
  chw_loop.addSupplyBranchForComponent(chiller1)
  chw_loop.addSupplyBranchForComponent(chiller2)
  chw_loop.addSupplyBranchForComponent(chiller_bypass_pipe)
  chw_supply_outlet_pipe.addToNode(chw_supply_outlet_node)

  # Add a setpoint manager to control the
  # chilled water to a constant temperature
  chw_t_c = 7.0
  chw_t_sch = BTAP::Resources::Schedules.create_annual_constant_ruleset_schedule(model, 'CHW Temp', 'Temperature', chw_t_c)
  chw_t_stpt_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, chw_t_sch)
  chw_t_stpt_manager.addToNode(chw_supply_outlet_node)

  return chiller1, chiller2
end

#setup_cw_loop_with_components(model, cw_loop, chiller1, chiller2) ⇒ Object

of setup_chw_loop_with_components



1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1753

def setup_cw_loop_with_components(model, cw_loop, chiller1, chiller2)
  cw_loop.setName('Condenser Water Loop')
  cw_sizing_plant = cw_loop.sizingPlant
  cw_sizing_plant.setLoopType('Condenser')
  cw_sizing_plant.setDesignLoopExitTemperature(29.0)
  cw_sizing_plant.setLoopDesignTemperatureDifference(6.0)

  # Note: pump of 'Condenser water loop' has been changed to the variable one as the constant one caused fatal errors for LargeOffice-Montreal-NaturalGas for some ECMs and inputs.
  # Fatal error was: 'Plant temperatures are getting far too cold, check controls and relative loads and capacities'.
  # Note that the variable speed pump has been already used for 'Hot Water Loop' and 'Chilled Water Loop'.
  cw_pump = OpenStudio::Model::PumpVariableSpeed.new(model)

  clg_tower = OpenStudio::Model::CoolingTowerSingleSpeed.new(model)

  # TO DO: Need to define and set cooling tower curves

  clg_tower_bypass_pipe = OpenStudio::Model::PipeAdiabatic.new(model)

  cw_supply_outlet_pipe = OpenStudio::Model::PipeAdiabatic.new(model)

  # Add the components to the condenser water loop
  cw_supply_inlet_node = cw_loop.supplyInletNode
  cw_supply_outlet_node = cw_loop.supplyOutletNode
  cw_pump.addToNode(cw_supply_inlet_node)
  clg_tower.setDesignInletAirWetBulbTemperature(24.0)
  clg_tower.setDesignInletAirDryBulbTemperature(35.0)
  clg_tower.setDesignApproachTemperature(5.0)
  clg_tower.setDesignRangeTemperature(6.0)
  cw_loop.addSupplyBranchForComponent(clg_tower)
  cw_loop.addSupplyBranchForComponent(clg_tower_bypass_pipe)
  cw_supply_outlet_pipe.addToNode(cw_supply_outlet_node)
  cw_loop.addDemandBranchForComponent(chiller1)
  cw_loop.addDemandBranchForComponent(chiller2)

  # Add a setpoint manager to control the
  # condenser water to constant temperature
  cw_t_c = 29.0
  cw_t_sch = BTAP::Resources::Schedules.create_annual_constant_ruleset_schedule(model, 'CW Temp', 'Temperature', cw_t_c)
  cw_t_stpt_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, cw_t_sch)
  cw_t_stpt_manager.addToNode(cw_supply_outlet_node)

  return clg_tower
end

#setup_hw_loop_with_components(model, hw_loop, boiler_fueltype, pump_flow_sch) ⇒ Object



1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1650

def setup_hw_loop_with_components(model,
                                  hw_loop,
                                  boiler_fueltype,
                                  pump_flow_sch)
  hw_loop.setName('Hot Water Loop')
  sizing_plant = hw_loop.sizingPlant
  sizing_plant.setLoopType('Heating')
  sizing_plant.setDesignLoopExitTemperature(82.0) #@todo units
  sizing_plant.setLoopDesignTemperatureDifference(16.0)

  # pump (set to variable speed for now till fix to run away plant temperature is found)
  # pump = OpenStudio::Model::PumpConstantSpeed.new(model)
  pump = OpenStudio::Model::PumpVariableSpeed.new(model)
  # @todo the keyword "setPumpFlowRateSchedule" does not seem to work. A message
  # was sent to NREL to let them know about this. Once there is a fix for this,
  # use the proper pump schedule depending on whether we have two-pipe or four-pipe
  # fan coils.
  #            pump.resetPumpFlowRateSchedule()
  #            pump.setPumpFlowRateSchedule(pump_flow_sch)

  # boiler
  boiler1 = OpenStudio::Model::BoilerHotWater.new(model)
  boiler2 = OpenStudio::Model::BoilerHotWater.new(model)
  boiler1.setFuelType(boiler_fueltype)
  boiler2.setFuelType(boiler_fueltype)
  boiler1.setName('Primary Boiler')
  boiler2.setName('Secondary Boiler')

  # boiler_bypass_pipe
  boiler_bypass_pipe = OpenStudio::Model::PipeAdiabatic.new(model)

  # supply_outlet_pipe
  supply_outlet_pipe = OpenStudio::Model::PipeAdiabatic.new(model)

  # Add the components to the hot water loop
  hw_supply_inlet_node = hw_loop.supplyInletNode
  hw_supply_outlet_node = hw_loop.supplyOutletNode
  pump.addToNode(hw_supply_inlet_node)

  hw_loop.addSupplyBranchForComponent(boiler1)
  hw_loop.addSupplyBranchForComponent(boiler2)
  hw_loop.addSupplyBranchForComponent(boiler_bypass_pipe)
  supply_outlet_pipe.addToNode(hw_supply_outlet_node)

  # Add a setpoint manager to control the
  # hot water based on outdoor temperature
  hw_oareset_stpt_manager = OpenStudio::Model::SetpointManagerOutdoorAirReset.new(model)
  hw_oareset_stpt_manager.setControlVariable('Temperature')
  hw_oareset_stpt_manager.setSetpointatOutdoorLowTemperature(82.0)
  hw_oareset_stpt_manager.setOutdoorLowTemperature(-16.0)
  hw_oareset_stpt_manager.setSetpointatOutdoorHighTemperature(60.0)
  hw_oareset_stpt_manager.setOutdoorHighTemperature(0.0)
  hw_oareset_stpt_manager.addToNode(hw_supply_outlet_node)
end

#space_apply_infiltration_rate(space) ⇒ Double

TODO:

handle doors and vestibules

Set the infiltration rate for this space to include the impact of air leakage requirements in the standard.

Returns:

  • (Double)

    true if successful, false if not



1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1181

def space_apply_infiltration_rate(space)
  # Remove infiltration rates set at the space type.
  infiltration_data = @standards_data['infiltration']
  unless space.spaceType.empty?
    space.spaceType.get.spaceInfiltrationDesignFlowRates.each(&:remove)
  end
  # Remove infiltration rates set at the space object.
  space.spaceInfiltrationDesignFlowRates.each(&:remove)

  exterior_wall_and_roof_and_subsurface_area = OpenstudioStandards::Geometry.space_get_exterior_wall_and_subsurface_and_roof_area(space) # To do
  # Don't create an object if there is no exterior wall area
  if exterior_wall_and_roof_and_subsurface_area <= 0.0
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.Model', "For #{template}, no exterior wall area was found, no infiltration will be added.")
    return true
  end
  # Calculate the total infiltration, assuming
  # that it only occurs through exterior walls and roofs (not floors as
  # explicit stated in the NECB2011 so overhang/cantilevered floors will
  # have no effective infiltration)
  tot_infil_m3_per_s = get_standards_constant('infiltration_rate_m3_per_s_per_m2') * exterior_wall_and_roof_and_subsurface_area
  # Now spread the total infiltration rate over all
  # exterior surface area (for the E+ input field) this will include the exterior floor if present.
  all_ext_infil_m3_per_s_per_m2 = tot_infil_m3_per_s / space.exteriorArea

  OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Space', "For #{space.name}, adj infil = #{all_ext_infil_m3_per_s_per_m2.round(8)} m^3/s*m^2.")

  # Get any infiltration schedule already assigned to this space or its space type
  # If not, the always on schedule will be applied.
  infil_sch = nil
  unless space.spaceInfiltrationDesignFlowRates.empty?
    old_infil = space.spaceInfiltrationDesignFlowRates[0]
    if old_infil.schedule.is_initialized
      infil_sch = old_infil.schedule.get
    end
  end

  if infil_sch.nil? && space.spaceType.is_initialized
    space_type = space.spaceType.get
    unless space_type.spaceInfiltrationDesignFlowRates.empty?
      old_infil = space_type.spaceInfiltrationDesignFlowRates[0]
      if old_infil.schedule.is_initialized
        infil_sch = old_infil.schedule.get
      end
    end
  end

  if infil_sch.nil?
    infil_sch = space.model.alwaysOnDiscreteSchedule
  end

  # Create an infiltration rate object for this space
  infiltration = OpenStudio::Model::SpaceInfiltrationDesignFlowRate.new(space.model)
  infiltration.setName("#{space.name} Infiltration")
  infiltration.setFlowperExteriorSurfaceArea(all_ext_infil_m3_per_s_per_m2)
  infiltration.setSchedule(infil_sch)
  infiltration.setConstantTermCoefficient(get_standards_constant('infiltration_constant_term_coefficient'))
  infiltration.setTemperatureTermCoefficient(get_standards_constant('infiltration_constant_term_coefficient'))
  infiltration.setVelocityTermCoefficient(get_standards_constant('infiltration_velocity_term_coefficient'))
  infiltration.setVelocitySquaredTermCoefficient(get_standards_constant('infiltration_velocity_squared_term_coefficient'))
  infiltration.setSpace(space)

  return true
end

#space_surface_report(space) ⇒ Object

This method gathers the surface information for the space to determine if spaces are the same.



584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 584

def space_surface_report(space)
  surface_report = []
  space_floor_area = space.floorArea
  ['Outdoors', 'Ground'].each do |bc|
    surfaces = BTAP::Geometry::Surfaces.filter_by_boundary_condition(space.surfaces, [bc]).each do |surface|
      # sum wall area and subsurface area by direction. This is the old way so excluding top and bottom surfaces.
      # new way
      glazings = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(surface.subSurfaces, ['FixedWindow',
                                                                                            'OperableWindow',
                                                                                            'GlassDoor',
                                                                                            'Skylight',
                                                                                            'TubularDaylightDiffuser',
                                                                                            'TubularDaylightDome'])
      doors = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(surface.subSurfaces, ['Door',
                                                                                         'OverheadDoor'])
      azimuth = (surface.azimuth() * 180.0 / Math::PI)
      tilt = (surface.tilt() * 180.0 / Math::PI)
      surface_data = surface_report.detect do |curr_surface_data|
        curr_surface_data[:surface_type] == surface.surfaceType &&
          curr_surface_data[:azimuth] == azimuth &&
          curr_surface_data[:tilt] == tilt &&
          curr_surface_data[:boundary_condition] == bc
      end

      if surface_data.nil?
        surface_data = {
          surface_type: surface.surfaceType,
          azimuth: azimuth,
          tilt: tilt,
          boundary_condition: bc,
          surface_area: 0,
          surface_area_to_floor_ratio: 0,
          glazed_subsurface_area: 0,
          glazed_subsurface_area_to_floor_ratio: 0,
          opaque_subsurface_area: 0,
          opaque_subsurface_area_to_floor_ratio: 0
        }
        surface_report << surface_data
      end
      surface_data[:surface_area] += surface.grossArea.to_i
      surface_data[:surface_area_to_floor_ratio] += surface.grossArea / space.floorArea

      surface_data[:glazed_subsurface_area] += glazings.map { |subsurface| subsurface.grossArea * subsurface.multiplier }.inject(0) { |sum, x| sum + x }.to_i
      surface_data[:glazed_subsurface_area_to_floor_ratio] += glazings.map { |subsurface| subsurface.grossArea * subsurface.multiplier }.inject(0) { |sum, x| sum + x } / space.floorArea

      surface_data[:surface_area] += doors.map { |subsurface| subsurface.grossArea * subsurface.multiplier }.inject(0) { |sum, x| sum + x }.to_i
      surface_data[:surface_area_to_floor_ratio] += doors.map { |subsurface| subsurface.grossArea * subsurface.multiplier }.inject(0) { |sum, x| sum + x } / space.floorArea
    end
  end
  surface_report.sort! { |a, b| [a[:surface_type], a[:azimuth], a[:tilt], a[:boundary_condition]] <=> [b[:surface_type], b[:azimuth], b[:tilt], b[:boundary_condition]] }

  return surface_report
end

#space_type_apply_internal_loads(space_type:, set_people: true, set_lights: true, set_electric_equipment: true, set_gas_equipment: true, set_ventilation: true, set_infiltration: true, lights_type: 'NECB_Default', lights_scale: 1.0) ⇒ Boolean

Sets the selected internal loads to standards-based or typical values. For each category that is selected get all load instances. Remove all but the first instance if multiple instances. Add a new instance/definition if no instance exists. Modify the definition for the remaining instance to have the specified values. This method does not alter any loads directly assigned to spaces. This method skips plenums.

Also, assign reasonable clothing, air velocity, and work efficiency inputs to allow reasonable thermal comfort metrics to be calculated. to return air, fraction radiant, and fraction visible.

Parameters:

  • set_people (Boolean) (defaults to: true)

    if true, set the people density.

  • set_lights (Boolean) (defaults to: true)

    if true, set the lighting density, lighting fraction

  • set_electric_equipment (Boolean) (defaults to: true)

    if true, set the electric equipment density

  • set_gas_equipment (Boolean) (defaults to: true)

    if true, set the gas equipment density

  • set_ventilation (Boolean) (defaults to: true)

    if true, set the ventilation rates (per-person and per-area)

  • set_infiltration (Boolean) (defaults to: true)

    if true, set the infiltration rates

Returns:

  • (Boolean)

    returns true if successful, false if not



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/openstudio-standards/standards/necb/NECB2011/beps_compliance_path.rb', line 47

def space_type_apply_internal_loads(space_type:,
                                    set_people: true,
                                    set_lights: true,
                                    set_electric_equipment: true,
                                    set_gas_equipment: true,
                                    set_ventilation: true,
                                    set_infiltration: true,
                                    lights_type: 'NECB_Default',
                                    lights_scale: 1.0)

  # Skip plenums
  # Check if the space type name
  # contains the word plenum.
  if space_type.name.get.to_s.downcase.include?('plenum')
    return false
  end

  if space_type.standardsSpaceType.is_initialized
    if space_type.standardsSpaceType.get.downcase.include?('plenum')
      return false
    end
  end

  # Get the space Type data from @standards data

  spacetype_data = @standards_data['tables']['space_types']['table']

  standards_building_type = space_type.standardsBuildingType.is_initialized ? space_type.standardsBuildingType.get : nil
  standards_space_type = space_type.standardsSpaceType.is_initialized ? space_type.standardsSpaceType.get : nil
  space_type_properties = spacetype_data.detect { |s| (s['building_type'] == standards_building_type) && (s['space_type'] == standards_space_type) }

  # Need to add a check, or it'll crash on space_type_properties['occupancy_per_area'].to_f below
  if space_type_properties.nil?
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} was not found in the standards data.")
    return false
  end
  # People
  people_have_info = false
  occupancy_per_area = space_type_properties['occupancy_per_area'].to_f
  people_have_info = true unless occupancy_per_area.zero?

  if set_people && people_have_info

    # Remove all but the first instance
    instances = space_type.people.sort
    if instances.size.zero?
      # Create a new definition and instance
      definition = OpenStudio::Model::PeopleDefinition.new(space_type.model)
      definition.setName("#{space_type.name} People Definition")
      instance = OpenStudio::Model::People.new(definition)
      instance.setName("#{space_type.name} People")
      instance.setSpaceType(space_type)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no people, one has been created.")
      instances << instance
    elsif instances.size > 1
      instances.each_with_index do |inst, i|
        next if i.zero?

        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "Removed #{inst.name} from #{space_type.name}.")
        inst.remove
      end
    end

    # Modify the definition of the instance
    space_type.people.sort.each do |inst|
      definition = inst.peopleDefinition
      unless occupancy_per_area.zero?
        definition.setPeopleperSpaceFloorArea(OpenStudio.convert(occupancy_per_area / 1000, 'people/ft^2', 'people/m^2').get)
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set occupancy to #{occupancy_per_area} people/1000 ft^2.")
      end

      # set fraction radiant  ##
      definition.setFractionRadiant(0.3)

      # Clothing schedule for thermal comfort metrics
      clothing_sch = space_type.model.getScheduleRulesetByName('Clothing Schedule')
      if clothing_sch.is_initialized
        clothing_sch = clothing_sch.get
      else
        clothing_sch = OpenStudio::Model::ScheduleRuleset.new(space_type.model)
        clothing_sch.setName('Clothing Schedule')
        clothing_sch.defaultDaySchedule.setName('Clothing Schedule Default Winter Clothes')
        clothing_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1.0)
        sch_rule = OpenStudio::Model::ScheduleRule.new(clothing_sch)
        sch_rule.daySchedule.setName('Clothing Schedule Summer Clothes')
        sch_rule.daySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0.5)
        sch_rule.setStartDate(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(5), 1))
        sch_rule.setEndDate(OpenStudio::Date.new(OpenStudio::MonthOfYear.new(9), 30))
      end
      inst.setClothingInsulationSchedule(clothing_sch)

      # Air velocity schedule for thermal comfort metrics
      air_velo_sch = space_type.model.getScheduleRulesetByName('Air Velocity Schedule')
      if air_velo_sch.is_initialized
        air_velo_sch = air_velo_sch.get
      else
        air_velo_sch = OpenStudio::Model::ScheduleRuleset.new(space_type.model)
        air_velo_sch.setName('Air Velocity Schedule')
        air_velo_sch.defaultDaySchedule.setName('Air Velocity Schedule Default')
        air_velo_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0.2)
      end
      inst.setAirVelocitySchedule(air_velo_sch)

      # Work efficiency schedule for thermal comfort metrics
      work_efficiency_sch = space_type.model.getScheduleRulesetByName('Work Efficiency Schedule')
      if work_efficiency_sch.is_initialized
        work_efficiency_sch = work_efficiency_sch.get
      else
        work_efficiency_sch = OpenStudio::Model::ScheduleRuleset.new(space_type.model)
        work_efficiency_sch.setName('Work Efficiency Schedule')
        work_efficiency_sch.defaultDaySchedule.setName('Work Efficiency Schedule Default')
        work_efficiency_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0)
      end
      inst.setWorkEfficiencySchedule(work_efficiency_sch)
    end

  end

  # Lights
  apply_standard_lights(set_lights: set_lights,
                        space_type: space_type,
                        space_type_properties: space_type_properties,
                        lights_type: lights_type,
                        lights_scale: lights_scale)

  # Electric Equipment
  elec_equip_have_info = false
  elec_equip_per_area = space_type_properties['electric_equipment_per_area'].to_f
  elec_equip_frac_latent = space_type_properties['electric_equipment_fraction_latent'].to_f
  elec_equip_frac_radiant = space_type_properties['electric_equipment_fraction_radiant'].to_f
  elec_equip_frac_lost = space_type_properties['electric_equipment_fraction_lost'].to_f
  elec_equip_have_info = true unless elec_equip_per_area.zero?

  if set_electric_equipment && elec_equip_have_info

    # Remove all but the first instance
    instances = space_type.electricEquipment.sort
    if instances.size.zero?
      definition = OpenStudio::Model::ElectricEquipmentDefinition.new(space_type.model)
      definition.setName("#{space_type.name} Elec Equip Definition")
      instance = OpenStudio::Model::ElectricEquipment.new(definition)
      instance.setName("#{space_type.name} Elec Equip")
      instance.setSpaceType(space_type)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no electric equipment, one has been created.")
      instances << instance
    elsif instances.size > 1
      instances.each_with_index do |inst, i|
        next if i.zero?

        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "Removed #{inst.name} from #{space_type.name}.")
        inst.remove
      end
    end

    # Modify the definition of the instance
    space_type.electricEquipment.sort.each do |inst|
      definition = inst.electricEquipmentDefinition
      unless elec_equip_per_area.zero?
        definition.setWattsperSpaceFloorArea(OpenStudio.convert(elec_equip_per_area.to_f, 'W/ft^2', 'W/m^2').get)
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set electric EPD to #{elec_equip_per_area} W/ft^2.")
      end
      unless elec_equip_frac_latent.zero?
        definition.setFractionLatent(elec_equip_frac_latent)
      end
      unless elec_equip_frac_radiant.zero?
        definition.setFractionRadiant(elec_equip_frac_radiant)
      end
      unless elec_equip_frac_lost.zero?
        definition.setFractionLost(elec_equip_frac_lost)
      end
    end

  end

  # Gas Equipment
  gas_equip_have_info = false
  gas_equip_per_area = space_type_properties['gas_equipment_per_area'].to_f
  gas_equip_frac_latent = space_type_properties['gas_equipment_fraction_latent'].to_f
  gas_equip_frac_radiant = space_type_properties['gas_equipment_fraction_radiant'].to_f
  gas_equip_frac_lost = space_type_properties['gas_equipment_fraction_lost'].to_f
  gas_equip_have_info = true unless gas_equip_per_area.zero?

  if set_gas_equipment && gas_equip_have_info

    # Remove all but the first instance
    instances = space_type.gasEquipment.sort
    if instances.size.zero?
      definition = OpenStudio::Model::GasEquipmentDefinition.new(space_type.model)
      definition.setName("#{space_type.name} Gas Equip Definition")
      instance = OpenStudio::Model::GasEquipment.new(definition)
      instance.setName("#{space_type.name} Gas Equip")
      instance.setSpaceType(space_type)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no gas equipment, one has been created.")
      instances << instance
    elsif instances.size > 1
      instances.each_with_index do |inst, i|
        next if i.zero?

        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "Removed #{inst.name} from #{space_type.name}.")
        inst.remove
      end
    end

    # Modify the definition of the instance
    space_type.gasEquipment.sort.each do |inst|
      definition = inst.gasEquipmentDefinition
      unless gas_equip_per_area.zero?
        definition.setWattsperSpaceFloorArea(OpenStudio.convert(gas_equip_per_area.to_f, 'Btu/hr*ft^2', 'W/m^2').get)
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set gas EPD to #{elec_equip_per_area} Btu/hr*ft^2.")
      end
      unless gas_equip_frac_latent.zero?
        definition.setFractionLatent(gas_equip_frac_latent)
      end
      unless gas_equip_frac_radiant.zero?
        definition.setFractionRadiant(gas_equip_frac_radiant)
      end
      unless gas_equip_frac_lost.zero?
        definition.setFractionLost(gas_equip_frac_lost)
      end
    end

  end

  # Ventilation
  ventilation_have_info = false
  ventilation_per_area = space_type_properties['ventilation_per_area'].to_f
  ventilation_per_person = space_type_properties['ventilation_per_person'].to_f
  ventilation_ach = space_type_properties['ventilation_air_changes'].to_f
  ventilation_occupancy_per_area = space_type_properties['ventilation_occupancy_rate_people_per_1000ft2'].to_f
  ventilation_have_info = true unless ventilation_per_area.zero?
  ventilation_have_info = true unless ventilation_per_person.zero?
  ventilation_have_info = true unless ventilation_ach.zero?

  # Get the design OA or create a new one if none exists
  ventilation = space_type.designSpecificationOutdoorAir
  if ventilation.is_initialized
    ventilation = ventilation.get
  else
    ventilation = OpenStudio::Model::DesignSpecificationOutdoorAir.new(space_type.model)
    ventilation.setName("#{space_type.name} Ventilation")
    space_type.setDesignSpecificationOutdoorAir(ventilation)
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no ventilation specification, one has been created.")
  end

  if set_ventilation && ventilation_have_info

    # Modify the ventilation properties
    ventilation.setOutdoorAirMethod('Sum')
    unless ventilation_per_area.zero?
      ventilation.setOutdoorAirFlowperFloorArea(OpenStudio.convert(ventilation_per_area.to_f, 'ft^3/min*ft^2', 'm^3/s*m^2').get)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set ventilation per area to #{ventilation_per_area} cfm/ft^2.")
    end
    unless ventilation_per_person.zero?
      # For BTAP we often use an occupancy per area rate for ventilation which is different from the one used for
      # everything else.  The mod_ventilation_per_person rate adjusts the per person ventilation rate so that the
      # proper ventilation rate is calculated when using the general occupant per area rate.
      mod_ventilation_per_person = ventilation_per_person * ventilation_occupancy_per_area / occupancy_per_area
      ventilation.setOutdoorAirFlowperPerson(OpenStudio.convert(mod_ventilation_per_person.to_f, 'ft^3/min*person', 'm^3/s*person').get)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set ventilation per person to #{mod_ventilation_per_person} cfm/person.")
    end
    unless ventilation_ach.zero?
      ventilation.setOutdoorAirFlowAirChangesperHour(ventilation_ach)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set ventilation to #{ventilation_ach} ACH.")
    end

  elsif set_ventilation && !ventilation_have_info

    # All space types must have a design spec OA
    # object for ventilation controls to work correctly,
    # even if the values are all zero.
    ventilation.setOutdoorAirFlowperFloorArea(0)
    ventilation.setOutdoorAirFlowperPerson(0)
    ventilation.setOutdoorAirFlowAirChangesperHour(0)

  end

  # Infiltration
  infiltration_have_info = false
  infiltration_per_area_ext = space_type_properties['infiltration_per_exterior_area'].to_f
  infiltration_per_area_ext_wall = space_type_properties['infiltration_per_exterior_wall_area'].to_f
  infiltration_ach = space_type_properties['infiltration_air_changes'].to_f
  unless infiltration_per_area_ext.zero? && infiltration_per_area_ext_wall.zero? && infiltration_ach.zero?
    infiltration_have_info = true
  end

  return unless set_infiltration && infiltration_have_info

  # Remove all but the first instance
  instances = space_type.spaceInfiltrationDesignFlowRates.sort
  if instances.size.zero?
    instance = OpenStudio::Model::SpaceInfiltrationDesignFlowRate.new(space_type.model)
    instance.setName("#{space_type.name} Infiltration")
    instance.setSpaceType(space_type)
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} had no infiltration objects, one has been created.")
    instances << instance
  elsif instances.size > 1
    instances.each_with_index do |inst, i|
      next if i.zero?

      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "Removed #{inst.name} from #{space_type.name}.")
      inst.remove
    end
  end

  # Modify each instance
  space_type.spaceInfiltrationDesignFlowRates.sort.each do |inst|
    unless infiltration_per_area_ext.zero?
      inst.setFlowperExteriorSurfaceArea(OpenStudio.convert(infiltration_per_area_ext.to_f, 'ft^3/min*ft^2', 'm^3/s*m^2').get)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set infiltration to #{ventilation_ach} per ft^2 exterior surface area.")
    end
    unless infiltration_per_area_ext_wall.zero?
      inst.setFlowperExteriorWallArea(OpenStudio.convert(infiltration_per_area_ext_wall.to_f, 'ft^3/min*ft^2', 'm^3/s*m^2').get)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set infiltration to #{infiltration_per_area_ext_wall} per ft^2 exterior wall area.")
    end
    unless infiltration_ach.zero?
      inst.setAirChangesperHour(infiltration_ach)
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SpaceType', "#{space_type.name} set infiltration to #{ventilation_ach} ACH.")
    end
  end
end

#store_space_sizing_loads(model) ⇒ Object

Method to store space sizing loads. This is needed because later when the zones are destroyed this information will be lost.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 191

def store_space_sizing_loads(model)
  @stored_space_heating_sizing_loads = {}
  @stored_space_cooling_sizing_loads = {}
  model.getSpaces.sort.each do |space|
    space_type = space.spaceType.get.standardsSpaceType.get

    # error if zone design load methods are not available
    if space.model.version < OpenStudio::VersionString.new('3.6.0')
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.autozone', "Required ThermalZone methods .autosizedHeatingDesignLoad and .autosizedCoolingDesignLoad are not available in pre-OpenStudio 3.6.0 versions. Use a more recent version of OpenStudio.")
    end

    @stored_space_heating_sizing_loads[space] = space_type == '- undefined -' ? 0.0 : space.thermalZone.get.autosizedHeatingDesignLoad.get / space.floorArea
    @stored_space_cooling_sizing_loads[space] = space_type == '- undefined -' ? 0.0 : space.thermalZone.get.autosizedCoolingDesignLoad.get / space.floorArea
  end
end

#stored_space_cooling_load(space) ⇒ Object

Returns the cooling load per area for space after sizing runs has been done.



220
221
222
223
224
225
226
227
228
229
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 220

def stored_space_cooling_load(space)
  if @stored_space_cooling_sizing_loads.nil?
    # do a sizing run.
    raise('autorun sizing run failed!') if model_run_sizing_run(space.model, "#{Dir.pwd}/autozone") == false

    # collect sizing information on each space.
    store_space_sizing_loads(space.model)
  end
  @stored_space_cooling_sizing_loads[space]
end

#stored_space_heating_load(space) ⇒ Object

Returns heating load per area for space after sizing run has been done.



208
209
210
211
212
213
214
215
216
217
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 208

def stored_space_heating_load(space)
  if @stored_space_heating_sizing_loads.nil?
    # do a sizing run.
    raise('autorun sizing run failed!') if model_run_sizing_run(space.model, "#{Dir.pwd}/autozone") == false

    # collect sizing information on each space.
    store_space_sizing_loads(space.model)
  end
  @stored_space_heating_sizing_loads[space]
end

#stored_zone_cooling_load(zone) ⇒ Object

Returns the cooling load per area for zone after sizing runs has been done.



241
242
243
244
245
246
247
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 241

def stored_zone_cooling_load(zone)
  total = 0.0
  zone.spaces.each do |space|
    total += stored_space_cooling_load(space)
  end
  return total
end

#stored_zone_heating_load(zone) ⇒ Object

# Returns the heating load per area for zone after sizing runs has been done.



232
233
234
235
236
237
238
# File 'lib/openstudio-standards/standards/necb/NECB2011/autozone.rb', line 232

def stored_zone_heating_load(zone)
  total = 0.0
  zone.spaces.each do |space|
    total += stored_space_heating_load(space)
  end
  return total
end

#surfaces_are_in_contact?(surf1, surf2) ⇒ Boolean

check if two surfaces are in contact. For every two consecutive vertices on surface 1, loop through two consecutive vertices of surface two. Then check whether the vertices of surfaces 2 are on the same line as the vertices from surface 1. If the two vectors defined by the two vertices on surface 1 and those on surface 2 overlap, then the two surfaces are in contact. If a side from surface 2 is in contact with a side from surface 1, the length of the side from surface 2 is limited to the length of the side from surface 1. created by: Kamel Haddad ([email protected])

Returns:

  • (Boolean)


789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 789

def surfaces_are_in_contact?(surf1,surf2)
  surfaces_in_contact = false
  vert1 = surf1.vertices[0]
  for index1 in 1..surf1.vertices.size
    if index1 < surf1.vertices.size
      vert2 = surf1.vertices[index1]
    else
      vert2 = surf1.vertices[0]
    end
    seg12_length = ((vert2.x-vert1.x)**2+(vert2.y-vert1.y)**2+(vert2.z-vert1.z)**2)**0.5
    surf2_seg_length = 0.0
    vert3 = surf2.vertices[0]
    for index2 in 1..surf2.vertices.size
      if index2 < surf2.vertices.size
        vert4 = surf2.vertices[index2]
      else
        vert4 = surf2.vertices[0]
      end
      vert1_2_3_same_line_and_dir = three_vertices_same_line_and_dir?(vert1,vert2,vert3)
      if vert1_2_3_same_line_and_dir
        vert1_2_4_same_line_and_dir = three_vertices_same_line_and_dir?(vert1,vert2,vert4)
        if vert1_2_4_same_line_and_dir
          surfaces_in_contact = true
          seg34_length = ((vert4.x-vert3.x)**2+(vert4.y-vert3.y)**2+(vert4.z-vert3.z)**2)**0.5
          surf2_seg_length += seg34_length
          raise("Surface #{surf2.name.to_s} has sides in contact with surface #{surf1.name.to_s} but with a length greater than the max.") if surf2_seg_length > seg12_length
        end
      end
      vert3 = vert4
    end
    vert1 = vert2
  end

  return surfaces_in_contact
end

#thermal_zone_demand_control_ventilation_required?(thermal_zone, climate_zone) ⇒ Boolean

TODO:

Add exception logic for 90.1-2013 for cells, sickrooms, labs, barbers, salons, and bowling alleys

Determine if demand control ventilation (DCV) is required for this zone based on area and occupant density. Does not account for System requirements like ERV, economizer, etc. Those are accounted for in the AirLoopHVAC method of the same name.

Returns:

  • (Boolean)

    Returns true if required, false if not.



1512
1513
1514
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1512

def thermal_zone_demand_control_ventilation_required?(thermal_zone, climate_zone)
  return false
end

#thermal_zone_get_centroid_per_floor(thermal_zone) ⇒ Object

This method cycles through the spaces in a thermal zone and then sorts them by story. The method then cycles through the spaces on a story and then calculates the centroid of the spaces in the thermal zone on that floor. The method returns an array of hashes, one for each story. Each hash has the following structure:

{
    story_name: Name of a given story.
    spaces: Array containing all of the spaces in the thermal zone on the story in story_name.
    centroid: Array containing the x, y, and z coordinates of the centroid of the ceilings of the spaces
              listed in 'spaces:' above.
    ceiling_area: Total area of the ceilings of the spaces in 'spaces:' above.
}

Only spaces which are conditioned (heated or cooled) and are not plenums are included.



1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 1808

def thermal_zone_get_centroid_per_floor(thermal_zone)
  stories = []
  thermal_zone.spaces.sort.each do |space|
    spaceType_name = space.spaceType.get.nameString
    sp_type = spaceType_name[15..-1]
    # Including regular expressions in the following match for cases where extra characters, which do not belong, are
    # added to either the space type in the model or the space type reference file.
    sp_type_info = @standards_data['space_types'].detect do |data|
      (Regexp.new(data['space_type'].to_s.upcase).match(sp_type.upcase) || Regexp.new(sp_type.upcase).match(data['space_type'].to_s.upcase) || (data['space_type'].to_s.upcase == sp_type.upcase)) &&
        (data['building_type'].to_s == 'Space Function')
    end
    if sp_type_info.nil?
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.thermal_zone_get_centroid_per_floor', "The space type called #{sp_type} could not be found.  Please check that the schedules.json file is available and that the space types are spelled correctly")
      next
    end
    # Determine if space is heated or cooled via spacetype heating or cooling setpoints also checking if the space is
    # a plenum by checking if there is a hvac system associtated with it
    if sp_type_info['heating_setpoint_schedule'].nil?
      heated = false
    else
      heated = true
    end
    if sp_type_info['cooling_setpoint_schedule'].nil?
      cooled = false
    else
      cooled = true
    end
    if (sp_type_info['necb_hvac_system_selection_type'] == '- undefined -') || /undefined/.match(sp_type_info['necb_hvac_system_selection_type'])
      not_plenum = false
    else
      not_plenum = true
    end
    # If the spaces are heated or cooled and are not a plenum then continue
    if (heated || cooled) && not_plenum
      # Get the story name and sit it to none if there is no story name
      story_name = space.buildingStory.get.nameString
      story_name = 'none' if story_name.nil?
      # If this is the first story in the arry then add a new one.
      if stories.empty?
        stories << {
          story_name: story_name,
          spaces: [space],
          centroid: [0, 0, 0],
          ceiling_area: 0
        }
        next
      else
        # If this is not the first story in the array check if the story already is in the array.
        i = nil
        stories.each_with_index do |storycheck, index|
          if storycheck[:story_name] == story_name
            i = index
          end
        end
        # If the story is not in the array then add it.
        if i.nil?
          stories << {
            story_name: story_name,
            spaces: [space],
            centroid: [0, 0, 0],
            ceiling_area: 0
          }
        else
          # If the story is already in the arry then add the space to the array of spaces for that story
          stories[i][:spaces] << space
        end
      end
    end
  end
  # Go through each story in the array above
  stories.each do |story|
    tz_centre = [0, 0, 0, 0]
    # Go through each space in a given story
    story[:spaces].each do |space|
      # Determine the top surface of the space and calculate it's centroid.
      # Get the coordinates of the origin for the space (the coordinates of points in the space are relative to this).
      xOrigin = space.xOrigin
      yOrigin = space.yOrigin
      zOrigin = space.zOrigin
      # Go through each surface in the space and find ceilings by determining which is called 'RoofCeiing'.  Find the
      # overall centroid of all the ceilings in the spaces.  Find centroid by multiplying the centroid of the surfaces
      # multiplied by the area of the surface and add them all up.  Then divide this by the overall area.  This is the
      # area weighted average of the centroid coordinates.
      ceiling_centroid = [0, 0, 0, 0]
      space.surfaces.each do |sp_surface|
        if sp_surface.surfaceType.to_s.upcase == 'ROOFCEILING'
          ceiling_centroid[0] = ceiling_centroid[0] + sp_surface.centroid.x.to_f * sp_surface.grossArea.to_f
          ceiling_centroid[1] = ceiling_centroid[1] + sp_surface.centroid.y.to_f * sp_surface.grossArea.to_f
          ceiling_centroid[2] = ceiling_centroid[2] + sp_surface.centroid.z.to_f * sp_surface.grossArea.to_f
          ceiling_centroid[3] = ceiling_centroid[3] + sp_surface.grossArea
        end
      end

      ceiling_centroid[0] = ceiling_centroid[0] / ceiling_centroid[3]
      ceiling_centroid[1] = ceiling_centroid[1] / ceiling_centroid[3]
      ceiling_centroid[2] = ceiling_centroid[2] / ceiling_centroid[3]

      # This part is used to determine the overall x, y centre of the thermal zone.  This is determined by summing the
      # x and y components times the ceiling area and diving by the total ceiling area.  I also added z since the
      # ceilings may not be all have the same height.
      tz_centre[0] += (ceiling_centroid[0] + xOrigin) * ceiling_centroid[3]
      tz_centre[1] += (ceiling_centroid[1] + yOrigin) * ceiling_centroid[3]
      tz_centre[2] += (ceiling_centroid[2] + zOrigin) * ceiling_centroid[3]
      tz_centre[3] += (ceiling_centroid[3])
    end
    tz_centre[0] /= tz_centre[3]
    tz_centre[1] /= tz_centre[3]
    tz_centre[2] /= tz_centre[3]
    # Update the :centroid and :ceiling_area hashes for the story to reflect the x, y, and z coordinates of the
    # overall centroid of spaces on that floor.
    story[:centroid] = tz_centre[0..2]
    story[:ceiling_area] = tz_centre[3]
  end
  return stories
end

#three_vertices_same_line_and_dir?(vert1, vert2, vert3) ⇒ Boolean

check that three vertices are on the same line. Also check that the vectors from vert1 and vert2 and from vert1 and vert3 are in the same direction. created by: Kamel Haddad ([email protected])

Returns:

  • (Boolean)


914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 914

def three_vertices_same_line_and_dir?(vert1,vert2,vert3)
  tol = 1.0e-5
  vec12x,vec12y,vec12z = -vert1.x+vert2.x,-vert1.y+vert2.y,-vert1.z+vert2.z # x,y,z of vector 12
  vec12x = 0.0 if vec12x.abs < tol
  vec12y = 0.0 if vec12y.abs < tol
  vec12z = 0.0 if vec12z.abs < tol
  vec13x,vec13y,vec13z = -vert1.x+vert3.x,-vert1.y+vert3.y,-vert1.z+vert3.z # x,y,z of vector 13
  vec13x = 0.0 if vec13x.abs < tol
  vec13y = 0.0 if vec13y.abs < tol
  vec13z = 0.0 if vec13z.abs < tol
  # x,y,z of the cross product of the vectors 12 and 13
  cross_12_13_x = vec12y*vec13z-vec12z*vec13y
  cross_12_13_y = vec12z*vec13x-vec12x*vec13z
  cross_12_13_z = vec12x*vec13y-vec12y*vec13x
  # vectors are in parallel when x,y,z of cross product are 0.0
  vertices_on_same_line = false
  vertices_on_same_line = true if (cross_12_13_x == 0.0) && (cross_12_13_y == 0.0) && (cross_12_13_z == 0.0)
  vectors_same_direction = false
  if vertices_on_same_line
    vec12_13_x_factor = vec13x*vec12x
    vec12_13_y_factor = vec13y*vec12y
    vec12_13_z_factor = vec13z*vec12z
    vectors_same_direction = true if (vec12_13_x_factor >= 0.0) && (vec12_13_y_factor >= 0.0) && (vec12_13_z_factor >= 0.0)
  end
  same_line_same_dir = vertices_on_same_line && vectors_same_direction

  return same_line_same_dir
end

#update_sys_name(airloop, sys_abbr: nil, sys_oa: nil, sys_hr: nil, sys_htg: nil, sys_clg: nil, sys_sf: nil, zone_htg: nil, zone_clg: nil, sys_rf: nil) ⇒ Object

Method to update the base system name based on the inputs provided. Only the parts of the name with string inputs are updated



2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
# File 'lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb', line 2290

def update_sys_name(airloop,
                    sys_abbr: nil,
                    sys_oa: nil,
                    sys_hr: nil,
                    sys_htg: nil,
                    sys_clg: nil,
                    sys_sf: nil,
                    zone_htg: nil,
                    zone_clg: nil,
                    sys_rf: nil)
  name_parts = airloop.name.to_s.split('|').reject(&:empty?)
  if sys_abbr.is_a? String then name_parts[0] = sys_abbr end
  if sys_oa.is_a? String then name_parts[1] = sys_oa end
  for i in 0..name_parts.size - 1
    if (name_parts[i].include? 'shr>') && (sys_hr.is_a? String)
      name_parts[i] = "shr>#{sys_hr}"
    elsif (name_parts[i].include? 'sh>') && (sys_htg.is_a? String)
      name_parts[i] = "sh>#{sys_htg}"
    elsif (name_parts[i].include? 'sc>') && (sys_clg.is_a? String)
      name_parts[i] = "sc>#{sys_clg}"
    elsif (name_parts[i].include? 'ssf') && (sys_sf.is_a? String)
      name_parts[i] = "ssf>#{sys_sf}"
    elsif (name_parts[i].include? 'zh>') && (zone_htg.is_a? String)
      name_parts[i] = "zh>#{zone_htg}"
    elsif (name_parts[i].include? 'zc>') && (zone_clg.is_a? String)
      name_parts[i] = "zc>#{zone_clg}"
    elsif (name_parts[i].include? 'srf>') && (sys_rf.is_a? String)
      name_parts[i] = "srf>#{sys_rf}"
    end
  end
  sys_name = ''
  name_parts.each { |part| sys_name += "#{part}|" }

  # Check if the last part of the system name is an integer.  If it is, then remove the last part from the system name.
  check_int = begin
                Integer(name_parts.last.strip)
              rescue StandardError
                nil
              end
  sys_name = sys_name.chop unless check_int.nil?

  airloop.setName(sys_name)
end

#validate_and_upate_space_types(model) ⇒ Object

This method will validate that the space types in the model are indeed the correct NECB spacetypes names.



1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1139

def validate_and_upate_space_types(model)
  space_type_vintage = determine_spacetype_vintage(model)
  if space_type_vintage.nil?
    message = "These some of the spacetypes in the model are not part of any necb standard.\n  Please ensure all spacetype in model are correct."
    puts "Error: #{message}"
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.Standards.NECB', message)
    return false
  elsif space_type_vintage == self.class.name
    # the spacetype in the model match the version we are trying to create.
    # no translation neccesary.
    return true
  else
    # Need to translate to current vintage.
    no_errors = true
    st_model_vintage_string = "#{space_type_vintage}_space_type"
    bt_model_vintage_string = "#{space_type_vintage}_building_type"
    st_target_vintage_string = "#{self.class.name}_space_type"
    bt_target_vintage_string = "#{self.class.name}_building_type"
    space_type_upgrade_map = @standards_data['space_type_upgrade_map']
    model.getSpaceTypes.sort.each do |st|
      space_type_map = space_type_upgrade_map.detect { |row| (row[st_model_vintage_string] == st.standardsSpaceType.get.to_s) && (row[bt_model_vintage_string] == st.standardsBuildingType.get.to_s) }
      st.setStandardsBuildingType(space_type_map[bt_target_vintage_string].to_s.strip)
      raise('could not set buildingtype') unless st.setStandardsBuildingType(space_type_map[bt_target_vintage_string].to_s.strip)
      raise('could not set this') unless st.setStandardsSpaceType(space_type_map[st_target_vintage_string].to_s.strip)

      # Set name of spacetype to new name.
      st.setName("#{st.standardsBuildingType.get} #{st.standardsSpaceType.get}")
    end
    return no_errors
  end
end

#water_heater_mixed_apply_efficiency(water_heater_mixed) ⇒ Boolean

Applies the standard efficiency ratings and typical losses and paraisitic loads to this object. Efficiency and skin loss coefficient (UA) Per PNNL www.energycodes.gov/sites/default/files/documents/PrototypeModelEnhancements_2014_0.pdf Appendix A: Service Water Heating

Returns:

  • (Boolean)

    true if successful, false if not



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb', line 103

def water_heater_mixed_apply_efficiency(water_heater_mixed)
  # Get the capacity of the water heater
  # @todo add capability to pull autosized water heater capacity
  # if the Sizing:WaterHeater object is ever implemented in OpenStudio.
  capacity_w = water_heater_mixed.heaterMaximumCapacity
  if capacity_w.empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, cannot find capacity, standard will not be applied.")
    return false
  else
    capacity_w = capacity_w.get
  end
  capacity_btu_per_hr = OpenStudio.convert(capacity_w, 'W', 'Btu/hr').get
  capacity_kbtu_per_hr = OpenStudio.convert(capacity_w, 'W', 'kBtu/hr').get

  # Get the volume of the water heater
  # @todo add capability to pull autosized water heater volume
  # if the Sizing:WaterHeater object is ever implemented in OpenStudio.
  volume_m3 = water_heater_mixed.tankVolume
  if volume_m3.empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, cannot find volume, standard will not be applied.")
    return false
  else
    volume_m3 = volume_m3.get
  end
  volume_gal = OpenStudio.convert(volume_m3, 'm^3', 'gal').get

  # Get the heater fuel type
  fuel_type = water_heater_mixed.heaterFuelType
  unless fuel_type == 'NaturalGas' || fuel_type == 'Electricity' || fuel_type == 'FuelOilNo2'
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.WaterHeaterMixed', "For #{water_heater_mixed.name}, fuel type of #{fuel_type} is not yet supported, standard will not be applied.")
  end

  # Calculate the water heater efficiency and
  # skin loss coefficient (UA)
  # Calculate the energy factor (EF)
  # From PNNL http://www.energycodes.gov/sites/default/files/documents/PrototypeModelEnhancements_2014_0.pdf
  # Appendix A: Service Water Heating
  water_heater_eff = nil
  ua_btu_per_hr_per_f = nil
  sl_btu_per_hr = nil
  case fuel_type
  when 'Electricity'
    volume_l_per_s = volume_m3 * 1000
    if capacity_btu_per_hr <= OpenStudio.convert(12, 'kW', 'Btu/hr').get
      # Fixed water heater efficiency per PNNL
      water_heater_eff = 1
      # Calculate the max allowable standby loss (SL)
      sl_w = if volume_l_per_s < 270
               40 + 0.2 * volume_l_per_s # assume bottom inlet
             else
               0.472 * volume_l_per_s - 33.5
               # assume bottom inlet
             end
      sl_btu_per_hr = OpenStudio.convert(sl_w, 'W', 'Btu/hr').get
    else
      # Fixed water heater efficiency per PNNL
      water_heater_eff = 1
      # Calculate the max allowable standby loss (SL)   # use this - NECB does not give SL calculation for cap > 12 kW
      sl_btu_per_hr = 20 + (35 * Math.sqrt(volume_gal))
    end
    # Calculate the skin loss coefficient (UA)
    ua_btu_per_hr_per_f = sl_btu_per_hr / 70
  when 'NaturalGas'
    if capacity_btu_per_hr <= 75_000
      # Fixed water heater thermal efficiency per PNNL
      water_heater_eff = 0.82
      # Calculate the minimum Energy Factor (EF)
      base_ef = 0.67
      vol_drt = 0.0019
      ef = base_ef - (vol_drt * volume_gal)
      # Calculate the Recovery Efficiency (RE)
      # based on a fixed capacity of 75,000 Btu/hr
      # and a fixed volume of 40 gallons by solving
      # this system of equations:
      # ua = (1/.95-1/re)/(67.5*(24/41094-1/(re*cap)))
      # 0.82 = (ua*67.5+cap*re)/cap
      cap = 75_000.0
      re = (Math.sqrt(6724 * ef**2 * cap**2 + 40_409_100 * ef**2 * cap - 28_080_900 * ef * cap + 29_318_000_625 * ef**2 - 58_636_001_250 * ef + 29_318_000_625) + 82 * ef * cap + 171_225 * ef - 171_225) / (200 * ef * cap)
      # Calculate the skin loss coefficient (UA)
      # based on the actual capacity.
      ua_btu_per_hr_per_f = (water_heater_eff - re) * capacity_btu_per_hr / 67.5
    else
      # Thermal efficiency requirement from 90.1
      et = 0.8
      # Calculate the max allowable standby loss (SL)
      cap_adj = 800
      vol_drt = 110
      sl_btu_per_hr = (capacity_btu_per_hr / cap_adj + vol_drt * Math.sqrt(volume_gal))
      # Calculate the skin loss coefficient (UA)
      ua_btu_per_hr_per_f = (sl_btu_per_hr * et) / 70
      # Calculate water heater efficiency
      water_heater_eff = (ua_btu_per_hr_per_f * 70 + capacity_btu_per_hr * et) / capacity_btu_per_hr
    end
  end

  # Convert to SI
  ua_btu_per_hr_per_c = OpenStudio.convert(ua_btu_per_hr_per_f, 'Btu/hr*R', 'W/K').get
  # Set the water heater properties
  # Efficiency
  water_heater_mixed.setHeaterThermalEfficiency(water_heater_eff)
  # Skin loss
  water_heater_mixed.setOffCycleLossCoefficienttoAmbientTemperature(ua_btu_per_hr_per_c)
  water_heater_mixed.setOnCycleLossCoefficienttoAmbientTemperature(ua_btu_per_hr_per_c)
  # @todo Parasitic loss (pilot light)
  # PNNL document says pilot lights were removed, but IDFs
  # still have the on/off cycle parasitic fuel consumptions filled in
  water_heater_mixed.setOnCycleParasiticFuelType(fuel_type)
  # self.setOffCycleParasiticFuelConsumptionRate(??)
  water_heater_mixed.setOnCycleParasiticHeatFractiontoTank(0)
  water_heater_mixed.setOffCycleParasiticFuelType(fuel_type)
  # self.setOffCycleParasiticFuelConsumptionRate(??)
  water_heater_mixed.setOffCycleParasiticHeatFractiontoTank(0.8)

  # set part-load performance curve
  if (fuel_type == 'NaturalGas') || (fuel_type == 'FuelOilNo2')
    plf_vs_plr_curve = model_add_curve(water_heater_mixed.model, 'SWH-EFFFPLR-NECB2011')
    water_heater_mixed.setPartLoadFactorCurve(plf_vs_plr_curve)
  end

  # Append the name with standards information
  water_heater_mixed.setName("#{water_heater_mixed.name} #{water_heater_eff.round(3)} Therm Eff")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.WaterHeaterMixed', "For #{template}: #{water_heater_mixed.name}; thermal efficiency = #{water_heater_eff.round(3)}, skin-loss UA = #{ua_btu_per_hr_per_f.round}Btu/hr-R")
  return true
end

#zone_hvac_component_occupancy_ventilation_control(zone_hvac_component) ⇒ Object

do not apply zone hvac ventilation control



1094
1095
1096
# File 'lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb', line 1094

def zone_hvac_component_occupancy_ventilation_control(zone_hvac_component)
  return false
end