openstudio-standards 0.8.5 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/data/standards/test_performance_expected_dd_results.csv +90 -90
  3. data/lib/openstudio-standards/btap/activity.rb +858 -61
  4. data/lib/openstudio-standards/btap/attributes.rb +297 -103
  5. data/lib/openstudio-standards/btap/bridging.rb +1335 -593
  6. data/lib/openstudio-standards/btap/btap.rb +60 -181
  7. data/lib/openstudio-standards/btap/carbon/btap_carbon.rb +199 -142
  8. data/lib/openstudio-standards/btap/common/constructions.json +1275 -0
  9. data/lib/openstudio-standards/btap/common/costs.csv +1969 -0
  10. data/lib/openstudio-standards/btap/common/costs_local_factors.csv +2315 -0
  11. data/lib/openstudio-standards/btap/common/materials_glazing.csv +61 -0
  12. data/lib/openstudio-standards/btap/{common_resources → common}/materials_hvac.csv +1685 -1685
  13. data/lib/openstudio-standards/btap/{common_resources → common}/materials_lighting.csv +266 -266
  14. data/lib/openstudio-standards/btap/common/materials_opaque.csv +214 -0
  15. data/lib/openstudio-standards/btap/common/necb_building_types.csv +35 -0
  16. data/lib/openstudio-standards/btap/common/necb_space_types.csv +110 -0
  17. data/lib/openstudio-standards/btap/common/thermal_bridging.csv +73 -0
  18. data/lib/openstudio-standards/btap/costing/btap_costing.rb +346 -429
  19. data/lib/openstudio-standards/btap/costing/database.rb +104 -0
  20. data/lib/openstudio-standards/btap/costing/daylighting_sensor_control_costing.rb +356 -344
  21. data/lib/openstudio-standards/btap/costing/dcv_costing.rb +282 -296
  22. data/lib/openstudio-standards/btap/costing/envelope_costing.rb +179 -192
  23. data/lib/openstudio-standards/btap/costing/heating_cooling_costing.rb +2477 -2316
  24. data/lib/openstudio-standards/btap/costing/led_lighting_costing.rb +127 -142
  25. data/lib/openstudio-standards/btap/costing/lighting_costing.rb +216 -198
  26. data/lib/openstudio-standards/btap/costing/nv_costing.rb +522 -523
  27. data/lib/openstudio-standards/btap/costing/pv_ground_costing.rb +649 -649
  28. data/lib/openstudio-standards/btap/costing/shw_costing.rb +626 -625
  29. data/lib/openstudio-standards/btap/costing/thermal_bridging_costing.rb +72 -0
  30. data/lib/openstudio-standards/btap/costing/ventilation_costing.rb +2363 -2361
  31. data/lib/openstudio-standards/btap/deprecated.rb +1033 -0
  32. data/lib/openstudio-standards/btap/envelope.rb +313 -572
  33. data/lib/openstudio-standards/btap/fileio.rb +152 -1070
  34. data/lib/openstudio-standards/btap/geometry.rb +486 -766
  35. data/lib/openstudio-standards/btap/linear_regression.rb +130 -0
  36. data/lib/openstudio-standards/btap/paths.rb +46 -0
  37. data/lib/openstudio-standards/btap/qaqc/analysis.rb +163 -0
  38. data/lib/openstudio-standards/btap/qaqc/data.rb +2293 -0
  39. data/lib/openstudio-standards/btap/qaqc/datapoint.rb +641 -0
  40. data/lib/openstudio-standards/btap/schedules.rb +11 -611
  41. data/lib/openstudio-standards/btap/structure.rb +574 -189
  42. data/lib/openstudio-standards/constructions/materials/information.rb +1 -1
  43. data/lib/openstudio-standards/create_typical/create_typical.rb +1 -1
  44. data/lib/openstudio-standards/exterior_lighting/create.rb +135 -85
  45. data/lib/openstudio-standards/exterior_lighting/data/typical_exterior_lighting.json +20 -0
  46. data/lib/openstudio-standards/exterior_lighting/information.rb +23 -2
  47. data/lib/openstudio-standards/geometry/create.rb +7 -3
  48. data/lib/openstudio-standards/prototypes/ashrae_90_1/ashrae_90_1_2016/ashrae_90_1_2016.Model.rb +3 -2
  49. data/lib/openstudio-standards/prototypes/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.Model.rb +3 -2
  50. data/lib/openstudio-standards/prototypes/common/data/thermostat_schedule_lookup.json +84 -84
  51. data/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +4 -4
  52. data/lib/openstudio-standards/refrigeration/create_walkin.rb +45 -5
  53. data/lib/openstudio-standards/refrigeration/data/refrigerated_walkins.csv +75 -75
  54. data/lib/openstudio-standards/schedules/parametric.rb +6 -0
  55. data/lib/openstudio-standards/service_water_heating/create_typical.rb +10 -3
  56. data/lib/openstudio-standards/service_water_heating/create_water_heating_loop.rb +30 -14
  57. data/lib/openstudio-standards/service_water_heating/information.rb +4 -1
  58. data/lib/openstudio-standards/sql_file/unmet_hours.rb +2 -1
  59. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXSingleSpeed.rb +3 -1
  60. data/lib/openstudio-standards/standards/Standards.CoilHeatingDXSingleSpeed.rb +3 -1
  61. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.boilers.json +12 -0
  62. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2004/data/ashrae_90_1_2004.construction_properties.json +18432 -3072
  63. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.boilers.json +26 -0
  64. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2007/data/ashrae_90_1_2007.construction_properties.json +8532 -1422
  65. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.boilers.json +26 -0
  66. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2010/data/ashrae_90_1_2010.construction_properties.json +8532 -1422
  67. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.boilers.json +16 -0
  68. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.computer_room_acs.json +30 -30
  69. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2013/data/ashrae_90_1_2013.construction_properties.json +7560 -1260
  70. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/ashrae_90_1_2016.PumpVariableSpeed.rb +1 -2
  71. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.boilers.json +16 -0
  72. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2016/data/ashrae_90_1_2016.construction_properties.json +7560 -1260
  73. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.AirLoopHVAC.rb +3 -3
  74. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/ashrae_90_1_2019.PumpVariableSpeed.rb +1 -2
  75. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.boilers.json +22 -0
  76. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.construction_properties.json +7236 -1206
  77. data/lib/openstudio-standards/standards/ashrae_90_1/ashrae_90_1_2019/data/ashrae_90_1_2019.furnaces.json +223 -19
  78. data/lib/openstudio-standards/standards/ashrae_90_1/data/ashrae_90_1.schedules.json +37 -1
  79. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.boilers.json +3 -0
  80. data/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm_2019/data/ashrae_90_1_prm_2019.construction_properties.json +6900 -1150
  81. data/lib/openstudio-standards/standards/cbes/data/cbes.schedules.json +1 -1
  82. data/lib/openstudio-standards/standards/deer/data/deer.schedules.json +1 -1
  83. data/lib/openstudio-standards/standards/necb/BTAP1980TO2010/data/surface_thermal_transmittance.json +76 -40
  84. data/lib/openstudio-standards/standards/necb/BTAPPRE1980/data/surface_thermal_transmittance.json +76 -40
  85. data/lib/openstudio-standards/standards/necb/ECMS/hvac_systems.rb +6 -4
  86. data/lib/openstudio-standards/standards/necb/NECB2011/NECB2011_QAQC.json +2988 -0
  87. data/lib/openstudio-standards/standards/necb/NECB2011/autozone.rb +25 -3
  88. data/lib/openstudio-standards/standards/necb/NECB2011/building_envelope.rb +672 -534
  89. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/Hospital.osm +1791 -1792
  90. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/LEEPMultiTower.osm +7 -1
  91. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/LEEPTownHouse.osm +7 -1
  92. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/LargeHotel.osm +5 -6
  93. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/LargeOffice.osm +8 -9
  94. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/NorthernEducation.osm +10 -4
  95. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/NorthernHealthCare.osm +14 -15
  96. data/lib/openstudio-standards/standards/necb/NECB2011/data/geometry/Outpatient.osm +7 -1
  97. data/lib/openstudio-standards/standards/necb/NECB2011/data/surface_thermal_transmittance.json +76 -40
  98. data/lib/openstudio-standards/standards/necb/NECB2011/hvac_systems.rb +14 -6
  99. data/lib/openstudio-standards/standards/necb/NECB2011/necb_2011.rb +542 -130
  100. data/lib/openstudio-standards/standards/necb/NECB2011/qaqc/necb_qaqc.rb +30 -63
  101. data/lib/openstudio-standards/standards/necb/NECB2011/service_water_heating.rb +7 -2
  102. data/lib/openstudio-standards/standards/necb/NECB2015/data/surface_thermal_transmittance.json +90 -0
  103. data/lib/openstudio-standards/standards/necb/NECB2015/necb_2015.rb +8 -6
  104. data/lib/openstudio-standards/standards/necb/NECB2017/data/surface_thermal_transmittance.json +76 -40
  105. data/lib/openstudio-standards/standards/necb/NECB2017/necb_2017.rb +2 -1
  106. data/lib/openstudio-standards/standards/necb/NECB2020/building_envelope.rb +0 -117
  107. data/lib/openstudio-standards/standards/necb/NECB2020/data/surface_thermal_transmittance.json +76 -45
  108. data/lib/openstudio-standards/standards/necb/NECB2020/necb_2020.rb +218 -0
  109. data/lib/openstudio-standards/standards/necb/common/construction_defaults.osm +12 -7
  110. data/lib/openstudio-standards/standards/standard.rb +1 -1
  111. data/lib/openstudio-standards/version.rb +1 -1
  112. data/lib/openstudio-standards.rb +9 -4
  113. metadata +59 -66
  114. data/data/inventory/thermal_bridging.csv +0 -90
  115. data/data/standards/OpenStudio_Standards-ashrae_90_1-ALL-comstock(space_types).xlsx +0 -0
  116. data/data/standards/openstudio_standards_duplicates_log.csv +0 -7962
  117. data/lib/openstudio-standards/btap/NECB_building_types.csv +0 -35
  118. data/lib/openstudio-standards/btap/NECB_space_types.csv +0 -109
  119. data/lib/openstudio-standards/btap/WeatherData1.json +0 -2110
  120. data/lib/openstudio-standards/btap/WeatherData1.xlsx +0 -0
  121. data/lib/openstudio-standards/btap/btap_result.rb +0 -1144
  122. data/lib/openstudio-standards/btap/btap_test_helper.rb +0 -86
  123. data/lib/openstudio-standards/btap/common_paths.rb +0 -81
  124. data/lib/openstudio-standards/btap/common_resources/ConstructionProperties.csv +0 -52
  125. data/lib/openstudio-standards/btap/common_resources/Constructions.csv +0 -37
  126. data/lib/openstudio-standards/btap/common_resources/construction_sets.csv +0 -1270
  127. data/lib/openstudio-standards/btap/common_resources/constructions_glazing.csv +0 -121
  128. data/lib/openstudio-standards/btap/common_resources/constructions_opaque.csv +0 -2256
  129. data/lib/openstudio-standards/btap/common_resources/costs.csv +0 -1897
  130. data/lib/openstudio-standards/btap/common_resources/costs_local_factors.csv +0 -2315
  131. data/lib/openstudio-standards/btap/common_resources/materials_glazing.csv +0 -35
  132. data/lib/openstudio-standards/btap/common_resources/materials_opaque.csv +0 -164
  133. data/lib/openstudio-standards/btap/costing/btap_measure_helper.rb +0 -359
  134. data/lib/openstudio-standards/btap/costing/btap_workflow.rb +0 -117
  135. data/lib/openstudio-standards/btap/costing/copy_test_results_files_to_expected_results.rb +0 -11
  136. data/lib/openstudio-standards/btap/costing/cost_building_from_file.rb +0 -136
  137. data/lib/openstudio-standards/btap/costing/costing_database_wrapper.rb +0 -173
  138. data/lib/openstudio-standards/btap/costing/dummy.epw +0 -8768
  139. data/lib/openstudio-standards/btap/costing/dummy.osm +0 -5320
  140. data/lib/openstudio-standards/btap/costing/neb_end_use_prices.csv +0 -42
  141. data/lib/openstudio-standards/btap/costing/necb_2011_spacetype_info.csv +0 -225
  142. data/lib/openstudio-standards/btap/costing/necb_reference_runs.csv +0 -28705
  143. data/lib/openstudio-standards/btap/costing/parallel_tests.rb +0 -92
  144. data/lib/openstudio-standards/btap/costing/test_list.txt +0 -15
  145. data/lib/openstudio-standards/btap/costing/test_run_all_test_locally.rb +0 -27
  146. data/lib/openstudio-standards/btap/costing/test_run_costing_tests.rb +0 -80
  147. data/lib/openstudio-standards/btap/necb_2011_spacetype_info.csv +0 -225
  148. data/lib/openstudio-standards/btap/utilities/Add_template_field_to_json.rb +0 -22
  149. data/lib/openstudio-standards/btap/utilities/convert_surfaces_to_adiabatic_necb_8426.rb +0 -204
  150. data/lib/openstudio-standards/btap/utilities/generate_space_types.rb +0 -8
  151. data/lib/openstudio-standards/btap/utilities/necb_to_epw_map.rb +0 -50
  152. data/lib/openstudio-standards/btap/utilities/os_sim_extract.rb +0 -48
  153. data/lib/openstudio-standards/btap/utilities/rename_surfaces.rb +0 -24
  154. data/lib/openstudio-standards/btap/utilities/round_surf_coords.rb +0 -32
  155. data/lib/openstudio-standards/btap/utilities/sched_create.rb +0 -145
  156. data/lib/openstudio-standards/btap/utilities/set_mult_to_adiabatic.rb +0 -53
  157. data/lib/openstudio-standards/btap/utilities/space_type.rb +0 -78
  158. data/lib/openstudio-standards/btap/utilities/weatherData1_xlsx_to_json.rb +0 -56
  159. data/lib/openstudio-standards/btap/vintagizer.rb +0 -161
  160. data/lib/openstudio-standards/btap/visualization.rb +0 -72
  161. data/lib/openstudio-standards/standards/necb/NECB2011/data/necb_2011_spacetype_info.csv +0 -225
  162. data/lib/openstudio-standards/standards/necb/common/btap_analysis.rb +0 -79
  163. data/lib/openstudio-standards/standards/necb/common/btap_data.rb +0 -2656
  164. data/lib/openstudio-standards/standards/necb/common/btap_datapoint.rb +0 -653
  165. data/lib/openstudio-standards/standards/necb/common/necb_reference_runs.csv +0 -28705
  166. data/lib/openstudio-standards/standards/necb/common/utility_pricing_2025-02-20.csv +0 -92
  167. /data/lib/openstudio-standards/btap/{common_resources → common}/carbon_frame.csv +0 -0
  168. /data/lib/openstudio-standards/btap/{common_resources → common}/carbon_glazing.csv +0 -0
  169. /data/lib/openstudio-standards/btap/{common_resources → common}/carbon_opaque.csv +0 -0
  170. /data/lib/openstudio-standards/btap/{common_resources → common}/hvac_vent_ahu.csv +0 -0
  171. /data/lib/openstudio-standards/btap/{common_resources → common}/lighting.csv +0 -0
  172. /data/lib/openstudio-standards/btap/{common_resources → common}/lighting_sets.csv +0 -0
  173. /data/lib/openstudio-standards/btap/{common_resources → common}/locations.csv +0 -0
  174. /data/lib/openstudio-standards/btap/{costing → common}/mech_sizing.json +0 -0
@@ -0,0 +1,2293 @@
1
+ module BTAP
2
+ class Data
3
+ attr_accessor :osa_file
4
+ attr_accessor :osw_file
5
+ attr_accessor :osm_file
6
+ attr_accessor :sqlite_file
7
+ attr_accessor :btap_data
8
+
9
+ def initialize(
10
+ model:,
11
+ runner: nil,
12
+ cost_result:,
13
+ carbon_result:,
14
+ baseline_cost_equipment_total_cost_per_m_sq: -1.0,
15
+ baseline_cost_utility_neb_total_cost_per_m_sq: -1.0,
16
+ baseline_energy_eui_total_gj_per_m_sq: -1.0,
17
+ qaqc:,
18
+ npv_start_year: 2022,
19
+ npv_end_year: 2041,
20
+ npv_discount_rate: 0.03,
21
+ npv_discount_rate_carbon: 0.03,
22
+ oerd_utility_pricing: nil,
23
+ utility_pricing_year: 2020)
24
+
25
+ oerd_utility_pricing = false
26
+ @model = model
27
+ @error_warning = []
28
+ @sqlite_file = model.sqlFile
29
+ @standard = Standard.build('NECB2011')
30
+ @standards_data = @standard.load_standards_database_new()
31
+ @btap_data = {}
32
+ @btap_results_version = 1.00
33
+
34
+ # Conditioned floor area is used so much. May as well make it a object variable.
35
+ # setup the queries
36
+ command = "SELECT Value
37
+ FROM TabularDataWithStrings
38
+ WHERE ReportName='AnnualBuildingUtilityPerformanceSummary'
39
+ AND ReportForString='Entire Facility'
40
+ AND TableName='Building Area'
41
+ AND RowName = 'Net Conditioned Building Area'
42
+ AND ColumnName='Area'"
43
+ area = @sqlite_file.get.execAndReturnFirstDouble(command)
44
+ # make sure all the data are available
45
+ if area.empty?
46
+ @conditioned_floor_area_m_sq = 0.0
47
+ else
48
+ @conditioned_floor_area_m_sq = area.get
49
+ end
50
+
51
+
52
+ @btap_data['simulation_btap_data_version'] = '0.1'
53
+ # @btap_data["simulation_openstudio_version"] = open("| \"#{OpenStudio.getOpenStudioCLI}\" openstudio_version").read().strip
54
+ # @btap_data["simulation_energyplus_version"] = open("| \"#{OpenStudio.getOpenStudioCLI}\" energyplus_version").read().strip
55
+ @btap_data['simulation_os_standards_revision'] = OpenstudioStandards.git_revision
56
+ @btap_data['simulation_os_standards_version'] = OpenstudioStandards::VERSION
57
+ @btap_data['simulation_date'] = Time.now
58
+ @btap_data.merge!(building_data)
59
+ @btap_data.merge!(building_costing_data(cost_result)) unless cost_result.nil?
60
+ @btap_data.merge!(climate_data)
61
+ @btap_data.merge!(service_water_heating_data)
62
+ @btap_data.merge!(energy_eui_data(model))
63
+ @btap_data.merge!(energy_peak_data)
64
+ @btap_data.merge!(utility(model: model, utility_pricing_year: utility_pricing_year))
65
+ @btap_data.merge!(unmet_hours(model))
66
+ @btap_data.merge!outdoor_air_data(model)
67
+
68
+ # Data in tables...
69
+ @btap_data.merge!('measures_data_table' => measures_data_table(runner)) unless runner.nil?
70
+ @btap_data.merge!('envelope_exterior_surface_table' => envelope_exterior_surface_table)
71
+ @btap_data.merge!('space_table' => space_table(model, cost_result))
72
+ @btap_data.merge!('space_type_table' => space_type_table(model))
73
+ # This does not work with the new VRF or CCASHP systems. Commenting it for now.
74
+ # @btap_data.merge!({'zone_table' => thermal_zones_table(model, cost_result)['table']})
75
+ @btap_data.merge!('zone_equip_table' => thermal_zones_equipment_table(model))
76
+ # This does not work with the new VRF or CCASHP systems. Commenting it for now.
77
+ # @btap_data.merge!({'air_loop_table' => air_loops_table(model, cost_result)})
78
+ # @btap_data.merge!({'sql_raw_data' => sql_data_tables(model)})
79
+ @btap_data.merge!('eplusout_err_table' => eplusout_err_table(model))
80
+
81
+ # Remainder of costing data in separate tables:
82
+ @btap_data.merge!('envelope_construction_cost_table' => cost_result['envelope']['construction_costs']) unless cost_result.nil?
83
+ @btap_data.merge!('lighting_fixture_cost_table' => cost_result['lighting']['fixture_report']) unless cost_result.nil?
84
+ ideal_air = true
85
+ model.getThermalZones.each do |zone|
86
+ ideal_air = false if zone.useIdealAirLoads == false
87
+ end
88
+ unless ideal_air
89
+ @btap_data.merge!('h_and_c_plant_equipment_cost_table' => cost_result['heating_and_cooling']['plant_equipment']) unless cost_result.nil?
90
+ @btap_data.merge!('h_and_c_plant_zonal_systems_cost_table' => cost_result['heating_and_cooling']['zonal_systems']) unless cost_result.nil?
91
+ # This does not work with the new VRF or CCASHP systems. Commenting it for now.
92
+ # @btap_data.merge!('system_coils_cost_table' => coil_cost_table(cost_result))
93
+ # This does not work with the new VRF or CCASHP systems. Commenting it for now.
94
+ # @btap_data.merge!('terminal_VAV_cost_table' => terminal_VAV_cost_table(cost_result))
95
+ # This does not work with the new VRF or CCASHP systems. Commenting it for now.
96
+ # @btap_data.merge!('trunk_ducts_cost_table' => trunk_ducts_cost_table(cost_result))
97
+ end
98
+ # calculate energy demands and peak loads calculations as per PHIUS and NECB and compare them
99
+ phius_performance_indicators(model)
100
+ # The below method calculates energy performance indicators (i.e. TEDI and MEUI) as per BC Energy Step Code
101
+ bc_energy_step_code_performance_indicators
102
+ # calculates annual peak electricity cost (dollar)
103
+ annual_peak_kW_per_m_sq = oerd_electricity_cost(model: model, utility_pricing_year: utility_pricing_year, oerd_utility_pricing: oerd_utility_pricing)
104
+ # calculate net present value
105
+ net_present_value(npv_start_year: npv_start_year, npv_end_year: npv_end_year, npv_discount_rate: npv_discount_rate, npv_discount_rate_carbon: npv_discount_rate_carbon, oerd_utility_pricing: oerd_utility_pricing, annual_peak_kW_per_m_sq: annual_peak_kW_per_m_sq) unless cost_result.nil?
106
+
107
+ measure_metrics(qaqc)
108
+ @btap_data
109
+ end
110
+
111
+ class << self
112
+ def validate_optional(var, model, return_value = "N/A")
113
+ if var.empty?
114
+ return return_value
115
+ else
116
+ return var.get
117
+ end
118
+ end
119
+
120
+ def look_up_csv_data(csv_fname, search_criteria)
121
+ options = { headers: :first_row,
122
+ converters: [:numeric] }
123
+ unless File.exist?(csv_fname)
124
+ raise "File: [#{csv_fname}] Does not exist"
125
+ end
126
+
127
+ matches = nil
128
+ headers = nil
129
+ CSV.open(csv_fname, 'r', **options) do |csv|
130
+
131
+ # Since CSV includes Enumerable we can use 'find_all' which will
132
+ # return all the elements of the Enumerble for which the block returns
133
+ # true
134
+ matches = csv.find_all do |row|
135
+ match = true
136
+ search_criteria.keys.each do |key|
137
+ match &&= (row[key].strip == search_criteria[key].strip)
138
+ end
139
+ match
140
+ end
141
+ headers = csv.headers
142
+ end
143
+ raise('More than one match') if matches.size > 1
144
+
145
+ puts "Zero matches found for [#{search_criteria}]" if matches.empty?
146
+ return matches[0]
147
+ end
148
+
149
+ def get_total_nominal_capacity(model)
150
+ total_nominal_capacity = 0
151
+ model.getSpaces.sort.each do |space|
152
+ unless space.thermalZone.empty?
153
+ zone_name = space.thermalZone.get.name.get.upcase
154
+ area = model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='InputVerificationandResultsSummary' AND ReportForString='Entire Facility' AND TableName='Zone Summary' AND ColumnName='Area' AND RowName='#{zone_name}'")
155
+ area = self.validate_optional(area, model, -1)
156
+ multiplier = model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='InputVerificationandResultsSummary' AND ReportForString='Entire Facility' AND TableName='Zone Summary' AND ColumnName='Multipliers' AND RowName='#{zone_name}'")
157
+ multiplier = self.validate_optional(multiplier, model, -1)
158
+ area_per_person = model.sqlFile().get().execAndReturnFirstDouble("SELECT Value FROM TabularDataWithStrings WHERE ReportName='InputVerificationandResultsSummary' AND ReportForString='Entire Facility' AND TableName='Zone Summary' AND ColumnName='People' AND RowName='#{zone_name}'")
159
+ area_per_person = self.validate_optional(area_per_person, model, -1)
160
+ next if area_per_person == 0
161
+ puts "area: #{area} multiplier: #{multiplier} area_per_person: #{area_per_person}"
162
+ total_nominal_capacity += area*multiplier/area_per_person
163
+ end
164
+ end
165
+ return total_nominal_capacity
166
+ end
167
+ end
168
+
169
+ # Oct-2019 JTB: This function must be passed a hash and will flatten mixtures of hashes
170
+ # and arrays of hashes. Embedded arrays are enumerated (starting at 1).
171
+ def flatten_mix(hash)
172
+ hash.each_with_object({}) do |(k, v), h|
173
+ if v.is_a?(Hash)
174
+ flatten_mix(v).map do |h_k, h_v|
175
+ h["#{k}.#{h_k}".to_sym] = h_v
176
+ end
177
+ elsif v.is_a?(Array)
178
+ v.map.with_index do |e, ndx|
179
+ if e.is_a?(Hash)
180
+ flatten_mix(e).map do |e_k, e_v|
181
+ h["#{k}.#{e_k}.#{ndx + 1}".to_sym] = e_v
182
+ end
183
+ # if there is another array within the array v, flatten more
184
+ # but this is as deep as we go with embedded arrays!
185
+ elsif e.is_a?(Array)
186
+ e.map.with_index do |e1, ndx1|
187
+ if e1.is_a?(Hash)
188
+ flatten_mix(e1).map do |e1_k, e1_v|
189
+ h["#{k}.#{e1_k}.#{ndx1 + 1}".to_sym] = e1_v
190
+ end
191
+ else
192
+ # Stop flattening here!
193
+ h[k] = v
194
+ end
195
+ end
196
+ else
197
+ h[k] = v
198
+ end
199
+ end
200
+ else
201
+ h[k] = v
202
+ end
203
+ end
204
+ end
205
+
206
+ # General Building Data that there is alway either zero of 1 of.
207
+ def building_data
208
+ # Store Building data.
209
+ building_data = {}
210
+ building_data['bldg_name'] = @model.building.get.name.get
211
+ building_data['bldg_conditioned_floor_area_m_sq'] = @conditioned_floor_area_m_sq
212
+ building_data['bldg_exterior_area_m_sq'] = @model.building.get.exteriorSurfaceArea # m_sq
213
+ building_data['bldg_volume_m_cu'] = @model.building.get.airVolume # m_cu
214
+ building_data['bldg_standards_template'] = @model.building.get.standardsTemplate.empty? ? nil : @model.building.get.standardsTemplate.get
215
+ building_data['bldg_standards_building_type'] = @model.building.get.standardsBuildingType.empty? ? nil : @model.building.get.standardsBuildingType.get
216
+ building_data['bldg_standards_number_of_stories'] = @model.building.get.standardsNumberOfStories.empty? ? nil : @model.building.get.standardsNumberOfStories.get
217
+ building_data['bldg_standards_number_of_above_ground_stories'] = @model.building.get.standardsNumberOfAboveGroundStories.empty? ? nil : @model.building.get.standardsNumberOfAboveGroundStories.get
218
+ building_data['bldg_standards_number_of_living_units'] = @model.building.get.standardsNumberOfLivingUnits.empty? ? nil : @model.building.get.standardsNumberOfAboveGroundStories.get
219
+ building_data['bldg_nominal_floor_to_ceiling_height'] = @model.building.get.nominalFloortoCeilingHeight.empty? ? nil : @model.building.get.nominalFloortoCeilingHeight.get
220
+ building_data['bldg_nominal_floor_to_floor_height'] = @model.building.get.nominalFloortoFloorHeight.empty? ? nil : @model.building.get.nominalFloortoFloorHeight.get
221
+ building_data['bldg_surface_to_volume_ratio'] = @model.building.get.exteriorSurfaceArea / @model.building.get.airVolume
222
+ building_data['bldg_fdwr'] = (BTAP::Geometry.get_fwdr(@model) * 100.0).round(1)
223
+ building_data['bldg_srr'] = (BTAP::Geometry.get_srr(@model) * 100.0).round(1)
224
+
225
+ return building_data
226
+ end
227
+
228
+ def building_costing_data(cost_result)
229
+ building_data = {}
230
+ building_data['cost_province_state'] = cost_result['province_state']
231
+ building_data['cost_city'] = cost_result['city']
232
+ building_data['cost_equipment_envelope_total_cost_per_m_sq'] = (cost_result['totals']['envelope']) / @conditioned_floor_area_m_sq
233
+ building_data['cost_equipment_thermal_bridging_total_cost_per_m_sq'] = (cost_result['totals']['thermal_bridging']) / @conditioned_floor_area_m_sq
234
+ building_data['cost_equipment_lighting_total_cost_per_m_sq'] = (cost_result['totals']['lighting']) / @conditioned_floor_area_m_sq
235
+ building_data['cost_equipment_heating_and_cooling_total_cost_per_m_sq'] = (cost_result['totals']['heating_and_cooling']) / @conditioned_floor_area_m_sq
236
+ building_data['cost_equipment_shw_total_cost_per_m_sq'] = (cost_result['totals']['shw']) / @conditioned_floor_area_m_sq
237
+ building_data['cost_equipment_ventilation_total_cost_per_m_sq'] = (cost_result['totals']['ventilation']) / @conditioned_floor_area_m_sq
238
+ building_data['cost_equipment_renewables_total_cost_per_m_sq'] = (cost_result['totals']['renewables']) / @conditioned_floor_area_m_sq
239
+ building_data['cost_equipment_total_cost_per_m_sq'] = (cost_result['totals']['grand_total']) / @conditioned_floor_area_m_sq
240
+ building_data['unrealistic_assemblies'] = \
241
+ cost_result['envelope'].has_key?('unrealistic_assembly_cost') ? true : false
242
+ # building_data.merge!(cost_result['envelope'].select{|k,v| k!='construction_costs' && k!='total_envelope_cost'})
243
+ # building_data.merge!(cost_result['shw'].select{|k,v| k!='shw_total'})
244
+ # building_data.merge!(flatten_mix(cost_result['ventilation'].select{|k,v| k=='mech_to_roof'.to_sym}))
245
+ return building_data
246
+ end
247
+
248
+ def net_present_value(npv_start_year: 2022, npv_end_year: 2041, npv_discount_rate: 0.03, npv_discount_rate_carbon: 0.03, oerd_utility_pricing: false, annual_peak_kW_per_m_sq: nil)
249
+
250
+ # Find end year in the neb data
251
+ neb_header = CSV.read(Paths::NEB_PRICES_PATH, headers: true).headers
252
+ neb_header.delete_if { |item| ["building_type", "province", "fuel_type"].include?(item) } # remove "building_type", "province", "fuel_type" from neb_header in order to have only years in neb_header
253
+ neb_header.delete_if { |item| ["rate_class", "units", "references", "links"].include?(item) } if oerd_utility_pricing # remove additional headers if the OERD data is used
254
+ neb_header.map(&:to_f) #convert years to float
255
+ year_max = neb_header.max
256
+
257
+ # Convert a string to a float
258
+ if npv_start_year.instance_of?(String) && npv_start_year != 'NECB_Default' && npv_start_year != 'none' && !npv_start_yeay.nil?
259
+ npv_start_year = npv_start_year.to_f
260
+ end
261
+ if npv_end_year.instance_of?(String) && npv_end_year != 'NECB_Default' && npv_end_year != 'none' && !npv_end_year.nil?
262
+ npv_end_year = npv_end_year.to_f
263
+ end
264
+ if npv_discount_rate.instance_of?(String) && npv_discount_rate != 'NECB_Default' && npv_discount_rate != 'none' && !npv_discount_rate.nil?
265
+ npv_discount_rate = npv_discount_rate.to_f
266
+ end
267
+ if npv_discount_rate_carbon.instance_of?(String) && npv_discount_rate_carbon != 'NECB_Default' && npv_discount_rate_carbon != 'none' && !npv_discount_rate_carbon.nil?
268
+ npv_discount_rate_carbon = npv_discount_rate_carbon.to_f
269
+ end
270
+
271
+
272
+ # Set default npv_start_year as 2022, npv_end_year as 2041, npv_discount_rate as 3%
273
+ if npv_start_year == 'NECB_Default' || npv_start_year.nil? || npv_start_year == 'none'
274
+ npv_start_year = 2022
275
+ end
276
+ if npv_end_year == 'NECB_Default' || npv_end_year.nil? || npv_end_year == 'none'
277
+ npv_end_year = 2041
278
+ end
279
+ if npv_discount_rate == 'NECB_Default' || npv_discount_rate.nil? || npv_discount_rate == 'none'
280
+ npv_discount_rate = 0.03
281
+ end
282
+ if npv_discount_rate_carbon == 'NECB_Default' || npv_discount_rate_carbon.nil? || npv_discount_rate_carbon == 'none'
283
+ npv_discount_rate_carbon = 0.03
284
+ end
285
+
286
+ # Set npv_end_year as year_max if users' input > neb's end year
287
+ if npv_end_year > year_max.to_f
288
+ npv_end_year = year_max.to_f
289
+ warn "WARNING: Your npv_end_year for the calculation of net present value is larger than that in Canada Energy Regulator (CER) (i.e. #{year_max}). So, npv_end_year has been reset as #{year_max}."
290
+ end
291
+
292
+ # Create a hash of the neb data.
293
+ neb_data = CSV.parse(File.read(Paths::NEB_PRICES_PATH), headers: true, converters: :numeric).map(&:to_h)
294
+
295
+ # Create a hash of the ECCC's electricity emissions factors for the current and future years for GHG "without Biomass and RNG CO2 emissions".
296
+ eccc_ghg_elec_emission_factors = CSV.parse(File.read(Paths::ECCC_GHG_ELECTRICITY_PATH), headers: true, converters: :numeric).map(&:to_h)
297
+
298
+ # Find which province the proposed building is located in
299
+ building_type = 'Commercial'
300
+ geography_data = climate_data
301
+ province_abbreviation = geography_data['location_state_province_region']
302
+ province = @standards_data['province_map'][province_abbreviation]
303
+
304
+ # Note: If there is on-site energy generation (e.g. PV), it should be considered in the calculation of EUI for the calculation of energy use cost and NPV.
305
+ # To do so, it has been assumed that on-site energy generation is only for electricity.
306
+ # Electricity EUI of a building is re-calculated for NPV. It will be: ['energy_eui_electricity_gj_per_m_sq' - ('total_site_eui_gj_per_m_sq' - 'net_site_eui_gj_per_m_sq')]
307
+ # Note that if there is no on-site energy generation, 'total_site_eui_gj_per_m_sq' and 'net_site_eui_gj_per_m_sq' will be equal.
308
+ # Note: 'total_site_eui_gj_per_m_sq' is the gross energy consumed by the building (REF: https://unmethours.com/question/25416/what-is-the-difference-between-site-energy-and-source-energy/)
309
+ # Note: 'net_site_eui_gj_per_m_sq' is the final energy consumed by the building after accounting for on-site energy generations (e.g. PV) (REF: https://unmethours.com/question/25416/what-is-the-difference-between-site-energy-and-source-energy/)
310
+
311
+ # Calculate npv of electricity
312
+ onsite_elec_generation = @btap_data['total_site_eui_gj_per_m_sq'] - @btap_data['net_site_eui_gj_per_m_sq']
313
+ if onsite_elec_generation > 0.0
314
+ eui_elec = @btap_data['energy_eui_electricity_gj_per_m_sq'] - onsite_elec_generation
315
+ else
316
+ eui_elec = @btap_data['energy_eui_electricity_gj_per_m_sq']
317
+ end
318
+ # Find the appropriate row for electricity GHG emmission factors
319
+ row_emm_factor = eccc_ghg_elec_emission_factors.detect do |data|
320
+ (data['province'] == province)
321
+ end
322
+ # Find the appropriate row for electricity pricing
323
+ row = neb_data.detect do |data|
324
+ (data['building_type'] == building_type) && (data['province'] == province) && (data['fuel_type'] == 'Electricity')
325
+ end
326
+ row_peak = nil
327
+ if oerd_utility_pricing && !annual_peak_kW_per_m_sq.nil?
328
+ row_peak = neb_data.detect do |data|
329
+ (data['building_type'] == building_type) && (data['province'] == province) && (data['fuel_type'] == 'Electricity Peak')
330
+ end
331
+ end
332
+ npv_elec = 0.0
333
+ year_index = 1.0
334
+ npv_elec_ghg = 0.0
335
+ npv_elec_peak = 0.0
336
+ emm_factor_conv = 3600.0 #GWH to GJ
337
+ if eui_elec > 0.0
338
+ for year in npv_start_year.to_int..npv_end_year.to_int
339
+ npv_elec += (eui_elec * row[year.to_s]) / (1+npv_discount_rate)**year_index
340
+ npv_elec_ghg += (((eui_elec * row_emm_factor[year.to_i.to_s].to_f) / emm_factor_conv) * get_national_ghg_cost(year: year.to_i)) / (1+npv_discount_rate_carbon)**year_index
341
+ npv_elec_peak += (annual_peak_kW_per_m_sq * row_peak[year.to_i.to_s]).to_f / (1+npv_discount_rate)**year_index unless row_peak.nil? || annual_peak_kW_per_m_sq.nil?
342
+ year_index += 1.0
343
+ end
344
+ end
345
+
346
+ # Calculate npv of natural gas
347
+ eui_ngas= @btap_data['energy_eui_natural_gas_gj_per_m_sq']
348
+ # Convert to tonnes of ghg/m^2
349
+ ghg_ngas = @btap_data['cost_utility_ghg_natural gas_kg_per_m_sq'].to_f/1000.0
350
+ row = neb_data.detect do |data|
351
+ (data['building_type'] == building_type) && (data['province'] == province) && (data['fuel_type'] == 'Natural Gas')
352
+ end
353
+ npv_ngas = 0.0
354
+ npv_ngas_ghg = 0.0
355
+ year_index = 1.0
356
+ for year in npv_start_year.to_int..npv_end_year.to_int
357
+ npv_ngas += (eui_ngas * row[year.to_s]) / (1+npv_discount_rate)**year_index
358
+ npv_ngas_ghg += (ghg_ngas * get_national_ghg_cost(year: year.to_i)) / (1+npv_discount_rate_carbon)**year_index
359
+ year_index += 1.0
360
+ end
361
+ # puts "npv_ngas is #{npv_ngas}"
362
+
363
+ # Calculate npv of oil
364
+ eui_oil= @btap_data['energy_eui_additional_fuel_gj_per_m_sq']
365
+ # Convert to tonnes of ghg/m^2
366
+ ghg_oil = @btap_data['cost_utility_ghg_oil_kg_per_m_sq'].to_f/1000.0
367
+ row = neb_data.detect do |data|
368
+ (data['building_type'] == building_type) && (data['province'] == province) && (data['fuel_type'] == 'Oil')
369
+ end
370
+ npv_oil = 0.0
371
+ npv_oil_ghg = 0.0
372
+ year_index = 1.0
373
+ for year in npv_start_year.to_int..npv_end_year.to_int
374
+ npv_oil += (eui_oil * row[year.to_s]) / (1+npv_discount_rate)**year_index
375
+ npv_oil_ghg += (ghg_oil * get_national_ghg_cost(year: year.to_i)) / (1+npv_discount_rate_carbon)**year_index
376
+ year_index += 1.0
377
+ end
378
+ # puts "npv_oil is #{npv_oil}"
379
+
380
+ # Calculate total npv
381
+ npv_total = @btap_data['cost_equipment_total_cost_per_m_sq'] + npv_elec + npv_ngas + npv_oil
382
+ npv_ghg_total = npv_elec_ghg + npv_ngas_ghg + npv_oil_ghg
383
+ npv_total_with_ghg_and_peak = npv_ghg_total + npv_total + npv_elec_peak
384
+ @btap_data.merge!('npv_total_per_m_sq' => npv_total)
385
+ @btap_data.merge!('npv_carbon_pricing_per_m_sq' => npv_ghg_total)
386
+ @btap_data.merge!('npv_peak_elec_per_m_sq' => npv_elec_peak)
387
+ @btap_data.merge!('npv_with_carbon_and_peak_pricing_total_per_m_sq' => npv_total_with_ghg_and_peak)
388
+ end
389
+
390
+ def envelope_exterior_surface_table
391
+ surfaces = @model.getSurfaces.sort
392
+ outdoor_surfaces = BTAP::Geometry::Surfaces.filter_by_boundary_condition(surfaces, 'Outdoors')
393
+ ground_surfaces = BTAP::Geometry::Surfaces.filter_by_boundary_condition(surfaces, ['Ground', 'Foundation'])
394
+ exterior_opaque_surfaces = outdoor_surfaces + ground_surfaces
395
+ # outdoor_surfaces.each { |surface| puts surface.name}
396
+ # get surface table from sql
397
+ table = get_sql_table_to_json(@model, 'EnvelopeSummary', 'Entire Facility', 'Opaque Exterior')
398
+ raise('Could not get opaque surface table from E+ sql') if table.empty?
399
+
400
+ # add space name to table.
401
+ table['table'].each do |row|
402
+ surface = exterior_opaque_surfaces.detect { |curr_surface| curr_surface.name.get.downcase == row['name'].downcase }
403
+ raise("Could not find surface #{row['name'].downcase} in #{outdoor_surfaces.map { |curr_surface| curr_surface.name.get.downcase }}") if surface.nil?
404
+
405
+ row['os_type'] = surface.surfaceType
406
+ row['boundary_condition'] = surface.outsideBoundaryCondition
407
+ space_includes_surface = @model.getSpaces.detect { |space| space.surfaces.include?(surface) }
408
+ row['space_name'] = space_includes_surface.nil? || !space_includes_surface.name.is_initialized ? 'NA' : space_includes_surface.name.get
409
+ end
410
+ opaque = table
411
+
412
+ # Fenestrations
413
+ fenestrations = BTAP::Geometry::Surfaces.filter_subsurfaces_by_types(@model.getSubSurfaces.sort, ['GlassDoor', 'FixedWindow', 'OperableWindow', 'Skylight', 'TubularDaylightDiffuser', 'TubularDaylightDome'])
414
+ # get surface table from sql
415
+ table = get_sql_table_to_json(@model, 'EnvelopeSummary', 'Entire Facility', 'Exterior Fenestration')
416
+ # Exclude totals and averages by deleting row with that in their name.
417
+ table['table'].delete_if { |row| !!(row['name'] =~ /Total|Average/) }
418
+ raise('Could not get fenestration surface table from E+ sql') if table.empty?
419
+
420
+ # add space name to table.
421
+ table['table'].each do |row|
422
+ subsurface = fenestrations.detect { |surface| surface.name.get.downcase == row['name'].downcase }
423
+ raise("Could not find surface #{row['name'].downcase} in #{fenestrations.map { |surface| surface.name.get.downcase }}") if subsurface.nil?
424
+
425
+ row['os_type'] = subsurface.subSurfaceType
426
+ row['boundary_condition'] = subsurface.outsideBoundaryCondition
427
+ parent_surface = subsurface.surface.get
428
+ space_includes_surface = @model.getSpaces.detect { |space| space.surfaces.include?(parent_surface) }
429
+ row['space_name'] = space_includes_surface.nil? || !space_includes_surface.name.is_initialized ? 'NA' : space_includes_surface.name.get
430
+ end
431
+ glazing = table
432
+
433
+ # return as a single table.
434
+
435
+ return glazing['table'] + opaque['table']
436
+ end
437
+
438
+ def space_table(model, cost_result)
439
+ # Store Space data.
440
+ table = []
441
+ model.getSpaces.sort.each do |space|
442
+ spaceinfo = {}
443
+ table << spaceinfo
444
+ spaceinfo['thermal_zone_name'] = space.thermalZone.empty? ? 'None' : space.thermalZone.get.name.get # should be assigned a thermalzone name.
445
+ spaceinfo['space_name'] = space.name.get # name should be defined test
446
+ spaceinfo['multiplier'] = space.multiplier
447
+ spaceinfo['volume'] = space.volume # should be greater than zero
448
+ spaceinfo['exterior_wall_area'] = space.exteriorWallArea # just for information.
449
+ spaceinfo['space_type_name'] = space.spaceType.get.name.get unless space.spaceType.empty? # should have a space types name defined.
450
+ spaceinfo['breathing_zone_outdoor_airflow_vbz'] = -1
451
+ spaceinfo['infiltration_flow_per_m_sq'] = space.infiltrationDesignFlowPerExteriorSurfaceArea
452
+ spaceinfo['floor_area_m2'] = space.floorArea
453
+ spaceinfo['building_type'] = space.spaceType.get.standardsBuildingType.empty? ? 'None' : space.spaceType.get.standardsBuildingType.get
454
+ spaceinfo['is_conditioned'] = space.thermalZone.get.isConditioned.get unless space.thermalZone.empty? or space.thermalZone.get.isConditioned.empty?
455
+ # shw
456
+ spaceinfo['shw_peak_flow_rate_m_cu_per_s'] = 0
457
+ spaceinfo['shw_peak_flow_rate_per_floor_area_m_cu_per_s_per_m_sq'] = 0
458
+ space.waterUseEquipment.each do |equipment|
459
+ spaceinfo['shw_peak_flow_rate_m_cu_per_s'] += equipment.waterUseEquipmentDefinition.peakFlowRate
460
+ spaceinfo['shw_peak_flow_rate_per_floor_area_m_cu_per_s_per_m_sq'] += equipment.waterUseEquipmentDefinition.peakFlowRate / space.floorArea
461
+ area_per_occ = space.spaceType.get.getFloorAreaPerPerson(space.floorArea)
462
+ # Watt per person = m_cu/s/m_cu * 1000W/kW * (specific heat * dT) * m_sq/person
463
+ spaceinfo['shw_watts_per_person'] = spaceinfo['shw_peak_flow_rate_per_floor_area_m_cu_per_s_per_m_sq'] * 1000 * (4.19 * 44.4) * 1000 * area_per_occ
464
+ end
465
+ unless cost_result.nil?
466
+ # Including space level lighting costs in the existing space table...
467
+ spaceLgtInfo = cost_result['lighting']['space_report'].detect { |curr_spaceLgtInfo| curr_spaceLgtInfo['zone'].downcase == spaceinfo['thermal_zone_name'].downcase }
468
+ raise("Could not find zone name \"#{spaceinfo['thermal_zone_name']}\" in lighting space_report") if spaceLgtInfo.nil?
469
+
470
+ spaceinfo['space_type'] = spaceLgtInfo['space_type']
471
+ spaceinfo['fixture_type'] = spaceLgtInfo['fixture_type']
472
+ # Note spelling mistake of "description" in cost_result hash fixed below in copy
473
+ spaceinfo['fixture_description'] = spaceLgtInfo['fixture_desciption']
474
+ spaceinfo['height_avg_ft'] = spaceLgtInfo['height_avg_ft']
475
+ spaceinfo['floor_area_ft2'] = spaceLgtInfo['floor_area_ft2']
476
+ spaceinfo['lighting_cost'] = spaceLgtInfo['cost']
477
+ spaceinfo['lighting_cost_per_ft2'] = spaceLgtInfo['cost_per_ft2']
478
+ spaceinfo['lighting_note'] = spaceLgtInfo['note']
479
+ end
480
+ end
481
+ table.sort_by! { |spaceinfo| [spaceinfo['thermal_zone_name'], spaceinfo['space_name']] }
482
+ return table
483
+ end
484
+
485
+ def climate_data
486
+ # Store Geography Data
487
+ geography_data = {}
488
+ geography_data['location_necb_hdd'] = @standard.get_necb_hdd18(model: @model, necb_hdd: true)
489
+ geography_data['location_weather_file'] = File.basename(@model.getWeatherFile.path.get.to_s)
490
+ weather_file_path = @model.weatherFile.get.path.get.to_s
491
+ stat_file_path = weather_file_path.gsub('.epw', '.stat')
492
+ stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
493
+ geography_data['location_epw_cdd'] = stat_file.cdd18
494
+ geography_data['location_epw_hdd'] = stat_file.hdd18
495
+ geography_data['location_necb_climate_zone'] = @standard.get_climate_zone_name(geography_data['location_necb_hdd'])
496
+ geography_data['location_city'] = @model.getWeatherFile.city
497
+ geography_data['location_state_province_region'] = @model.getWeatherFile.stateProvinceRegion
498
+ geography_data['location_country'] = @model.getWeatherFile.country
499
+ geography_data['location_latitude'] = @model.getWeatherFile.latitude
500
+ geography_data['location_longitude'] = @model.getWeatherFile.longitude
501
+ return geography_data
502
+ end
503
+
504
+ ### The 'annual_peak_electricity_cost_dollar' function calculates annual peak electricity cost (dollar)
505
+ ### for OEE-Electrification project using the OERD's electricity rates for commercial buildings
506
+ ### that Danielle Krauel of OEE provided to CE-O
507
+ def oerd_electricity_cost(model:, utility_pricing_year: 2020, oerd_utility_pricing: false)
508
+ unless oerd_utility_pricing
509
+ @btap_data.merge!('cost_utility_oerd_electricity_peak_annual_cost_per_m_sq' => 0.00) unless oerd_utility_pricing
510
+ @btap_data.merge!('cost_utility_oerd_electricity_energy_annual_cost_per_m_sq' => 0.00) unless oerd_utility_pricing
511
+ return 0.00
512
+ end
513
+
514
+ #===================================================================================================================
515
+ ### OERD's electricity rates for commercial buildings
516
+ neb_data = CSV.parse(File.read(Paths::NEB_PRICES_PATH), headers: true, converters: :numeric).map(&:to_h)
517
+ #===================================================================================================================
518
+ #===================================================================================================================
519
+ #===================================================================================================================
520
+ #===================================================================================================================
521
+ #===================================================================================================================
522
+ ##################### Calculate annual electricity peak cost using OERD's electricity rate #########################
523
+ #===================================================================================================================
524
+ #===================================================================================================================
525
+ ### Find index for hourly data of 'ElectricityNet:Facility' (J)
526
+ query = "
527
+ SELECT ReportDataDictionaryIndex
528
+ FROM ReportDataDictionary
529
+ WHERE Name=='ElectricityNet:Facility' AND ReportingFrequency == 'Hourly' AND Units == 'J'
530
+ "
531
+ index_timestep_electricity = model.sqlFile.get.execAndReturnVectorOfInt(query).get[0]
532
+ number_of_timesteps_per_hour = 1
533
+ puts "Trying hourly ElectricityNet:Facility"
534
+
535
+ ### Find index for timestep data of 'Electricity:Facility' (J) if 'ElectricityNet:Facility' data is not available
536
+ if index_timestep_electricity.nil?
537
+ query = "
538
+ SELECT ReportDataDictionaryIndex
539
+ FROM ReportDataDictionary
540
+ WHERE Name=='Electricity:Facility' AND ReportingFrequency == 'Zone Timestep' AND Units == 'J'
541
+ "
542
+ index_timestep_electricity = model.sqlFile.get.execAndReturnVectorOfInt(query).get[0]
543
+ number_of_timesteps_per_hour = model.getTimestep.numberOfTimestepsPerHour
544
+ puts "Trying timestep Electricity:Facility"
545
+ end
546
+
547
+ ### Find index for hourly data of 'Electricity:Facility' (J) if timestep data is not available
548
+ if index_timestep_electricity.nil?
549
+ query = "
550
+ SELECT ReportDataDictionaryIndex
551
+ FROM ReportDataDictionary
552
+ WHERE Name=='Electricity:Facility' AND ReportingFrequency == 'Hourly' AND Units == 'J'
553
+ "
554
+ index_timestep_electricity = model.sqlFile.get.execAndReturnVectorOfInt(query).get[0]
555
+ number_of_timesteps_per_hour = 1
556
+ puts "Trying hourly Electricity:Facility"
557
+ end
558
+
559
+ if index_timestep_electricity.nil?
560
+ puts "No hourly or timestep data available. Peak data not available"
561
+ @btap_data.merge!('cost_utility_oerd_electricity_peak_annual_cost_per_m_sq' => 0.00) unless oerd_utility_pricing
562
+ @btap_data.merge!('cost_utility_oerd_electricity_energy_annual_cost_per_m_sq' => 0.00) unless oerd_utility_pricing
563
+ return 0.00
564
+ end
565
+ ### Get number of timesteps from the model
566
+ timestep_second = 3600 / number_of_timesteps_per_hour
567
+ #===================================================================================================================
568
+ ### Calculate number of timesteps of the whole year, and create dates/times for the whole year
569
+ number_of_timesteps_of_year = 365 * 24 * number_of_timesteps_per_hour
570
+ timesteps_of_year = []
571
+ d = Time.new(2006, 1, 1, 0)
572
+ (0...number_of_timesteps_of_year).each do |increment|
573
+ timesteps_of_year << (d + (60 * 60 / number_of_timesteps_per_hour) * increment).strftime('%Y-%m-%d %H:%M')
574
+ end
575
+ #===================================================================================================================
576
+
577
+ #===================================================================================================================
578
+ ### Get timestep output for Electricity:Facility output.
579
+ query = "
580
+ Select VariableValue
581
+ FROM ReportVariableData
582
+ WHERE
583
+ ReportVariableDataDictionaryIndex = #{index_timestep_electricity}
584
+ "
585
+ timestep_values = model.sqlFile.get.execAndReturnVectorOfDouble(query).get
586
+ #===================================================================================================================
587
+ ### Calculate electricity peak (J) for each month of the year
588
+ # Find month of each timestep
589
+ keys_month = timesteps_of_year.map{ |timestep| DateTime.parse(timestep).to_date.month }
590
+ # Create a hash where its keys are months and values for eahc month are electricity energy use (J) of all timesteps for that month
591
+ hash_months_electricity = keys_month.zip(timestep_values).group_by(&:first).map{|key, value| [key, value.map(&:last)]}.to_h
592
+ # Find maximum of electricity energy use (J) among all timesteps for each month of the year
593
+ monthly_peak_J = hash_months_electricity.map { |key, value| [key, value.max()] }.to_h
594
+ #===================================================================================================================
595
+ ### Convert J to kW of maximum electricity energy use (J)
596
+ monthly_peak_kW = monthly_peak_J.map { |key, value| [key, value * 0.001 / timestep_second] }.to_h
597
+ ### Normalize by building floor area
598
+ annual_peak_kW_per_m_sq = monthly_peak_kW.values.sum / @btap_data['bldg_conditioned_floor_area_m_sq'].to_f
599
+ #===================================================================================================================
600
+ ### Get province of the model and find its peak electricity price from the utility_pricing_2025-02-20.csv
601
+ province = @btap_data['location_state_province_region']
602
+ # Find which province the proposed building is located in
603
+ building_type = 'Commercial'
604
+ geography_data = climate_data
605
+ province_abbreviation = geography_data['location_state_province_region']
606
+ province = @standards_data['province_map'][province_abbreviation]
607
+ row_peak = neb_data.detect do |data|
608
+ (data['building_type'] == building_type) && (data['province'] == province) && (data['fuel_type'] == 'Electricity Peak')
609
+ end
610
+ province_dollar_kW_month = row_peak[utility_pricing_year.to_i.to_s]
611
+ #===================================================================================================================
612
+ ### Calculate cost of peak electricity for each month
613
+ monthly_peak_cost = monthly_peak_kW.map { |key, value| [key, value * province_dollar_kW_month] }.to_h
614
+ #===================================================================================================================
615
+ ### Calculate cost of peak electricity for the whole year in dollar
616
+ annual_peak_cost_dollar = monthly_peak_cost.values.sum
617
+ #===================================================================================================================
618
+ ### Calculate cost of peak electricity for the whole year in dollar normalized by floor area
619
+ annual_peak_cost_dollar_per_m_sq = annual_peak_cost_dollar / @btap_data['bldg_conditioned_floor_area_m_sq']
620
+ #===================================================================================================================
621
+ ### Merge the calculated value to @btap_data
622
+ @btap_data.merge!('cost_utility_oerd_electricity_peak_annual_cost_per_m_sq' => annual_peak_cost_dollar_per_m_sq.round(2))
623
+ #===================================================================================================================
624
+ #===================================================================================================================
625
+ #===================================================================================================================
626
+ #===================================================================================================================
627
+ #===================================================================================================================
628
+ ################### Calculate annual electricity energy use cost using OERD's electricity rate #####################
629
+ #===================================================================================================================
630
+ ### Get province of the model and find its peak electricity price from the pricing_2025-02-20.csv
631
+ row_elec = neb_data.detect do |data|
632
+ (data['building_type'] == building_type) && (data['province'] == province) && (data['fuel_type'] == 'Electricity')
633
+ end
634
+ province_dollar_kWh = row_elec[utility_pricing_year.to_i.to_s]
635
+ #===================================================================================================================
636
+ # Calculate cost of energy electricity for the whole year in dollar normalized by floor area
637
+ annual_energy_cost_dollar_per_m_sq = @btap_data['energy_eui_electricity_gj_per_m_sq'] * 277.778 * province_dollar_kWh
638
+ #===================================================================================================================
639
+ ### Merge the calculated value to @btap_data
640
+ @btap_data.merge!('cost_utility_oerd_electricity_energy_annual_cost_per_m_sq' => annual_energy_cost_dollar_per_m_sq.round(2))
641
+ #===================================================================================================================
642
+ return annual_peak_kW_per_m_sq
643
+ end
644
+
645
+ def utility(model:, utility_pricing_year: 2020)
646
+ economics_data = {}
647
+ building_type = 'Commercial'
648
+ province = @standards_data['province_map'][model.getWeatherFile.stateProvinceRegion]
649
+ neb_eplus_fuel_map = {'Natural Gas' => {eplus_fuel_name: 'NaturalGas',
650
+ eplus_table_name: 'Annual and Peak Values - Natural Gas',
651
+ eplus_row_name: 'NaturalGas:Facility',
652
+ eplus_column_name: 'Natural Gas Annual Value'},
653
+ 'Electricity' => {eplus_fuel_name: 'Electricity',
654
+ eplus_table_name: 'Annual and Peak Values - Electricity',
655
+ eplus_row_name: 'Electricity:Facility',
656
+ eplus_column_name: 'Electricity Annual Value'},
657
+ 'Oil' => {eplus_fuel_name: 'FuelOilNo2',
658
+ eplus_table_name: 'Annual and Peak Values - Other',
659
+ eplus_row_name: 'FuelOilNo2:Facility',
660
+ eplus_column_name: 'Annual Value'}
661
+ }
662
+ economics_data['cost_utility_neb_total_cost_per_m_sq'] = 0.0
663
+ economics_data['cost_utility_ghg_total_kg_per_m_sq'] = 0.0
664
+ # Create a hash of the neb data.
665
+ neb_data = CSV.parse(File.read(Paths::NEB_PRICES_PATH), headers: true, converters: :numeric).map(&:to_h)
666
+
667
+ neb_eplus_fuel_map.each do |neb_fuel, ep_fuel|
668
+ next if neb_fuel.to_s.downcase == 'electricity peak'
669
+ row = neb_data.detect do |data|
670
+ (data['building_type'].to_s == building_type) &&
671
+ (data['province'].to_s == province) &&
672
+ (data['fuel_type'].to_s == neb_fuel)
673
+ end
674
+ utility_pricing_year_s = utility_pricing_year.to_i.to_s
675
+ neb_fuel_cost = row[utility_pricing_year_s]
676
+ fuel_consumption_gj = 0.0
677
+ sql_command = "SELECT Value FROM tabulardatawithstrings
678
+ WHERE ReportName='EnergyMeters'
679
+ AND ReportForString='Entire Facility'
680
+ AND TableName='#{ep_fuel[:eplus_table_name]}'
681
+ AND RowName='#{ep_fuel[:eplus_row_name]}'
682
+ AND ColumnName='#{ep_fuel[:eplus_column_name]}'
683
+ AND Units='GJ'"
684
+ fuel_consumption_gj = model.sqlFile.get.execAndReturnFirstDouble(sql_command).is_initialized ? model.sqlFile.get.execAndReturnFirstDouble(sql_command).get : 0.0
685
+
686
+ # Determine costs in $$
687
+ economics_data["cost_utility_neb_#{neb_fuel.downcase}_cost_per_m_sq"] = fuel_consumption_gj * neb_fuel_cost.to_f / @conditioned_floor_area_m_sq
688
+ economics_data['cost_utility_neb_total_cost_per_m_sq'] += economics_data["cost_utility_neb_#{neb_fuel.downcase}_cost_per_m_sq"]
689
+ # Determine cost in GHG kg of CO2
690
+ if neb_fuel.downcase == 'electricity' # Use ECCC data for utility emission factors rather than NEB data.
691
+ # Create a hash of the ECCC's electricity emissions factors for the current and future years for GHG "without Biomass and RNG CO2 emissions".
692
+ eccc_ghg_elec_emission_factors = CSV.parse(File.read(Paths::ECCC_GHG_ELECTRICITY_PATH), headers: true, converters: :numeric).map(&:to_h)
693
+ # Find the appropriate row for electricity GHG emmission factors
694
+ row_emm_factor = eccc_ghg_elec_emission_factors.detect { |data| (data['province'] == province) }
695
+ emm_factor_conv = 3600.0
696
+ economics_data["cost_utility_ghg_#{neb_fuel.downcase}_kg_per_m_sq"] = ((fuel_consumption_gj * row_emm_factor[utility_pricing_year.to_i.to_s].to_f) / emm_factor_conv) * 1000 / @conditioned_floor_area_m_sq
697
+ else
698
+ economics_data["cost_utility_ghg_#{neb_fuel.downcase}_kg_per_m_sq"] = fuel_consumption_gj * get_utility_ghg_kg_per_gj(province: model.getWeatherFile.stateProvinceRegion, fuel_type: ep_fuel[:eplus_fuel_name]) / @conditioned_floor_area_m_sq
699
+ end
700
+ economics_data['cost_utility_ghg_total_kg_per_m_sq'] += economics_data["cost_utility_ghg_#{neb_fuel.downcase}_kg_per_m_sq"]
701
+ end
702
+ # Commenting out block charge rates for now....
703
+
704
+ # Fuel cost based local utility rates
705
+ # sql_command = "SELECT RowName FROM TabularDataWithStrings
706
+ # WHERE ReportName='LEEDsummary'
707
+ # AND ReportForString='Entire Facility'
708
+ # AND TableName='EAp2-7. Energy Cost Summary'
709
+ # AND ColumnName='Total Energy Cost'"
710
+ # costing_rownames = model.sqlFile().get().execAndReturnVectorOfString(sql_command)
711
+
712
+ #==> ["Electricity", "Natural Gas", "Additional", "Total"]
713
+ # costing_rownames = self.class.validate_optional(costing_rownames, model, "N/A")
714
+ # unless costing_rownames == "N/A"
715
+ # costing_rownames.each do |rowname|
716
+ # sql_command = "SELECT Value FROM TabularDataWithStrings
717
+ # WHERE ReportName='LEEDsummary'
718
+ # AND ReportForString='Entire Facility'
719
+ # AND TableName='EAp2-7. Energy Cost Summary'
720
+ # AND ColumnName='Total Energy Cost'
721
+ # AND RowName='#{rowname}'"
722
+ # case rowname
723
+ # when "Electricity"
724
+ # economics_data["cost_utility_block_electricity_cost_per_m_sq"] = model.sqlFile().get().execAndReturnFirstDouble(sql_command).get / @conditioned_floor_area_m_sq
725
+ # when "Natural Gas"
726
+ # economics_data["cost_utility_block_natural_gas_cost_per_m_sq"] = model.sqlFile().get().execAndReturnFirstDouble(sql_command).get / @conditioned_floor_area_m_sq
727
+ # when "Additional"
728
+ # economics_data["cost_utility_block_additional_cost_per_m_sq"] = model.sqlFile().get().execAndReturnFirstDouble(sql_command).get / @conditioned_floor_area_m_sq
729
+ # when "Total"
730
+ # economics_data["cost_utility_block_total_cost_per_m_sq"] = model.sqlFile().get().execAndReturnFirstDouble(sql_command).get / @conditioned_floor_area_m_sq
731
+ # end
732
+ # end
733
+ # else
734
+ # @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'"
735
+ # end
736
+ return economics_data
737
+ end
738
+
739
+ def space_type_table(model)
740
+ table = []
741
+ model.getSpaceTypes.sort.each do |spaceType|
742
+ next if spaceType.floorArea == 0
743
+
744
+ # data for space type breakdown
745
+ display = spaceType.name.get
746
+ floor_area_si = 0
747
+ # loop through spaces so I can skip if not included in floor area
748
+ spaceType.spaces.sort.each do |space|
749
+ next if !space.partofTotalFloorArea
750
+
751
+ floor_area_si += space.floorArea * space.multiplier
752
+ end
753
+ space_type_info = {}
754
+ space_type_info['name'] = spaceType.name.get
755
+ space_type_info['floor_m_sq'] = floor_area_si
756
+ space_type_info['percent_area'] = (floor_area_si / @conditioned_floor_area_m_sq * 100.0).round(2)
757
+ space_type_info['occ_per_m_sq'] = !spaceType.peoplePerFloorArea.empty? ? spaceType.peoplePerFloorArea.get : nil
758
+ space_type_info['occ_schedule'] = !spaceType.defaultScheduleSet.empty? && !spaceType.defaultScheduleSet.get.numberofPeopleSchedule.empty? ? spaceType.defaultScheduleSet.get.numberofPeopleSchedule.get.name.get : nil
759
+ space_type_info['lighting_w_per_m_sq'] = !spaceType.lightingPowerPerFloorArea.empty? ? spaceType.lightingPowerPerFloorArea.get : nil
760
+ space_type_info['electric_w_per_m_sq'] = !spaceType.electricEquipmentPowerPerFloorArea.empty? ? spaceType.electricEquipmentPowerPerFloorArea.get : nil
761
+ space_type_info['gas_w_per_m_sq'] = !spaceType.gasEquipmentPowerPerFloorArea.empty? ? spaceType.gasEquipmentPowerPerFloorArea.get : nil
762
+ table << space_type_info
763
+ end
764
+ return table
765
+ end
766
+
767
+ def thermal_zones_table(model, cost_result)
768
+ # Get E+ zone table.
769
+ zones = @model.getThermalZones
770
+ # get surface table from sql
771
+ table = get_sql_table_to_json(@model, 'InputVerificationandResultsSummary', 'Entire Facility', 'Zone Summary')
772
+ # Get rid of totals and averages.
773
+ table['table'].delete_if { |row| !!(row['name'] =~ /Total|Average/) }
774
+ raise('Could not get zone table from E+ sql') if table.empty?
775
+
776
+ # Go through zone objects
777
+ zones.each do |zone|
778
+ # get E+ zone row
779
+ row = table['table'].detect { |curr_row| zone.name.get.downcase == curr_row['name'].downcase }
780
+ raise("Could not find zone #{row['name']} in #{zones.map { |curr_zone| curr_zone.name.get }}") if row.nil?
781
+
782
+ row['is_ideal_air_loads'] = zone.useIdealAirLoads
783
+ row['heating_sizing_factor'] = zone.sizingZone.zoneHeatingSizingFactor.empty? ? -1.0 : zone.sizingZone.zoneHeatingSizingFactor.get
784
+ row['cooling_sizing_factor'] = zone.sizingZone.zoneCoolingSizingFactor.empty? ? -1.0 : zone.sizingZone.zoneCoolingSizingFactor.get
785
+ row['zone_heating_design_supply_air_temperature'] = zone.sizingZone.zoneHeatingDesignSupplyAirTemperature
786
+ row['zone_cooling_design_supply_air_temperature'] = zone.sizingZone.zoneCoolingDesignSupplyAirTemperature
787
+ # Get Air loop that it is connected to if possible
788
+ row['air_loop_name'] = nil
789
+ model.getAirLoopHVACs.sort.each do |air_loop|
790
+ row['air_loop_name'] = air_loop.name.get if air_loop.thermalZones.include?(zone)
791
+ end
792
+ # Get Breathing zone outdoor air flow.
793
+ sql_command = "
794
+ SELECT Value FROM TabularDataWithStrings
795
+ WHERE ReportName='Standard62.1Summary'
796
+ AND ReportForString='Entire Facility'
797
+ AND TableName='Zone Ventilation Parameters'
798
+ AND ColumnName='Breathing Zone Outdoor Airflow - Vbz'
799
+ AND Units='m3/s'
800
+ AND RowName='#{row['name']}'
801
+ "
802
+ breathing_zone_outdoor_airflow_vbz = model.sqlFile.get.execAndReturnFirstDouble(sql_command)
803
+ row['breathing_zone_outdoor_airflow_vbz'] = breathing_zone_outdoor_airflow_vbz.empty? ? nil : breathing_zone_outdoor_airflow_vbz.get
804
+
805
+ if zone.useIdealAirLoads == false
806
+ # Including ventilation tz_distribution cost data into zone_table
807
+ zoneName = row['name'].downcase
808
+ storyHash = cost_result['ventilation']['tz_distribution'.to_sym][0].detect { |currstoryHash| zoneName.include?(currstoryHash[:Story].to_s.downcase) }
809
+ if storyHash
810
+ tzHash = storyHash[:thermal_zones].detect { |currtzHash| zoneName == currtzHash[:ThermalZone].to_s.downcase && tzHash[:ducting_direction].to_s.downcase == 'supply' }
811
+ if tzHash
812
+ row['ducting_direction'] = tzHash[:ducting_direction]
813
+ row['tz_mult'] = tzHash[:tz_mult]
814
+ row['airflow_m3ps'] = tzHash[:airflow_m3ps]
815
+ row['num_diff'] = tzHash[:num_diff]
816
+ row['ducting_lbs'] = tzHash[:ducting_lbs]
817
+ row['duct_insulation_ft2'] = tzHash[:duct_insulation_ft2]
818
+ row['flex_duct_sz_in'] = tzHash[:flex_duct_sz_in]
819
+ row['flex_duct_length_ft'] = tzHash[:flex_duct_length_ft]
820
+ row['duct_cost'] = tzHash[:cost]
821
+ # Check if there is a return duct hash and, if so, modify ducting direction & cost to include
822
+ # Return duct. Note that all other return duct costing values are identical to Supply duct
823
+ tzHash1 = storyHash[:thermal_zones].detect { |currtzHash1| zoneName == currtzHash1[:ThermalZone].to_s.downcase && currtzHash1[:ducting_direction].to_s.downcase == 'return' }
824
+ if !tzHash1.nil?
825
+ row['ducting_direction'] = 'Supply & Return'
826
+ row['duct_cost'] += tzHash1[:cost]
827
+ end
828
+ end
829
+ end
830
+
831
+ # Including thermal zone HRV return ducting distribution cost information
832
+ floorHash = cost_result['ventilation']['hrv_return_ducting'.to_sym].detect { |currfloorHash| zoneName.include?(currfloorHash[:floor].to_s.downcase) }
833
+ if floorHash
834
+ airSysArr = floorHash[:air_systems].select { |airSys| airSys[:air_system].to_s.downcase == row['air_loop_name'].downcase }
835
+ if !airSysArr.empty?
836
+ airSysArr.each do |airSysHash|
837
+ airSys_tz_hash = airSysHash[:tz_dist].detect { |curr_airSys_tz_hash| curr_airSys_tz_hash[:tz].to_s.downcase == zoneName }
838
+ if airSys_tz_hash
839
+ row['hrv_ret_dist_m'] = airSys_tz_hash[:hrv_ret_dist_m]
840
+ row['hrv_ret_size_in'] = airSys_tz_hash[:hrv_ret_size_in]
841
+ row['hrv_ret_duct_cost'] = airSys_tz_hash[:cost]
842
+ end
843
+ end
844
+ end
845
+ end
846
+ end
847
+ end
848
+
849
+ return table
850
+ end
851
+
852
+ def thermal_zones_equipment_table(model)
853
+ # Store Thermal zone data
854
+ table = []
855
+ model.getThermalZones.sort.each do |zone|
856
+ zone.equipmentInHeatingOrder.each do |equipment|
857
+ item = {}
858
+ item['air_loop_name'] = nil
859
+ model.getAirLoopHVACs.sort.each do |air_loop|
860
+ if air_loop.thermalZones.include?(zone)
861
+ item['air_loop_name'] = air_loop.name.empty? ? 'None' : air_loop.name.get
862
+ else
863
+ item['air_loop_name'] = 'None'
864
+ end
865
+ end
866
+ item['thermal_zone_name'] = zone.name.empty? ? 'None' : zone.name.get
867
+ item['zone_equipment_name'] = equipment.name.empty? ? 'None' : equipment.name.get
868
+ item['type'] = get_actual_child_object(equipment).class.name
869
+ table << item
870
+ end
871
+ end
872
+
873
+ table.sort_by! { |item| [item['air_loop_name'], item['thermal_zone_name'], item['zone_equipment_name']] }
874
+ return table
875
+ end
876
+
877
+ def air_loops_table(model, cost_result)
878
+ # Store Air Loop Information
879
+ table = []
880
+ model.getAirLoopHVACs.sort.each do |air_loop|
881
+ air_loop_info = {}
882
+ air_loop_info['name'] = air_loop.name.get
883
+ sql_command = " SELECT Value FROM TabularDataWithStrings
884
+ WHERE ReportName='Standard62.1Summary'
885
+ AND ReportForString='Entire Facility'
886
+ AND TableName='System Ventilation Parameters'
887
+ AND ColumnName='Area Outdoor Air Rate - Ra'
888
+ AND Units='m3/s-m2'
889
+ AND RowName='#{air_loop.name.get.to_s.upcase}' "
890
+ air_loop_info['area_outdoor_air_rate_m_cu_per_s_m_sq'] = self.class.validate_optional(model.sqlFile.get.execAndReturnFirstDouble(sql_command), model, -1.0)
891
+
892
+ air_loop_info['total_floor_area_served'] = 0.0
893
+ air_loop_info['total_breathing_zone_outdoor_airflow_vbz'] = 0.0
894
+ air_loop.thermalZones.sort.each do |zone|
895
+ sql_command = " SELECT Value FROM TabularDataWithStrings
896
+ WHERE ReportName='Standard62.1Summary'
897
+ AND ReportForString='Entire Facility'
898
+ AND TableName='Zone Ventilation Parameters'
899
+ AND ColumnName='Breathing Zone Outdoor Airflow - Vbz'
900
+ AND Units='m3/s'
901
+ AND RowName='#{zone.name.get.to_s.upcase}' "
902
+ vbz = self.class.validate_optional(model.sqlFile.get.execAndReturnFirstDouble(sql_command), model, 0)
903
+ air_loop_info['total_breathing_zone_outdoor_airflow_vbz'] += vbz
904
+ air_loop_info['total_floor_area_served'] += zone.floorArea
905
+ end
906
+ air_loop_info['outdoor_air_l_per_s'] = -1.0
907
+ unless air_loop_info['area_outdoor_air_rate_m_cu_per_s_m_sq'] == -1.0
908
+ air_loop_info['outdoor_air_l_per_s'] = air_loop_info['area_outdoor_air_rate_m_cu_per_s_m_sq'] * air_loop_info['total_floor_area_served'] * 1000
909
+ end
910
+
911
+ # SUpply Fan
912
+ unless air_loop.supplyFan.empty?
913
+ if air_loop.supplyFan.get.to_FanConstantVolume.is_initialized
914
+ air_loop_info['supply_fan_type'] = 'CV'
915
+ fan = air_loop.supplyFan.get.to_FanConstantVolume.get
916
+ elsif air_loop.supplyFan.get.to_FanVariableVolume.is_initialized
917
+ air_loop_info['supply_fan_type'] = 'VV'
918
+ fan = air_loop.supplyFan.get.to_FanVariableVolume.get
919
+ end
920
+ air_loop_info['supply_fan_name'] = fan.name.get
921
+ air_loop_info['supply_fan_efficiency'] = fan.fanEfficiency
922
+ air_loop_info['supply_fan_motor_efficiency'] = fan.motorEfficiency
923
+ air_loop_info['supply_fan_pressure_rise'] = fan.pressureRise
924
+ air_loop_info['supply_fan_max_air_flow_rate_m_cu_per_s'] = -1.0
925
+ sql_command = " SELECT RowName FROM TabularDataWithStrings
926
+ WHERE ReportName='EquipmentSummary'
927
+ AND ReportForString='Entire Facility'
928
+ AND TableName='Fans'
929
+ AND ColumnName='Max Air Flow Rate'
930
+ AND Units='m3/s' "
931
+ max_air_flow_info = model.sqlFile.get.execAndReturnVectorOfString(sql_command)
932
+ max_air_flow_info = self.class.validate_optional(max_air_flow_info, model, 'N/A')
933
+ if max_air_flow_info != 'N/A'
934
+ if max_air_flow_info.include?(air_loop_info['supply_fan_name'].to_s.upcase)
935
+ sql_command = " SELECT Value FROM TabularDataWithStrings
936
+ WHERE ReportName='EquipmentSummary'
937
+ AND ReportForString='Entire Facility'
938
+ AND TableName='Fans'
939
+ AND ColumnName='Max Air Flow Rate'
940
+ AND Units='m3/s'
941
+ AND RowName='#{air_loop_info['supply_fan_name'].upcase}' "
942
+ air_loop_info['supply_fan_max_air_flow_rate_m_cu_per_s'] = model.sqlFile.get.execAndReturnFirstDouble(sql_command).get
943
+ sql_coommand = " SELECT Value FROM TabularDataWithStrings
944
+ WHERE ReportName='EquipmentSummary'
945
+ AND ReportForString='Entire Facility'
946
+ AND TableName='Fans'
947
+ AND ColumnName='Rated Electric Power'
948
+ AND Units='W'
949
+ AND RowName='#{air_loop_info['supply_fan_name'].upcase}' "
950
+ air_loop_info['supply_fan_rated_electric_power_w'] = model.sqlFile.get.execAndReturnFirstDouble(sql_coommand).get
951
+ else
952
+ @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'"
953
+ end
954
+ else
955
+ @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' "
956
+ end
957
+ end
958
+
959
+ # Fan
960
+ unless air_loop.returnFan.empty?
961
+ air_loop_info['return_fan'] = {}
962
+ if air_loop.returnFan.get.to_FanConstantVolume.is_initialized
963
+ air_loop_info['return_fan_type'] = 'CV'
964
+ fan = air_loop.returnFan.get.to_FanConstantVolume.get
965
+ elsif air_loop.returnFan.get.to_FanVariableVolume.is_initialized
966
+ air_loop_info['return_fan_type'] = 'VV'
967
+ fan = air_loop.returnFan.get.to_FanVariableVolume.get
968
+ end
969
+ air_loop_info['return_fan_name'] = fan.name.get
970
+ air_loop_info['return_fan_efficiency'] = fan.fanEfficiency
971
+ air_loop_info['return_fan_motor_efficiency'] = fan.motorEfficiency
972
+ air_loop_info['return_fan_pressure_rise'] = fan.pressureRise
973
+ air_loop_info['return_fan_max_air_flow_rate_m_cu_per_s'] = -1.0
974
+ sql_command = " SELECT RowName FROM TabularDataWithStrings
975
+ WHERE ReportName='EquipmentSummary'
976
+ AND ReportForString='Entire Facility'
977
+ AND TableName='Fans'
978
+ AND ColumnName='Max Air Flow Rate'
979
+ AND Units='m3/s' "
980
+ max_air_flow_info = model.sqlFile.get.execAndReturnVectorOfString(sql_command)
981
+ max_air_flow_info = self.class.validate_optional(max_air_flow_info, model, 'N/A')
982
+ if max_air_flow_info != 'N/A'
983
+ if max_air_flow_info.include?(air_loop_info['return_fan_name'].to_s.upcase.to_s)
984
+ sql_command = " SELECT Value FROM TabularDataWithStrings
985
+ WHERE ReportName='EquipmentSummary'
986
+ AND ReportForString='Entire Facility'
987
+ AND TableName='Fans'
988
+ AND ColumnName='Max Air Flow Rate'
989
+ AND Units='m3/s'
990
+ AND RowName='#{air_loop_info['return_fan_name'].upcase}' "
991
+ air_loop_info['return_fan_max_air_flow_rate_m_cu_per_s'] = model.sqlFile.get.execAndReturnFirstDouble(sql_command).get
992
+ sql_coommand = " SELECT Value FROM TabularDataWithStrings
993
+ WHERE ReportName='EquipmentSummary'
994
+ AND ReportForString='Entire Facility'
995
+ AND TableName='Fans'
996
+ AND ColumnName='Rated Electric Power'
997
+ AND Units='W'
998
+ AND RowName='#{air_loop_info['return_fan_name'].upcase}' "
999
+ air_loop_info['return_fan_rated_electric_power_w'] = model.sqlFile.get.execAndReturnFirstDouble(sql_coommand).get
1000
+ else
1001
+ @error_warning << "#{air_loop_info['return_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'"
1002
+ end
1003
+ else
1004
+ @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' "
1005
+ end
1006
+ end
1007
+
1008
+ # economizer
1009
+ air_loop_info['economizer_name'] = air_loop.airLoopHVACOutdoorAirSystem.get.getControllerOutdoorAir.name.get
1010
+ air_loop_info['economizer_control_type'] = air_loop.airLoopHVACOutdoorAirSystem.get.getControllerOutdoorAir.getEconomizerControlType
1011
+
1012
+ # Include air system costs from ventilation costs
1013
+ sysNum = air_loop_info['name'][4].to_i
1014
+ sysCostInfo = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym].detect { |currsysCostInfo| currsysCostInfo[:name].to_s.downcase == air_loop_info['name'].downcase }
1015
+ if sysCostInfo.nil? && sysNum == 4
1016
+ # 04-Nov-2019 JTB: CK says that system_4 is handled the same way as system_1 so may see "Sys_4" substrings in the name of a system_1 airloop!
1017
+ sysCostInfo = cost_result['ventilation']['system_1'.to_s.to_sym].detect { |currsysCostInfo| currsysCostInfo[:name].to_s.downcase == air_loop_info['name'].downcase }
1018
+ raise("System name \"#{air_loop_info['name']}\" not found in ventilation cost info for system_1") if sysCostInfo.nil?
1019
+ else
1020
+ raise("System name \"#{air_loop_info['name']}\" not found in ventilation cost info for System_#{sysNum}") if sysCostInfo.nil?
1021
+ end
1022
+ if !sysCostInfo.nil?
1023
+ air_loop_info['airloop_flow_m3_per_s'] = sysCostInfo[:airloop_flow_m3_per_s]
1024
+ air_loop_info['num_rooftop_units'] = sysCostInfo[:num_rooftop_units]
1025
+ # air_loop_info['ahu_counter'] = sysCostInfo[:ahu_counter]
1026
+ # air_loop_info['ahu_l_per_s'] = sysCostInfo[:ahu_l_per_s]
1027
+ air_loop_info['base_ahu_cost'] = sysCostInfo[:base_ahu_cost]
1028
+ air_loop_info['revised_base_ahu_cost'] = sysCostInfo[:revised_base_ahu_cost]
1029
+ # Promote hrv data from it's own hash, if it isn't empty!
1030
+ if sysCostInfo[:hrv].empty?
1031
+ air_loop_info['hrv_type'] = sysCostInfo[:hrv]
1032
+ else
1033
+ air_loop_info['hrv_type'] = sysCostInfo[:hrv][:hrv_type]
1034
+ air_loop_info['hrv_name'] = sysCostInfo[:hrv][:hrv_name]
1035
+ air_loop_info['hrv_size_m3ps'] = sysCostInfo[:hrv][:hrv_size_m3ps]
1036
+ air_loop_info['hrv_return_fan_size_m3ps'] = sysCostInfo[:hrv][:hrv_return_fan_size_m3ps]
1037
+ air_loop_info['hrv_cost'] = sysCostInfo[:hrv][:hrv_cost]
1038
+ air_loop_info['revised_hrv_cost'] = sysCostInfo[:hrv][:revised_hrv_cost]
1039
+ end
1040
+ end
1041
+
1042
+ # Also include hrv_return_ducting information from ventilation costs.
1043
+ # Note that there can be multiple hrv return duct runs for the same air loop name
1044
+ # (on different floors) plus a system level trunk return duct section.
1045
+ hrv_retduct_byflr = cost_result['ventilation']['hrv_return_ducting'.to_sym].reject { |arr| arr[:floor].nil? }
1046
+ hrv_retduct_bysys = cost_result['ventilation']['hrv_return_ducting'.to_sym].reject { |arr| arr[:air_system].nil? }
1047
+
1048
+ # Floor level hrv return ducts...
1049
+ hrv_retduct_byflr.each do |arr1|
1050
+ flrNum = arr1[:floor].to_s[15].to_i
1051
+ airsys_byflr = arr1[:air_systems].select { |arr2| arr2[:air_system].to_s.downcase == air_loop_info['name'].to_s.downcase }
1052
+ airsys_byflr.each do |arr3|
1053
+ air_loop_info["flr#{flrNum}_floor_mult".to_sym] = arr3[:floor_mult]
1054
+ if !arr3[:hrv_ret_trunk].empty?
1055
+ # The hrv_ret_trunk embedded hash is not empty -- promote it
1056
+ air_loop_info["flr#{flrNum}_hrv_ret_trunk_len_m".to_sym] = arr3[:hrv_ret_trunk][:duct_length_m]
1057
+ air_loop_info["flr#{flrNum}_hrv_ret_trunk_dia_in".to_sym] = arr3[:hrv_ret_trunk][:dia_in]
1058
+ air_loop_info["flr#{flrNum}_hrv_ret_trunk_cost".to_sym] = arr3[:hrv_ret_trunk][:cost]
1059
+ # If not don't include anything!
1060
+ end
1061
+ # The individual thermal zone (by floor) ret duct distribution (tz_dist) added to zone_table
1062
+ end
1063
+ end
1064
+ # System level trunk hrv return ducts...
1065
+ hrv_retduct_bysys.each do |arr1|
1066
+ if arr1[:air_system].to_s.downcase == air_loop_info['name'].to_s.downcase
1067
+ air_loop_info[:hrv_building_trunk_length_m] = arr1[:hrv_building_trunk_length_m]
1068
+ air_loop_info[:hrv_building_trunk_dia_in] = arr1[:hrv_building_trunk_dia_in]
1069
+ air_loop_info["sys#{sysNum}_hrv_ret_duct_cost".to_sym] = arr1[:cost]
1070
+ end
1071
+ end
1072
+
1073
+ table << air_loop_info
1074
+ end
1075
+
1076
+ return table
1077
+ end
1078
+
1079
+ def coil_cost_table(cost_result)
1080
+ sys_coils_table = []
1081
+ (1..7).each do |sysNum|
1082
+ sys_table = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym].reject(&:empty?)
1083
+ sys_table.each do |sysHash|
1084
+ equip_info_table = sysHash[:equipment_info].reject(&:empty?)
1085
+ equip_info_table.each do |equipHash|
1086
+ sysCoilsInfo = {}
1087
+ sys_coils_table << sysCoilsInfo
1088
+ sysCoilsInfo[:sys_type] = sysNum
1089
+ sysCoilsInfo[:sys_name] = equipHash[:name]
1090
+ sysCoilsInfo[:eq_category] = equipHash[:eq_category]
1091
+ sysCoilsInfo[:heating_fuel] = equipHash[:heating_fuel]
1092
+ sysCoilsInfo[:cooling_type] = equipHash[:cooling_type]
1093
+ sysCoilsInfo[:capacity_kw] = equipHash[:capacity_kw]
1094
+ sysCoilsInfo[:coil_cost] = equipHash[:cost]
1095
+ end
1096
+ end
1097
+ end
1098
+ return sys_coils_table
1099
+ end
1100
+
1101
+ def terminal_VAV_cost_table(cost_result)
1102
+ terminal_VAV_table = []
1103
+ (1..7).each do |sysNum|
1104
+ ndx = 0
1105
+ until cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx].nil?
1106
+ ndx1 = 0
1107
+ until cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1].nil?
1108
+ sysTerminalInfo = {}
1109
+ terminal_VAV_table << sysTerminalInfo
1110
+ sysTerminalInfo[:sys_type] = sysNum
1111
+ sysTerminalInfo[:sys_name] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:name]
1112
+ sysTerminalInfo[:terminal] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:terminal]
1113
+ sysTerminalInfo[:zone_mult] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:zone_mult]
1114
+ sysTerminalInfo[:box_type] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:box_type]
1115
+ sysTerminalInfo[:box_name] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:box_name]
1116
+ sysTerminalInfo[:unit_size_kw] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:unit_info][:size_kw]
1117
+ sysTerminalInfo[:unit_air_flow_m3_per_s] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:unit_info][:air_flow_m3_per_s]
1118
+ sysTerminalInfo[:unit_pipe_dist_m] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:unit_info][:pipe_dist_m]
1119
+ sysTerminalInfo[:unit_elect_dist_m] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:unit_info][:elect_dist_m]
1120
+ sysTerminalInfo[:unit_num_units] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:unit_info][:num_units]
1121
+ sysTerminalInfo[:cost] = cost_result['ventilation']["system_#{sysNum}".to_s.to_sym][ndx][:reheat_recool][ndx1][:cost]
1122
+ ndx1 += 1
1123
+ end
1124
+ ndx += 1
1125
+ end
1126
+ end
1127
+ return terminal_VAV_table
1128
+ end
1129
+
1130
+ def trunk_ducts_cost_table(cost_result)
1131
+ # Create a trunk ducts cost table that combines the building trunk duct with the floor trunk ducts
1132
+ trunk_ducts_table = []
1133
+ ndx = 0
1134
+ insert_bld_trunk_duct = true
1135
+ until cost_result['ventilation']['floor_trunk_ducts'.to_sym][0][ndx].nil?
1136
+ if insert_bld_trunk_duct
1137
+ trunkDuctsInfo = {}
1138
+ trunk_ducts_table << trunkDuctsInfo
1139
+ trunkDuctsInfo[:Floor] = 'building_trunk'
1140
+ trunkDuctsInfo[:Predominant_space_type] = 'n/a'
1141
+ trunkDuctsInfo[:SupplyDuctSize_in] = cost_result['ventilation']['trunk_duct'.to_sym][ndx][:DuctSize_in]
1142
+ trunkDuctsInfo[:SupplyDuctLength_m] = cost_result['ventilation']['trunk_duct'.to_sym][ndx][:DuctLength_m]
1143
+ if cost_result['ventilation']['trunk_duct'.to_sym][ndx][:NumberRuns] == 2
1144
+ trunkDuctsInfo[:ReturnDuctSize_in] = cost_result['ventilation']['trunk_duct'.to_sym][ndx][:DuctSize_in]
1145
+ trunkDuctsInfo[:ReturnDuctLength_m] = cost_result['ventilation']['trunk_duct'.to_sym][ndx][:DuctLength_m]
1146
+ else
1147
+ trunkDuctsInfo[:ReturnDuctSize_in] = 0
1148
+ trunkDuctsInfo[:ReturnDuctLength_m] = 0
1149
+ end
1150
+ trunkDuctsInfo[:TotalDuctCost] = cost_result['ventilation']['trunk_duct'.to_sym][ndx][:DuctCost]
1151
+ trunkDuctsInfo[:Multiplier] = 1.0
1152
+ insert_bld_trunk_duct = false
1153
+ else
1154
+ trunkDuctsInfo = {}
1155
+ trunk_ducts_table << trunkDuctsInfo
1156
+ cost_result['ventilation']['floor_trunk_ducts'.to_sym][0][ndx].each do |k, v|
1157
+ trunkDuctsInfo[k] = v
1158
+ end
1159
+ ndx += 1
1160
+ end
1161
+ end
1162
+ return trunk_ducts_table
1163
+ end
1164
+
1165
+ def eplusout_err_table(model)
1166
+ table = []
1167
+ warnings = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='0' ")
1168
+ warnings = self.class.validate_optional(warnings, model, 'N/A')
1169
+ unless warnings == 'N/A'
1170
+ messages = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='0' ").get
1171
+ messages.each do |message|
1172
+ table << { 'error_type' => 'warning', 'message' => message }
1173
+ end
1174
+ messages = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='1' ").get
1175
+ messages.each do |message|
1176
+ table << { 'error_type' => 'severe', 'message' => message }
1177
+ end
1178
+ messages = model.sqlFile.get.execAndReturnVectorOfString("SELECT ErrorMessage FROM Errors WHERE ErrorType='2' ").get
1179
+ messages.each do |message|
1180
+ table << { 'error_type' => 'fatal', 'message' => message }
1181
+ end
1182
+ end
1183
+ return table
1184
+ end
1185
+
1186
+ def energy_peak_data
1187
+ # Primary heaing source
1188
+ data = {}
1189
+ command = "SELECT Value
1190
+ FROM TabularDataWithStrings
1191
+ WHERE ReportName='LEEDsummary'
1192
+ AND ReportForString='Entire Facility'
1193
+ AND TableName='Sec1.1A-General Information'
1194
+ AND RowName = 'Principal Heating Source'
1195
+ AND ColumnName='Data'"
1196
+ value = @sqlite_file.get.execAndReturnFirstString(command)
1197
+ # make sure all the data are availalbe
1198
+
1199
+ data['energy_principal_heating_source'] = 'unknown'
1200
+ unless value.empty?
1201
+ data['energy_principal_heating_source'] = value.get
1202
+ end
1203
+
1204
+ # Peaks
1205
+ electric_peak = @model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters'" \
1206
+ " AND ReportForString='Entire Facility' AND TableName='Annual and Peak Values - Electricity' AND RowName='Electricity:Facility'" \
1207
+ " AND ColumnName='Electricity Maximum Value' AND Units='W'")
1208
+ natural_gas_peak = @model.sqlFile.get.execAndReturnFirstDouble("SELECT Value FROM tabulardatawithstrings WHERE ReportName='EnergyMeters'" \
1209
+ " AND ReportForString='Entire Facility' AND TableName='Annual and Peak Values - Natural Gas' AND RowName='NaturalGas:Facility'" \
1210
+ " AND ColumnName='Natural Gas Maximum Value' AND Units='W'")
1211
+ data['energy_peak_electric_w_per_m_sq'] = electric_peak.empty? ? 0.0 : electric_peak.get / @conditioned_floor_area_m_sq
1212
+ data['energy_peak_natural_gas_w_per_m_sq'] = natural_gas_peak.empty? ? 0.0 : natural_gas_peak.get / @conditioned_floor_area_m_sq
1213
+
1214
+ # Peak heating load # @todo IMPORTANT NOTE: Peak heating load must be updated if a combination of fuel types is used in a building model.
1215
+ command = "SELECT Value
1216
+ FROM TabularDataWithStrings
1217
+ WHERE ReportName='EnergyMeters'
1218
+ AND ReportForString='Entire Facility'
1219
+ AND TableName='Annual and Peak Values - Electricity'
1220
+ AND RowName='Heating:Electricity'
1221
+ AND ColumnName='Electricity Maximum Value'
1222
+ AND Units='W'"
1223
+ heating_peak_w_electricity = @sqlite_file.get.execAndReturnFirstDouble(command)
1224
+ command = "SELECT Value
1225
+ FROM TabularDataWithStrings
1226
+ WHERE ReportName='EnergyMeters'
1227
+ AND ReportForString='Entire Facility'
1228
+ AND TableName='Annual and Peak Values - Natural Gas'
1229
+ AND RowName='Heating:NaturalGas'
1230
+ AND ColumnName='Natural Gas Maximum Value'
1231
+ AND Units='W'"
1232
+ heating_peak_w_gas = @sqlite_file.get.execAndReturnFirstDouble(command)
1233
+ heating_peak_w = [heating_peak_w_electricity.to_f, heating_peak_w_gas.to_f].max
1234
+ data['heating_peak_w_per_m_sq'] = heating_peak_w / @conditioned_floor_area_m_sq
1235
+
1236
+ # Peak cooling load # @todo IMPORTANT NOTE: Peak cooling load must be updated if a combination of fuel types is used in a building model.
1237
+ command = "SELECT Value
1238
+ FROM TabularDataWithStrings
1239
+ WHERE ReportName='EnergyMeters'
1240
+ AND ReportForString='Entire Facility'
1241
+ AND TableName='Annual and Peak Values - Electricity'
1242
+ AND RowName='Cooling:Electricity'
1243
+ AND ColumnName='Electricity Maximum Value'
1244
+ AND Units='W'"
1245
+ cooling_peak_w_electricity = @sqlite_file.get.execAndReturnFirstDouble(command)
1246
+ command = "SELECT Value
1247
+ FROM TabularDataWithStrings
1248
+ WHERE ReportName='EnergyMeters'
1249
+ AND ReportForString='Entire Facility'
1250
+ AND TableName='Annual and Peak Values - Natural Gas'
1251
+ AND RowName='Cooling:Electricity'
1252
+ AND ColumnName='Electricity Maximum Value'
1253
+ AND Units='W'"
1254
+ cooling_peak_w_gas = @sqlite_file.get.execAndReturnFirstDouble(command)
1255
+ cooling_peak_w = [cooling_peak_w_electricity.to_f, cooling_peak_w_gas.to_f].max
1256
+ data['cooling_peak_w_per_m_sq'] = cooling_peak_w / @conditioned_floor_area_m_sq
1257
+
1258
+ # Peak water system load # @todo IMPORTANT NOTE: Peak water system load must be updated if a combination of fuel types is used in a building model.
1259
+ command = "SELECT Value
1260
+ FROM TabularDataWithStrings
1261
+ WHERE ReportName='EnergyMeters'
1262
+ AND ReportForString='Entire Facility'
1263
+ AND TableName='Annual and Peak Values - Electricity'
1264
+ AND RowName='WaterSystems:Electricity'
1265
+ AND ColumnName='Electricity Maximum Value'
1266
+ AND Units='W'"
1267
+ water_heating_peak_w_electricity = @sqlite_file.get.execAndReturnFirstDouble(command)
1268
+ command = "SELECT Value
1269
+ FROM TabularDataWithStrings
1270
+ WHERE ReportName='EnergyMeters'
1271
+ AND ReportForString='Entire Facility'
1272
+ AND TableName='Annual and Peak Values - Natural Gas'
1273
+ AND RowName='WaterSystems:NaturalGas'
1274
+ AND ColumnName='Natural Gas Maximum Value'
1275
+ AND Units='W'"
1276
+ water_heating_peak_w_gas = @sqlite_file.get.execAndReturnFirstDouble(command)
1277
+ water_heating_peak_w = [water_heating_peak_w_electricity.to_f, water_heating_peak_w_gas.to_f].max
1278
+ data['energy_peak_water_systems_w_per_m_sq'] = water_heating_peak_w / @conditioned_floor_area_m_sq
1279
+
1280
+ #===================================================================================================================
1281
+ # Winter peak values
1282
+ ### Get number of timesteps from the model
1283
+ number_of_timesteps_per_hour = 1
1284
+ timestep_second = 3600 / number_of_timesteps_per_hour
1285
+ #===================================================================================================================
1286
+ ### Calculate number of timesteps of the whole year, and create dates/times for the whole year
1287
+ number_of_timesteps_of_year = 365 * 24 * number_of_timesteps_per_hour
1288
+ timesteps_of_year = []
1289
+ d = Time.new(2006, 1, 1, 0)
1290
+ (0...number_of_timesteps_of_year).each do |increment|
1291
+ timesteps_of_year << (d + (60 * 60 / number_of_timesteps_per_hour) * increment).strftime('%Y-%m-%d %H:%M')
1292
+ end
1293
+ # Find month of each timestep
1294
+ keys_month = timesteps_of_year.map{ |timestep| DateTime.parse(timestep).to_date.month }
1295
+ # Peak electricity in winter
1296
+ query = "
1297
+ SELECT ReportDataDictionaryIndex
1298
+ FROM ReportDataDictionary
1299
+ WHERE Name=='Electricity:Facility' AND ReportingFrequency == 'Hourly' AND Units == 'J'
1300
+ "
1301
+ index_timestep_electricity = @model.sqlFile.get.execAndReturnVectorOfInt(query).get[0]
1302
+ number_of_timesteps_per_hour = 1
1303
+ if index_timestep_electricity.nil?
1304
+ puts "No hourly or timestep data available. Peak data not available"
1305
+ data['energy_peak_electric_w_per_m_sq_winter'] = 0
1306
+ else
1307
+ ### Get timestep output for Electricity:Facility output.
1308
+ query = "
1309
+ Select VariableValue
1310
+ FROM ReportVariableData
1311
+ WHERE
1312
+ ReportVariableDataDictionaryIndex = #{index_timestep_electricity}
1313
+ "
1314
+ timestep_values = @model.sqlFile.get.execAndReturnVectorOfDouble(query).get
1315
+ # Create a hash where its keys are months and values for eahc month are electricity energy use (J) of all timesteps for that month
1316
+ hash_months_electricity = keys_month.zip(timestep_values).group_by(&:first).map{|key, value| [key, value.map(&:last)]}.to_h
1317
+ # Find maximum of electricity energy use (J) among all timesteps for each month of the year
1318
+ monthly_peak_J = hash_months_electricity.map { |key, value| [key, value.max()] }.to_h
1319
+ ### Convert J to W of maximum electricity energy use (J)
1320
+ monthly_peak_W = monthly_peak_J.map { |key, value| [key, value / timestep_second] }.to_h
1321
+ # Get winter peak
1322
+ winter_peak_W = [monthly_peak_W[12].to_f, monthly_peak_W[1].to_f, monthly_peak_W[2].to_f].max
1323
+ ### Normalize by building floor area
1324
+ annual_peak_W_per_m_sq = winter_peak_W / @btap_data['bldg_conditioned_floor_area_m_sq'].to_f
1325
+ data['energy_peak_electric_w_per_m_sq_winter'] = annual_peak_W_per_m_sq
1326
+ end
1327
+
1328
+ # Peak electricity heating in winter
1329
+ query = "
1330
+ SELECT ReportDataDictionaryIndex
1331
+ FROM ReportDataDictionary
1332
+ WHERE Name=='Heating:Electricity' AND ReportingFrequency == 'Hourly' AND Units == 'J'
1333
+ "
1334
+ index_timestep_electricity = @model.sqlFile.get.execAndReturnVectorOfInt(query).get[0]
1335
+ number_of_timesteps_per_hour = 1
1336
+ if index_timestep_electricity.nil?
1337
+ puts "No hourly or timestep data available. Peak data not available"
1338
+ data['energy_peak_electric_heating_w_per_m_sq_winter'] = 0
1339
+ else
1340
+ ### Get timestep output for Electricity:Facility output.
1341
+ query = "
1342
+ Select VariableValue
1343
+ FROM ReportVariableData
1344
+ WHERE
1345
+ ReportVariableDataDictionaryIndex = #{index_timestep_electricity}
1346
+ "
1347
+ timestep_values = @model.sqlFile.get.execAndReturnVectorOfDouble(query).get
1348
+ # Create a hash where its keys are months and values for eahc month are electricity energy use (J) of all timesteps for that month
1349
+ hash_months_electricity = keys_month.zip(timestep_values).group_by(&:first).map{|key, value| [key, value.map(&:last)]}.to_h
1350
+ # Find maximum of electricity energy use (J) among all timesteps for each month of the year
1351
+ monthly_peak_J = hash_months_electricity.map { |key, value| [key, value.max()] }.to_h
1352
+ ### Convert J to W of maximum electricity energy use (J)
1353
+ monthly_peak_W = monthly_peak_J.map { |key, value| [key, value / timestep_second] }.to_h
1354
+ # Get winter peak
1355
+ winter_peak_W = [monthly_peak_W[12].to_f, monthly_peak_W[1].to_f, monthly_peak_W[2].to_f].max
1356
+ ### Normalize by building floor area
1357
+ annual_peak_W_per_m_sq = winter_peak_W / @btap_data['bldg_conditioned_floor_area_m_sq'].to_f
1358
+ data['energy_peak_electric_heating_w_per_m_sq_winter'] = annual_peak_W_per_m_sq
1359
+ end
1360
+
1361
+ # Peak water system electricity heating in winter
1362
+ query = "
1363
+ SELECT ReportDataDictionaryIndex
1364
+ FROM ReportDataDictionary
1365
+ WHERE Name=='WaterSystems:Electricity' AND ReportingFrequency == 'Hourly' AND Units == 'J'
1366
+ "
1367
+ index_timestep_electricity = @model.sqlFile.get.execAndReturnVectorOfInt(query).get[0]
1368
+ number_of_timesteps_per_hour = 1
1369
+ if index_timestep_electricity.nil?
1370
+ puts "No hourly or timestep data available. Peak data not available"
1371
+ data['energy_peak_electric_water_systems_w_per_m_sq_winter'] = 0
1372
+ else
1373
+ ### Get timestep output for Electricity:Facility output.
1374
+ query = "
1375
+ Select VariableValue
1376
+ FROM ReportVariableData
1377
+ WHERE
1378
+ ReportVariableDataDictionaryIndex = #{index_timestep_electricity}
1379
+ "
1380
+ timestep_values = @model.sqlFile.get.execAndReturnVectorOfDouble(query).get
1381
+ # Create a hash where its keys are months and values for eahc month are electricity energy use (J) of all timesteps for that month
1382
+ hash_months_electricity = keys_month.zip(timestep_values).group_by(&:first).map{|key, value| [key, value.map(&:last)]}.to_h
1383
+ # Find maximum of electricity energy use (J) among all timesteps for each month of the year
1384
+ monthly_peak_J = hash_months_electricity.map { |key, value| [key, value.max()] }.to_h
1385
+ ### Convert J to W of maximum electricity energy use (J)
1386
+ monthly_peak_W = monthly_peak_J.map { |key, value| [key, value / timestep_second] }.to_h
1387
+ # Get winter peak
1388
+ winter_peak_W = [monthly_peak_W[12].to_f, monthly_peak_W[1].to_f, monthly_peak_W[2].to_f].max
1389
+ ### Normalize by building floor area
1390
+ annual_peak_W_per_m_sq = winter_peak_W / @btap_data['bldg_conditioned_floor_area_m_sq'].to_f
1391
+ data['energy_peak_electric_water_systems_w_per_m_sq_winter'] = annual_peak_W_per_m_sq
1392
+ end
1393
+
1394
+ return data
1395
+ end
1396
+
1397
+ def energy_eui_data(model)
1398
+ data = {}
1399
+ # default to zero to start.
1400
+ ['energy_eui_fans_gj_per_m_sq',
1401
+ 'energy_eui_heating_gj_per_m_sq',
1402
+ 'energy_eui_cooling_gj_per_m_sq',
1403
+ 'energy_eui_interior equipment_gj_per_m_sq',
1404
+ 'energy_eui_natural_gas_gj_per_m_sq',
1405
+ 'energy_eui_pumps_gj_per_m_sq',
1406
+ 'energy_eui_total_gj_per_m_sq',
1407
+ 'energy_eui_heat recovery_gj_per_m_sq',
1408
+ 'energy_eui_water systems_gj_per_m_sq'].each { |end_use| data[end_use] = 0.0 }
1409
+
1410
+ # Check if the HVAC of the model is GSHP
1411
+ plant_loops = model.getPlantLoops
1412
+ model_has_how_many_GSHP = 0.0
1413
+ plant_loops.each do |plantloop|
1414
+ if plantloop.name.to_s.upcase.include? "GLHX"
1415
+ model_has_how_many_GSHP += 1.0
1416
+ end
1417
+ end
1418
+
1419
+ # Get E+ End use table from sql
1420
+ table = get_sql_table_to_json(@model, 'AnnualBuildingUtilityPerformanceSummary', 'Entire Facility', 'End Uses')['table']
1421
+ # Get rid of totals and averages rows.. I want just the
1422
+ table.delete_if { |row| !!(row['name'] =~ /Total|Average/) }
1423
+ table.each do |row|
1424
+ # skip name and water_m3 columns
1425
+ energy_columns = row.select { |k, v| (k != 'name') && (k != 'water_m3') }
1426
+ # Store eui by use name.
1427
+ data["energy_eui_#{row['name'].downcase}_gj_per_m_sq"] = energy_columns.inject(0) { |sum, tuple| sum += tuple[1] } / @conditioned_floor_area_m_sq
1428
+ end
1429
+
1430
+ data['energy_eui_total_gj_per_m_sq'] = 0.0
1431
+
1432
+ ['natural_gas_GJ', 'electricity_GJ', 'additional_fuel_GJ', 'district_cooling_GJ', 'district_heating_GJ'].each do |column|
1433
+ data["energy_eui_#{column.downcase}_per_m_sq"] = table.inject(0) { |sum, row| sum + (row[column].nil? ? 0.0 : row[column]) } / @conditioned_floor_area_m_sq
1434
+ data['energy_eui_total_gj_per_m_sq'] += data["energy_eui_#{column.downcase}_per_m_sq"] unless data["energy_eui_#{column.downcase}_per_m_sq"].nil?
1435
+ end
1436
+
1437
+ # If the HVAC of the model is GSHP, district heating and cooling must be removed from EUIs for heating and cooling and total EUI
1438
+ # NOTE: it has been assumed that if a model has GSHP, that is the only HVAC type in the model. This assumption means that any district heating/cooling in the model is related to GSHP.
1439
+ if model_has_how_many_GSHP > 0.0
1440
+ data['energy_eui_heating_gj_per_m_sq'] -= data['energy_eui_district_heating_gj_per_m_sq']
1441
+ data['energy_eui_cooling_gj_per_m_sq'] -= data['energy_eui_district_cooling_gj_per_m_sq']
1442
+ data['energy_eui_total_gj_per_m_sq'] -= (data['energy_eui_district_heating_gj_per_m_sq'] + data['energy_eui_district_cooling_gj_per_m_sq'])
1443
+ end
1444
+
1445
+ # Get total and net site energy use intensity
1446
+ # Note: 'Total Site Energy' is the "gross" energy used by a building.
1447
+ # Note: 'Net Site Energy' is the final energy used by the building after considering any on-site energy generation (e.g. PV).
1448
+ # Reference: https://unmethours.com/question/25416/what-is-the-difference-between-site-energy-and-source-energy/
1449
+ # Reference: https://designbuilder.co.uk/helpv6.0/Content/KPIs.htm
1450
+ data['total_site_eui_gj_per_m_sq'] = data['energy_eui_total_gj_per_m_sq'].to_f
1451
+ command = "SELECT Value
1452
+ FROM TabularDataWithStrings
1453
+ WHERE ReportName='AnnualBuildingUtilityPerformanceSummary'
1454
+ AND ReportForString='Entire Facility'
1455
+ AND TableName='Site and Source Energy'
1456
+ AND RowName='Net Site Energy'
1457
+ AND ColumnName='Energy Per Conditioned Building Area'
1458
+ AND Units='MJ/m2'"
1459
+ net_site_eui_mj_per_m_sq = @sqlite_file.get.execAndReturnFirstDouble(command)
1460
+ data['net_site_eui_gj_per_m_sq'] = OpenStudio.convert(net_site_eui_mj_per_m_sq.to_f, 'MJ', 'GJ').get
1461
+ return data
1462
+ end
1463
+
1464
+ def unmet_hours(model)
1465
+ # Store unmet hour data
1466
+ unmet_hours = {}
1467
+ unmet_hours['unmet_hours_cooling'] = model.getFacility.hoursCoolingSetpointNotMet.get unless model.getFacility.hoursCoolingSetpointNotMet.empty?
1468
+ unmet_hours['unmet_hours_heating'] = model.getFacility.hoursHeatingSetpointNotMet.get unless model.getFacility.hoursHeatingSetpointNotMet.empty?
1469
+ command = "SELECT Value
1470
+ FROM TabularDataWithStrings
1471
+ WHERE ReportName='AnnualBuildingUtilityPerformanceSummary'
1472
+ AND ReportForString='Entire Facility'
1473
+ AND TableName='Comfort and Setpoint Not Met Summary'
1474
+ AND RowName='Time Setpoint Not Met During Occupied Cooling'
1475
+ AND ColumnName='Facility'
1476
+ AND Units='Hours'"
1477
+ unmet_hours['unmet_hours_cooling_during_occupied'] = @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1478
+ command = "SELECT Value
1479
+ FROM TabularDataWithStrings
1480
+ WHERE ReportName='AnnualBuildingUtilityPerformanceSummary'
1481
+ AND ReportForString='Entire Facility'
1482
+ AND TableName='Comfort and Setpoint Not Met Summary'
1483
+ AND RowName='Time Setpoint Not Met During Occupied Heating'
1484
+ AND ColumnName='Facility'
1485
+ AND Units='Hours'"
1486
+ unmet_hours['unmet_hours_heating_during_occupied'] = @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1487
+ return unmet_hours
1488
+ end
1489
+
1490
+ def service_water_heating_data
1491
+ service_water_heating = {}
1492
+ service_water_heating['shw_total_nominal_occupancy'] = -1
1493
+ # 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, :+)
1494
+ service_water_heating['shw_total_nominal_occupancy'] = self.class.get_total_nominal_capacity(@model)
1495
+
1496
+ service_water_heating['shw_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'")
1497
+ service_water_heating['shw_electricity_per_year'] = self.class.validate_optional(service_water_heating['shw_electricity_per_year'], @model, -1)
1498
+
1499
+ service_water_heating['shw_electricity_per_day'] = service_water_heating['shw_electricity_per_year'] / 365.5
1500
+ service_water_heating['shw_electricity_per_day_per_occupant'] = service_water_heating['shw_electricity_per_day'] / service_water_heating['shw_total_nominal_occupancy']
1501
+
1502
+ service_water_heating['shw_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'")
1503
+ service_water_heating['shw_natural_gas_per_year'] = self.class.validate_optional(service_water_heating['shw_natural_gas_per_year'], @model, -1)
1504
+
1505
+ service_water_heating['shw_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'")
1506
+ service_water_heating['shw_additional_fuel_per_year'] = self.class.validate_optional(service_water_heating['shw_additional_fuel_per_year'], @model, -1)
1507
+
1508
+ service_water_heating['shw_water_m_cu_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'")
1509
+ service_water_heating['shw_water_m_cu_per_year'] = self.class.validate_optional(service_water_heating['shw_water_m_cu_per_year'], @model, -1)
1510
+
1511
+ service_water_heating['shw_water_m_cu_per_day'] = service_water_heating['shw_water_m_cu_per_year'] / 365.5
1512
+ service_water_heating['shw_water_m_cu_per_day_per_occupant'] = service_water_heating['shw_water_m_cu_per_day'] / service_water_heating['shw_total_nominal_occupancy']
1513
+ return service_water_heating
1514
+ end
1515
+
1516
+ # The below method (outdoor_air_data extract a couple of outputs related to outdoor air from the .html output file)
1517
+ def outdoor_air_data(model)
1518
+ # Store outdoor air data
1519
+ outdoor_air_data = {}
1520
+ #===============================================================================================================
1521
+ airloops_total_outdoor_air_mechanical_ventilation_m3 = 0.0
1522
+ airloops_total_outdoor_air_natural_ventilation_m3 = 0.0
1523
+ zones_total_outdoor_air_mechanical_ventilation_m3 = 0.0
1524
+ zones_total_outdoor_air_natural_ventilation_m3 = 0.0
1525
+ zones_total_outdoor_air_infiltration_m3 = 0.0
1526
+ #===============================================================================================================
1527
+ # Total outdoor air by airLoop
1528
+ model.getAirLoopHVACs.sort.each do |air_loop|
1529
+ air_loop_name = air_loop.name.get.upcase
1530
+
1531
+ # Mechanical ventilation of all airloops
1532
+ command = "SELECT Value
1533
+ FROM TabularDataWithStrings
1534
+ WHERE ReportName='OutdoorAirDetails'
1535
+ AND ReportForString='Entire Facility'
1536
+ AND TableName='Total Outdoor Air by AirLoop'
1537
+ AND RowName='#{air_loop_name}'
1538
+ AND ColumnName='Mechanical Ventilation'
1539
+ AND Units='m3'"
1540
+ airloops_total_outdoor_air_mechanical_ventilation_m3 += @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1541
+
1542
+ # Natural ventilation of all airloops
1543
+ command = "SELECT Value
1544
+ FROM TabularDataWithStrings
1545
+ WHERE ReportName='OutdoorAirDetails'
1546
+ AND ReportForString='Entire Facility'
1547
+ AND TableName='Total Outdoor Air by AirLoop'
1548
+ AND RowName='#{air_loop_name}'
1549
+ AND ColumnName='Natural Ventilation'
1550
+ AND Units='m3'"
1551
+ airloops_total_outdoor_air_natural_ventilation_m3 += @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1552
+
1553
+ end
1554
+
1555
+ # Not-normalized mechanical/natural.
1556
+ outdoor_air_data['airloops_total_outdoor_air_mechanical_ventilation_m3'] = airloops_total_outdoor_air_mechanical_ventilation_m3
1557
+ outdoor_air_data['airloops_total_outdoor_air_natural_ventilation_m3'] = airloops_total_outdoor_air_natural_ventilation_m3
1558
+
1559
+ # Normalized mechanical/natural: ACH (air changes per hour)
1560
+ outdoor_air_data['airloops_total_outdoor_air_mechanical_ventilation_ach_1_per_hr'] = airloops_total_outdoor_air_mechanical_ventilation_m3 / (@btap_data['bldg_volume_m_cu'] * 365 * 24)
1561
+ outdoor_air_data['airloops_total_outdoor_air_natural_ventilation_ach_1_per_hr'] = airloops_total_outdoor_air_natural_ventilation_m3 / (@btap_data['bldg_volume_m_cu'] * 365 * 24)
1562
+
1563
+ # Normalized mechanical/natural: normalized by conditioned floor area
1564
+ outdoor_air_data['airloops_total_outdoor_air_mechanical_ventilation_flow_per_conditioned_floor_area_m3_per_s_m2'] = airloops_total_outdoor_air_mechanical_ventilation_m3 / (@conditioned_floor_area_m_sq * 365 * 24 * 3600)
1565
+ outdoor_air_data['airloops_total_outdoor_air_natural_ventilation_flow_per_conditioned_floor_area_m3_per_s_m2'] = airloops_total_outdoor_air_natural_ventilation_m3 / (@conditioned_floor_area_m_sq * 365 * 24 * 3600)
1566
+
1567
+ # Normalized mechanical/natural: normalized by exterior area
1568
+ outdoor_air_data['airloops_total_outdoor_air_mechanical_ventilation_flow_per_exterior_area_m3_per_s_m2'] = airloops_total_outdoor_air_mechanical_ventilation_m3 / (@btap_data['bldg_exterior_area_m_sq'] * 365 * 24 * 3600)
1569
+ outdoor_air_data['airloops_total_outdoor_air_natural_ventilation_flow_per_exterior_area_m3_per_s_m2'] = airloops_total_outdoor_air_natural_ventilation_m3 / (@btap_data['bldg_exterior_area_m_sq'] * 365 * 24 * 3600)
1570
+
1571
+ #===============================================================================================================
1572
+ # Total outdoor air by zone
1573
+ total_outdoor_air_mechanical_ventilation_zones_m3 = 0.0
1574
+ model.getThermalZones.sort.each do |zone|
1575
+ zone_name = zone.name.get.upcase
1576
+
1577
+ # Mechanical ventilation of all zones
1578
+ command = "SELECT Value
1579
+ FROM TabularDataWithStrings
1580
+ WHERE ReportName='OutdoorAirDetails'
1581
+ AND ReportForString='Entire Facility'
1582
+ AND TableName='Total Outdoor Air by Zone'
1583
+ AND RowName='#{zone_name}'
1584
+ AND ColumnName='Mechanical Ventilation'
1585
+ AND Units='m3'"
1586
+ zones_total_outdoor_air_mechanical_ventilation_m3 += @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1587
+
1588
+ # Natural ventilation of all zones
1589
+ command = "SELECT Value
1590
+ FROM TabularDataWithStrings
1591
+ WHERE ReportName='OutdoorAirDetails'
1592
+ AND ReportForString='Entire Facility'
1593
+ AND TableName='Total Outdoor Air by Zone'
1594
+ AND RowName='#{zone_name}'
1595
+ AND ColumnName='Natural Ventilation'
1596
+ AND Units='m3'"
1597
+ zones_total_outdoor_air_natural_ventilation_m3 += @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1598
+
1599
+ # Infiltration of all zones
1600
+ command = "SELECT Value
1601
+ FROM TabularDataWithStrings
1602
+ WHERE ReportName='OutdoorAirDetails'
1603
+ AND ReportForString='Entire Facility'
1604
+ AND TableName='Total Outdoor Air by Zone'
1605
+ AND RowName='#{zone_name}'
1606
+ AND ColumnName='Infiltration'
1607
+ AND Units='m3'"
1608
+ zones_total_outdoor_air_infiltration_m3 += @sqlite_file.get.execAndReturnFirstDouble(command).to_f
1609
+
1610
+ end
1611
+
1612
+ # Not-normalized mechanical/natural/infiltration.
1613
+ outdoor_air_data['zones_total_outdoor_air_mechanical_ventilation_m3'] = zones_total_outdoor_air_mechanical_ventilation_m3
1614
+ outdoor_air_data['zones_total_outdoor_air_natural_ventilation_m3'] = zones_total_outdoor_air_natural_ventilation_m3
1615
+ outdoor_air_data['zones_total_outdoor_air_infiltration_m3'] = zones_total_outdoor_air_infiltration_m3
1616
+
1617
+ # Normalized mechanical/natural/infiltration: ACH (air changes per hour)
1618
+ outdoor_air_data['zones_total_outdoor_air_mechanical_ventilation_ach_1_per_hr'] = zones_total_outdoor_air_mechanical_ventilation_m3 / (@btap_data['bldg_volume_m_cu'] * 365 * 24)
1619
+ outdoor_air_data['zones_total_outdoor_air_natural_ventilation_ach_1_per_hr'] = zones_total_outdoor_air_natural_ventilation_m3 / (@btap_data['bldg_volume_m_cu'] * 365 * 24)
1620
+ outdoor_air_data['zones_total_outdoor_air_infiltration_ach_1_per_hr'] = zones_total_outdoor_air_infiltration_m3 / (@btap_data['bldg_volume_m_cu'] * 365 * 24)
1621
+
1622
+ # Normalized mechanical/natural/infiltration: normalized by conditioned floor area
1623
+ outdoor_air_data['zones_total_outdoor_air_mechanical_ventilation_flow_per_conditioned_floor_area_m3_per_s_m2'] = zones_total_outdoor_air_mechanical_ventilation_m3 / (@conditioned_floor_area_m_sq * 365 * 24 * 3600)
1624
+ outdoor_air_data['zones_total_outdoor_air_natural_ventilation_flow_per_conditioned_floor_area_m3_per_s_m2'] = zones_total_outdoor_air_natural_ventilation_m3 / (@conditioned_floor_area_m_sq * 365 * 24 * 3600)
1625
+ outdoor_air_data['zones_total_outdoor_air_infiltration_flow_per_conditioned_floor_area_m3_per_s_m2'] = zones_total_outdoor_air_infiltration_m3 / (@conditioned_floor_area_m_sq * 365 * 24 * 3600)
1626
+
1627
+ # Normalized mechanical/natural/infiltration: normalized by exterior area
1628
+ outdoor_air_data['zones_total_outdoor_air_mechanical_ventilation_flow_per_exterior_area_m3_per_s_m2'] = zones_total_outdoor_air_mechanical_ventilation_m3 / (@btap_data['bldg_exterior_area_m_sq'] * 365 * 24 * 3600)
1629
+ outdoor_air_data['zones_total_outdoor_air_natural_ventilation_flow_per_exterior_area_m3_per_s_m2'] = zones_total_outdoor_air_natural_ventilation_m3 / (@btap_data['bldg_exterior_area_m_sq'] * 365 * 24 * 3600)
1630
+ outdoor_air_data['zones_total_outdoor_air_infiltration_flow_per_exterior_area_m3_per_s_m2'] = zones_total_outdoor_air_infiltration_m3 / (@btap_data['bldg_exterior_area_m_sq'] * 365 * 24 * 3600)
1631
+ #===============================================================================================================
1632
+
1633
+ return outdoor_air_data
1634
+ end
1635
+
1636
+ def sql_data_tables(model)
1637
+ puts 'Getting SQL Data into json...'
1638
+ start = Time.now
1639
+ sql_data = []
1640
+
1641
+ [
1642
+ ['AnnualBuildingUtilityPerformanceSummary', 'Entire Facility', 'End Uses']
1643
+ # ["AnnualBuildingUtilityPerformanceSummary", "Entire Facility", "Site and Source Energy"],
1644
+ # ["AnnualBuildingUtilityPerformanceSummary", "Entire Facility", "On-Site Thermal Sources"],
1645
+ # ["AnnualBuildingUtilityPerformanceSummary", "Entire Facility", "Comfort and Setpoint Not Met Summary"],
1646
+ # ["InputVerificationandResultsSummary", "Entire Facility", "Window-Wall Ratio"],
1647
+ # ["InputVerificationandResultsSummary", "Entire Facility", "Conditioned Window-Wall Ratio"],
1648
+ # ["InputVerificationandResultsSummary", "Entire Facility", "Skylight-Roof Ratio"],
1649
+ # ["DemandEndUseComponentsSummary", "Entire Facility", "End Uses"],
1650
+ # ["ComponentSizingSummary", "Entire Facility", "AirLoopHVAC"],
1651
+ # ["EnergyMeters", "Entire Facility", 'Annual and Peak Values - Natural Gas'],
1652
+ # ["EnergyMeters", "Entire Facility", 'Annual and Peak Values - Electricity'],
1653
+ # ["EnergyMeters", "Entire Facility", 'Annual and Peak Values - FuelOilNo2'],
1654
+ # ["EnergyMeters", "Entire Facility", 'Annual and Peak Values - Other'],
1655
+ # ["LEEDsummary", "Entire Facility", "EAp2-7. Energy Cost Summary"],
1656
+ # ["Standard62.1Summary", "Entire Facility", "Zone Ventilation Parameters"],
1657
+ # ["EquipmentSummary", "Entire Facility", "Fans"],
1658
+ # ["EquipmentSummary", "Entire Facility", "Heating Coils"],
1659
+ # ["EquipmentSummary", "Entire Facility", "Cooling Coils"],
1660
+ # ["EquipmentSummary", "Entire Facility", "Pumps"],
1661
+ # ["CoilSizingDetails", "Entire Facility", "Coils"] # Do not use! Takes very long to parse.
1662
+ ].each do |table|
1663
+ start = Time.now
1664
+ puts "Parsing #{table[0]}-#{table[1]}-#{table[2]}"
1665
+ sql_data << get_sql_table_to_json(model, table[0], table[1], table[2])
1666
+ finish = Time.now
1667
+ puts "....finish parsing in #{finish - start} seconds and stored in sql_data_tables hash."
1668
+ end
1669
+ end
1670
+
1671
+ # This measure will return an array of hashes with the varialbles used in the previous measures.
1672
+ def measures_data_table(runner)
1673
+ # Array to store hash row data.
1674
+ measure_variables_table = []
1675
+ # Go through each workstep.
1676
+ runner.workflow.workflowSteps.each do |step|
1677
+ # Check if the ws is a measure.
1678
+ if step.to_MeasureStep.is_initialized
1679
+ measure_step = step.to_MeasureStep.get
1680
+ # Set measure name using either the folder name or the measureStep name if possible.
1681
+ measure_name = measure_step.name.is_initialized ? measure_step.name.get : measure_step.measureDirName
1682
+ measures_to_skip = ['openstudio_results']
1683
+ unless measures_to_skip.include?(measure_name.to_s)
1684
+ # Check if the 'result' (?) is initialized?
1685
+ if measure_step.result.is_initialized
1686
+ result = measure_step.result.get
1687
+ # Iterate through the result object stepValues to obtain the arg values. I am assuming this is for arg, var. pivot, continous...
1688
+ result.stepValues.each do |arg|
1689
+ # Store the row /hash for the table.
1690
+ units = arg.units.empty? ? nil : arg.units
1691
+ value = nil
1692
+ case arg.variantType.value
1693
+ when 0
1694
+ value = arg.valueAsBoolean
1695
+ when 1..2
1696
+ value = arg.valueAsDouble
1697
+ when 3
1698
+ value = arg.valueAsString
1699
+ end
1700
+ measure_variables_table << { 'measure_name' => measure_name, 'arg_name' => arg.name, 'value' => value, 'units' => units, 'type' => arg.variantType.value }
1701
+ end
1702
+ end
1703
+ end
1704
+ end
1705
+ end
1706
+ return measure_variables_table
1707
+ end
1708
+
1709
+ # This should be done last.
1710
+
1711
+ def get_sql_table_to_json(model, report_name, report_for_string, table_name)
1712
+ table = []
1713
+ query_row_names = "
1714
+ SELECT DISTINCT
1715
+ RowName
1716
+ FROM
1717
+ tabulardatawithstrings
1718
+ WHERE
1719
+ ReportName='#{report_name}'
1720
+ AND
1721
+ ReportForString='#{report_for_string}'
1722
+ AND
1723
+ TableName='#{table_name}'"
1724
+ row_names = model.sqlFile.get.execAndReturnVectorOfString(query_row_names).get
1725
+
1726
+ # get Columns
1727
+ query_col_names = "
1728
+ SELECT DISTINCT
1729
+ ColumnName
1730
+ FROM tabulardatawithstrings
1731
+ WHERE ReportName='#{report_name}'
1732
+ AND ReportForString='#{report_for_string}'
1733
+ AND TableName='#{table_name}'"
1734
+ col_names = model.sqlFile.get.execAndReturnVectorOfString(query_col_names).get
1735
+
1736
+ # get units
1737
+ query_unit_names = "
1738
+ SELECT DISTINCT
1739
+ Units
1740
+ FROM tabulardatawithstrings
1741
+ WHERE ReportName='#{report_name}'
1742
+ AND ReportForString='#{report_for_string}'
1743
+ AND TableName='#{table_name}'"
1744
+ unit_names = model.sqlFile.get.execAndReturnVectorOfString(query_unit_names).get
1745
+
1746
+ row_names.each do |row|
1747
+ next if row.nil? || row == ''
1748
+
1749
+ row_hash = {}
1750
+ row_hash['name'] = row
1751
+ col_names.each do |col|
1752
+ unit_names.each do |unit|
1753
+ query = "
1754
+ SELECT
1755
+ Value
1756
+ FROM
1757
+ tabulardatawithstrings
1758
+ WHERE
1759
+ ReportName='#{report_name}'
1760
+ AND
1761
+ ReportForString='#{report_for_string}'
1762
+ AND
1763
+ TableName='#{table_name}'
1764
+ AND
1765
+ RowName='#{row}'
1766
+ AND
1767
+ ColumnName='#{col}'
1768
+ AND
1769
+ Units='#{unit}'
1770
+ "
1771
+ column_name = col.to_s.gsub(/\s+/, '_').downcase
1772
+ column_name += "_#{unit}" if unit != ''
1773
+ value = model.sqlFile.get.execAndReturnFirstString(query)
1774
+ next if value.empty? || value.get.nil?
1775
+
1776
+ value = value.get.strip
1777
+ # check is value is a number. The last chunk checks if the string includes an E, if not return true since it
1778
+ # is a regular number, if not it checks if it is in the E+ exponent format and returns the bool result of that.
1779
+ if (begin
1780
+ Float(value)
1781
+ rescue StandardError
1782
+ false
1783
+ end) && value.to_f != 0 && (value.include?('E') || value.include?('e') ? value =~ /\d*\.\d*E[+|-]\d*/ : true)
1784
+ row_hash[column_name] = value.to_f
1785
+ # Check if value is a date
1786
+ elsif unit == '' && value =~ /\d\d-\D\D\D-\d\d:\d\d/
1787
+ row_hash[column_name] = DateTime.parse(value)
1788
+ # skip if value in an empty string or a zero value
1789
+ elsif value != '' && value != '0.00'
1790
+ row_hash[column_name] = value
1791
+ end
1792
+ end
1793
+ end
1794
+ if row_hash.size > 1
1795
+ table << row_hash
1796
+ end
1797
+ end
1798
+ result = { 'report_name' => report_name, 'report_for_string' => report_for_string, 'table_name' => table_name, 'table' => table }
1799
+ return result
1800
+ end
1801
+
1802
+ # @todo SQL command units may have been converted wrong.
1803
+
1804
+ def get_actual_child_object(object)
1805
+ # monkey patch class to have a decendants static method to return all possible subclasses of the object.
1806
+ object.class.class_eval do
1807
+ def self.descendants
1808
+ ObjectSpace.each_object(Class).select { |k| k < self } << self
1809
+ end
1810
+ end
1811
+ # Dont try and match the class that the object is already in! So get decendants that are not of the current object class
1812
+ subclass_array = object.class.descendants.map { |classtype| classtype if classtype != object.class }.reject(&:nil?)
1813
+ subclass_array.each do |class_type|
1814
+ # convert class name to not have prefix.
1815
+ matches = class_type.name.match(/OpenStudio::Model::(?<object_name>.*)/)
1816
+ new_object = nil
1817
+ # Use eval (I know this is the devil) to try to cast to a subclass.
1818
+ eval_info = "new_object = object.to_#{matches['object_name']}"
1819
+ eval(eval_info)
1820
+ # if it does then clean it up and return it.
1821
+ if new_object.is_initialized
1822
+ new_object = new_object.get
1823
+ return get_actual_child_object(new_object)
1824
+ end
1825
+ end
1826
+ # It is not cast-able to any subclass.. so returning original object.
1827
+ return object
1828
+ end
1829
+
1830
+ def measure_metrics(qaqc)
1831
+ # Store mesure metric data that will be used in analysis tools.
1832
+ @btap_data['env_outdoor_walls_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:outdoor_walls_average_conductance_w_per_m2_k]
1833
+ @btap_data['env_outdoor_roofs_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:outdoor_roofs_average_conductance_w_per_m2_k]
1834
+ @btap_data['env_outdoor_floors_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:outdoor_floors_average_conductance_w_per_m2_k]
1835
+ @btap_data['env_ground_walls_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:ground_walls_average_conductance_w_per_m2_k]
1836
+ @btap_data['env_ground_roofs_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:ground_roofs_average_conductance_w_per_m2_k]
1837
+ @btap_data['env_ground_floors_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:ground_floors_average_conductance_w_per_m2_k]
1838
+ @btap_data['env_outdoor_windows_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:windows_average_conductance_w_per_m2_k]
1839
+ @btap_data['env_outdoor_doors_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:doors_average_conductance_w_per_m2_k]
1840
+ @btap_data['env_outdoor_overhead_doors_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:overhead_doors_average_conductance_w_per_m2_k]
1841
+ @btap_data['env_skylights_average_conductance-w_per_m_sq_k'] = qaqc[:envelope][:skylights_average_conductance_w_per_m2_k]
1842
+ @btap_data['env_fdwr'] = (BTAP::Geometry.get_fwdr(@model) * 100.0).round(1)
1843
+ @btap_data['env_srr'] = (BTAP::Geometry.get_srr(@model) * 100.0).round(1)
1844
+ unless @btap_data['measures_data_table'].nil?
1845
+ if @btap_data['measures_data_table'].detect { |item| item['measure_name'] == 'btap_standard_building_type_geometry' }.nil?
1846
+ @btap_data['env_x_scale'] = 1.0
1847
+ @btap_data['env_y_scale'] = 1.0
1848
+ @btap_data['env_z_scale'] = 1.0
1849
+ @btap_data['env_rotation'] = 0.0
1850
+ else
1851
+ @btap_data['env_x_scale'] = @btap_data['measures_data_table'].detect { |item| item['measure_name'] == 'btap_standard_building_type_geometry' && item['arg_name'] == 'x_scale' }['value']
1852
+ @btap_data['env_y_scale'] = @btap_data['measures_data_table'].detect { |item| item['measure_name'] == 'btap_standard_building_type_geometry' && item['arg_name'] == 'y_scale' }['value']
1853
+ @btap_data['env_z_scale'] = @btap_data['measures_data_table'].detect { |item| item['measure_name'] == 'btap_standard_building_type_geometry' && item['arg_name'] == 'z_scale' }['value']
1854
+ @btap_data['env_rotation'] = @btap_data['measures_data_table'].detect { |item| item['measure_name'] == 'btap_standard_building_type_geometry' && item['arg_name'] == 'relative_building_rotation' }['value']
1855
+ end
1856
+ end
1857
+
1858
+ # This does not work with the new VRF or CCASHP systems. Commenting it for now.
1859
+ # Determine dominant system type by air loop
1860
+
1861
+ # systems = {}
1862
+ # @btap_data["air_loop_table"].each do |loop|
1863
+ # Get system name part from regex
1864
+ # system_name = loop["name"].match(/(^.{0,6}).*/)[1]
1865
+ # systems[system_name] = 0.0 if systems[system_name] == nil
1866
+ # systems[system_name] += loop["total_floor_area_served"]
1867
+ # end
1868
+ # if systems.empty?
1869
+ # @btap_data["mm_hvac_dominant_system_type"] = "Unknown/IdealHVAC"
1870
+ # else
1871
+ # @btap_data["mm_hvac_dominant_system_type"] = systems.key(systems.values.max)
1872
+ # end
1873
+ return @btap_data
1874
+ end
1875
+
1876
+ def get_utility_ghg_kg_per_gj(province:, fuel_type:)
1877
+ ghg_data = [
1878
+ # Obtained from Portfolio Manager https://portfoliomanager.energystar.gov/pdf/reference/Emissions.pdf 10/10/2020
1879
+ #{ "province": 'AB', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 53.24, "CO2eq Emissions (g/m3)": 1939.0 },
1880
+ #{ "province": 'BC', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 53.19, "CO2eq Emissions (g/m3)": 1937.0 },
1881
+ #{ "province": 'MB', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.09, "CO2eq Emissions (g/m3)": 1897.0 },
1882
+ #{ "province": 'NB', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1883
+ #{ "province": 'NL', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1884
+ #{ "province": 'NT', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1885
+ #{ "province": 'NS', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1886
+ #{ "province": 'NU', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1887
+ #{ "province": 'ON', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.14, "CO2eq Emissions (g/m3)": 1912.0 },
1888
+ #{ "province": 'PE', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1889
+ #{ "province": 'QC', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.12, "CO2eq Emissions (g/m3)": 1898.0 },
1890
+ #{ "province": 'SK', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 50.53, "CO2eq Emissions (g/m3)": 1840.0 },
1891
+ #{ "province": 'YT', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.50, "CO2eq Emissions (g/m3)": 1912.0 },
1892
+
1893
+ #REF (Retrieved March 11, 2025) for Year 2022 of "Table A6.1–1 CO2 Emission Factors for Marketable Natural Gas" of National Inventory Report: https://data-donnees.az.ec.gc.ca/api/file?path=%2Fsubstances%2Fmonitor%2Fcanada-s-official-greenhouse-gas-inventory%2FD-Emission-Factors%2FEN_Annex6_Emission_Factors.pdf
1894
+ { "province": 'AB', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 53.87, "CO2eq Emissions (g/m3)": 1962.0 },
1895
+ { "province": 'BC', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 53.98, "CO2eq Emissions (g/m3)": 1966.0 },
1896
+ { "province": 'MB', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.58, "CO2eq Emissions (g/m3)": 1915.0 },
1897
+ { "province": 'NB', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.69, "CO2eq Emissions (g/m3)": 1919.0 },
1898
+ { "province": 'NL', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.69, "CO2eq Emissions (g/m3)": 1919.0 },
1899
+ { "province": 'NT', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 53.98, "CO2eq Emissions (g/m3)": 1966.0 },
1900
+ { "province": 'NS', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.69, "CO2eq Emissions (g/m3)": 1919.0 },
1901
+ { "province": 'NU', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 53.98, "CO2eq Emissions (g/m3)": 1966.0 },
1902
+ { "province": 'ON', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.75, "CO2eq Emissions (g/m3)": 1921.0 },
1903
+ { "province": 'PE', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.69, "CO2eq Emissions (g/m3)": 1919.0 },
1904
+ { "province": 'QC', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.88, "CO2eq Emissions (g/m3)": 1926.0 },
1905
+ { "province": 'SK', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 52.72, "CO2eq Emissions (g/m3)": 1920.0 },
1906
+ { "province": 'YT', "fuel_type": 'NaturalGas', "CO2eq Emissions (kg/MBtu)": 56.98, "CO2eq Emissions (g/m3)": 1966.0 },
1907
+
1908
+ # Obtained from Portfolio Manager https://portfoliomanager.energystar.gov/pdf/reference/Emissions.pdf 10/10/2020
1909
+ { "province": 'AB', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1910
+ { "province": 'BC', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1911
+ { "province": 'MB', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1912
+ { "province": 'NB', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1913
+ { "province": 'NL', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1914
+ { "province": 'NT', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1915
+ { "province": 'NS', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1916
+ { "province": 'NU', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1917
+ { "province": 'ON', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1918
+ { "province": 'PE', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1919
+ { "province": 'QC', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1920
+ { "province": 'SK', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1921
+ { "province": 'YT', "fuel_type": 'FuelOilNo2', "CO2eq Emissions (kg/MBtu)": 75.13, "CO2eq Emissions (g/m3)": 2763.0 },
1922
+
1923
+ { "province": 'AB', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1924
+ { "province": 'BC', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1925
+ { "province": 'MB', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1926
+ { "province": 'NB', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1927
+ { "province": 'NL', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1928
+ { "province": 'NT', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1929
+ { "province": 'NS', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1930
+ { "province": 'NU', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1931
+ { "province": 'ON', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1932
+ { "province": 'PE', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1933
+ { "province": 'QC', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1934
+ { "province": 'SK', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1935
+ { "province": 'YT', "fuel_type": 'Propane', "CO2eq Emissions (kg/MBtu)": 64.25, "CO2eq Emissions (g/m3)": 1548.00 },
1936
+
1937
+ { "province": 'AB', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 202.23, "CO2eq Emissions (g/m3)": 690.0 },
1938
+ { "province": 'BC', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 3.84, "CO2eq Emissions (g/m3)": 13.1 },
1939
+ { "province": 'MB', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 0.41, "CO2eq Emissions (g/m3)": 1.4 },
1940
+ { "province": 'NB', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 84.99, "CO2eq Emissions (g/m3)": 290.0 },
1941
+ { "province": 'NL', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 7.91, "CO2eq Emissions (g/m3)": 27.0 },
1942
+ { "province": 'NT', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 46.89, "CO2eq Emissions (g/m3)": 160.0 },
1943
+ { "province": 'NS', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 216.88, "CO2eq Emissions (g/m3)": 740.0 },
1944
+ { "province": 'NU', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 260.84, "CO2eq Emissions (g/m3)": 890.0 },
1945
+ { "province": 'ON', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 8.79, "CO2eq Emissions (g/m3)": 30.0 },
1946
+ { "province": 'PE', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 84.99, "CO2eq Emissions (g/m3)": 290.0 },
1947
+ { "province": 'QC', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 0.47, "CO2eq Emissions (g/m3)": 1.6 },
1948
+ { "province": 'SK', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 219.81, "CO2eq Emissions (g/m3)": 750.0 },
1949
+ { "province": 'YT', "fuel_type": 'Electricity', "CO2eq Emissions (kg/MBtu)": 23.15, "CO2eq Emissions (g/m3)": 79.0 }
1950
+ ]
1951
+ mbtu_to_gj = 1.05505585
1952
+ factor = ghg_data.detect { |item| (item[:province] == province) && (item[:fuel_type] == fuel_type) }
1953
+ raise "could not find ghg factor for province name #{province} and fuel_type #{fuel_type}" if factor.nil?
1954
+
1955
+ return factor[:"CO2eq Emissions (kg/MBtu)"] / mbtu_to_gj
1956
+ end
1957
+
1958
+ def bc_energy_step_code_performance_indicators
1959
+ # TEDI (Thermal Energy Demand Intensity) [kWh/(m2.year)]
1960
+ command = "SELECT Value
1961
+ FROM TabularDataWithStrings
1962
+ WHERE ReportName='EnergyMeters'
1963
+ AND ReportForString='Entire Facility'
1964
+ AND TableName='Annual and Peak Values - Other'
1965
+ AND RowName='Baseboard:EnergyTransfer'
1966
+ AND ColumnName='Annual Value'
1967
+ AND Units='GJ'"
1968
+ baseboard_energy_transfer_gj = @sqlite_file.get.execAndReturnFirstDouble(command)
1969
+ baseboard_energy_transfer_kwh = OpenStudio.convert(baseboard_energy_transfer_gj.to_f, 'GJ', 'kWh')
1970
+ command = "SELECT Value
1971
+ FROM TabularDataWithStrings
1972
+ WHERE ReportName='EnergyMeters'
1973
+ AND ReportForString='Entire Facility'
1974
+ AND TableName='Annual and Peak Values - Other'
1975
+ AND RowName='HeatingCoils:EnergyTransfer'
1976
+ AND ColumnName='Annual Value'
1977
+ AND Units='GJ'"
1978
+ heating_coils_energy_transfer_gj = @sqlite_file.get.execAndReturnFirstDouble(command)
1979
+ heating_coils_energy_transfer_kwh = OpenStudio.convert(heating_coils_energy_transfer_gj.to_f, 'GJ', 'kWh')
1980
+ tedi_kwh_per_m_sq = (baseboard_energy_transfer_kwh.to_f + heating_coils_energy_transfer_kwh.to_f) / @btap_data['bldg_conditioned_floor_area_m_sq']
1981
+ @btap_data.merge!('bc_step_code_tedi_kwh_per_m_sq' => tedi_kwh_per_m_sq)
1982
+
1983
+ # MEUI (Mechanical Energy Use Intensity) [kWh/(m2.year)]
1984
+ meui_gj_per_m_sq = @btap_data['energy_eui_heating_gj_per_m_sq'].to_f +
1985
+ @btap_data['energy_eui_cooling_gj_per_m_sq'].to_f +
1986
+ @btap_data['energy_eui_fans_gj_per_m_sq'].to_f +
1987
+ @btap_data['energy_eui_pumps_gj_per_m_sq'].to_f +
1988
+ @btap_data['energy_eui_water systems_gj_per_m_sq'].to_f
1989
+ meui_kwh_per_m_sq = OpenStudio.convert(meui_gj_per_m_sq, 'GJ', 'kWh').to_f
1990
+ @btap_data.merge!('bc_step_code_meui_kwh_per_m_sq' => meui_kwh_per_m_sq)
1991
+ end
1992
+
1993
+ # The below method calculates energy demands and peak loads calculations as per PHIUS and NECB; and compares them to see if NECB meets PHIUS' performance criteria.
1994
+ ### References:
1995
+ ### (1) PHIUS 2021 Passive Building Standard Standard-Setting Documentation. Available at https://www.phius.org/phius-certification-for-buildings-products/project-certification/phius-2021-emissions-down-scale-up
1996
+ ### (2) Wright, L. (2019). Setting the Heating/Cooling Performance Criteria for the PHIUS 2018 Passive Building Standard. In ASHRAE Topical Conference Proceedings, pp. 399-409
1997
+ def phius_performance_indicators(model)
1998
+ ### Envelope to Floor Area ratio (EnvFlr)
1999
+ ### Note: 'Floor Area' has been considered as iCFA (interior conditioned floor area) as per REF: Wright (2019)
2000
+ bldg_exterior_area_m_sq = @btap_data['bldg_exterior_area_m_sq']
2001
+ bldg_conditioned_floor_area_m_sq = @btap_data['bldg_conditioned_floor_area_m_sq']
2002
+ bldg_conditioned_floor_area_ft_sq = OpenStudio.convert(bldg_conditioned_floor_area_m_sq, 'm^2', 'ft^2').get
2003
+ envelope_to_floor_area_ratio = bldg_exterior_area_m_sq / bldg_conditioned_floor_area_m_sq
2004
+
2005
+ ### UnitDens: Unit density (1/ft2) (inverse of the floor area per unit) in PHIUS, 2021
2006
+ # Note: if commercial buildings, set the number of units to 1 and divide by the floor area
2007
+ # This is the list of building types considered as some sort of residential buildings when the whole building method is used
2008
+ building_type_names_necb_2011 = ['Dormitory', 'Hospital', 'Hotel', 'Motel', 'Multi-unit residential', 'Penitentiary']
2009
+ building_type_names_necb_2015 = ['Dormitory', 'Health care clinic', 'Hospital', 'Hotel/Motel', 'Long-term care - dwelling units', 'Long-term care - other', 'Multi-unit residential building', 'Penitentiary']
2010
+ building_type_names_necb_2017 = ['Dormitory', 'Health care clinic', 'Hospital', 'Hotel/Motel', 'Long-term care - dwelling units', 'Long-term care - other', 'Multi-unit residential building', 'Penitentiary']
2011
+ building_type_names_list = building_type_names_necb_2011 + building_type_names_necb_2015 + building_type_names_necb_2017
2012
+ building_type_names_list = building_type_names_list.uniq
2013
+ # This is the list of space types considered as some sort of residential spaces when the space type method is used
2014
+ space_type_names_necb_2011 = ['Dormitory - living quarters', 'Dwelling Unit(s)', 'Hotel/Motel - rooms', 'Hway lodging - rooms']
2015
+ space_type_names_necb_2015 = ['Guest room', 'Dormitory living quarters', 'Dwelling units general', 'Dwelling units long-term', 'Fire station sleeping quarters', 'Health care facility patient room', 'Health care facility recovery room']
2016
+ space_type_names_necb_2017 = ['Guest room', 'Dormitory living quarters', 'Dwelling units general', 'Dwelling units long-term', 'Fire station sleeping quarters', 'Health care facility patient room', 'Health care facility recovery room']
2017
+ space_type_names_list = space_type_names_necb_2011 + space_type_names_necb_2015 + space_type_names_necb_2017
2018
+ space_type_names_list = space_type_names_list.uniq
2019
+ sum_handle = 0.0
2020
+ number_of_dwelling_units = 0.0
2021
+ @btap_data['space_table'].each do |space_info|
2022
+ building_type_name = space_info['building_type'].sub! 'building', ''
2023
+ building_type_name = building_type_name.strip unless building_type_name.nil?
2024
+ space_type_name = space_info['space_type_name']
2025
+ if !space_type_name.include?('WholeBuilding')
2026
+ # puts "This_is_the_space_type_method"
2027
+ space_type_name = space_info['space_type_name'].sub! 'Space Function ', '' # This removes 'Space Function' from space type name
2028
+ if space_type_names_list.include?(space_type_name)
2029
+ number_of_dwelling_units += 1.0 * space_info['multiplier']
2030
+ sum_handle += OpenStudio.convert(space_info['floor_area_m2'], 'm^2', 'ft^2').get * space_info['multiplier']
2031
+ end
2032
+ elsif space_type_name.include?('WholeBuilding') && building_type_names_list.include?(building_type_name) && space_info['is_conditioned'] == 'Yes'
2033
+ # puts "This_is_the_whole_building_method"
2034
+ number_of_dwelling_units += 1.0 * space_info['multiplier']
2035
+ sum_handle += OpenStudio.convert(space_info['floor_area_m2'], 'm^2', 'ft^2').get * space_info['multiplier']
2036
+ end
2037
+ end
2038
+ # Calculate what percentage of conditioned floor area has space types of the 'space_type_names_list' list.
2039
+ # This percentage is used to determine if most of a building model is sort of dwelling type or not.
2040
+ # The threshold for this percentage has been set to 60% based on the below reference:
2041
+ # GSA (2012), Circulation: Defining and planning, U.S. General Services Administration, Available at Https://Www.Gsa.Gov/
2042
+ # The above reference says: 'As a general planning rule of thumb, Circulation Area comprises roughly 25 to 40% of the total Usable Area.'
2043
+ # Moreover, ~63% and ~41% of the SmallHotel and LargeHotel archetype, respectively, are guest rooms. So, the threshold has been chosen as 40%.
2044
+ percentage_dwelling = 100.0 * sum_handle / bldg_conditioned_floor_area_ft_sq
2045
+ # now, calculate UnitDens depending on whether a building model is sort of dwelling type or not
2046
+ if percentage_dwelling >= 40.0 && number_of_dwelling_units > 0.0
2047
+ unit_density_per_ft_sq = 1.0 / (sum_handle / number_of_dwelling_units)
2048
+ else # i.e. if commercial buildings, set the number of units to 1 and divide by the floor area
2049
+ unit_density_per_ft_sq = 1.0 / bldg_conditioned_floor_area_ft_sq
2050
+ end
2051
+
2052
+ ### Get weather file
2053
+ weather_file_path = model.weatherFile.get.path.get.to_s
2054
+ epw_file = model.weatherFile.get.file.get
2055
+ stat_file_path = weather_file_path.gsub('.epw', '.stat')
2056
+ stat_file = OpenstudioStandards::Weather::StatFile.new(stat_file_path)
2057
+
2058
+ ### Cooling Degree Days, base 50degF
2059
+ cdd10_degree_c_days = stat_file.cdd10
2060
+ cdd50_degree_f_days = cdd10_degree_c_days * 9.0 / 5.0
2061
+
2062
+ ### Heating Degree Days, base 65degF (note that base temperature of 18degC has been considered)
2063
+ hdd18_degree_c_days = stat_file.hdd18
2064
+ hdd65_degree_f_days = hdd18_degree_c_days * 9.0 / 5.0
2065
+
2066
+ ### Dehumidification degree days
2067
+ ### ('Dehumidification degree-days, base 0.010' in REF: Wright (2019))
2068
+ dehumidification_degree_days = OpenstudioStandards::Weather.epw_file_get_dehumidification_degree_days(epw_file)
2069
+
2070
+ ### annual global horizontal irradiance (GHI)
2071
+
2072
+ # Workaround for case when the weather file contains the February from a leap year but that February only has 28
2073
+ # days of data.
2074
+ has_leap_day = false
2075
+
2076
+ # Find the first day in February
2077
+ feb_index = epw_file.data.find_index { |entry| entry.date.monthOfYear.value == 2 }
2078
+
2079
+ # Find the year for February
2080
+ feb_year = epw_file.data[feb_index].year
2081
+ # Determine if February's year is a leap year
2082
+ leap_year = false
2083
+ if (feb_year % 100) > 0
2084
+ leap_year = true if (feb_year % 4) == 0
2085
+ else
2086
+ leap_year = true if (feb_year % 400) == 0
2087
+ end
2088
+ # If the February is from a leap year determine if it contains a leap day
2089
+ if leap_year
2090
+
2091
+ day = epw_file.data[feb_index].date.dayOfMonth
2092
+ inc = 0
2093
+
2094
+ while epw_file.data[feb_index].date.dayOfMonth == day
2095
+ feb_index += 1
2096
+ inc += 1
2097
+ end
2098
+
2099
+ has_leap_day = epw_file.data[feb_index + (inc * 28)].date.dayOfMonth == 29
2100
+ end
2101
+
2102
+ # If the February is from a leap year and there is no leap day then do not use the faulty OpenStudio Epw
2103
+ # .getTimeSeries method. Otherwise, use the method.
2104
+ if has_leap_day || !leap_year
2105
+ ghi_timeseries = epw_file.getTimeSeries('Global Horizontal Radiation').get.values
2106
+ else
2107
+ # Access the data directly instead of using the OpenStudio API to avoid the faulty OpenStudioEpw
2108
+ # .getTimeSeries method.
2109
+
2110
+ # Open the weather file
2111
+ regex_csv = /[^,]+/
2112
+ regex_num = /[0-9]/
2113
+ f = File.open(epw_file.path.to_s, 'r')
2114
+ i = 0
2115
+
2116
+ # Skip the header
2117
+ i += 1 until f.readline[0] =~ regex_num
2118
+
2119
+ # Get all of the hourly weather data
2120
+ lines = IO.readlines(f)[i..-1]
2121
+
2122
+ # Get hourly weather data for a specific column
2123
+ ghi_timeseries = lines.map {|line| Float(line.scan(regex_csv)[13])}
2124
+ end
2125
+
2126
+ annual_ghi_kwh_per_m_sq = ghi_timeseries.sum / 1000.0
2127
+
2128
+ ### THD-1 Temperature at the colder of the two heating design conditions in PHIUS, 2021
2129
+ ### ('Heating design temperature' in REF: Wright (2019))
2130
+ thd_degree_c = stat_file.heating_design_info[1]
2131
+ thd_degree_f = OpenStudio.convert(thd_degree_c, 'C', 'F').get
2132
+
2133
+ ### TCD Temperature at the cooling design condition in PHIUS, 2021
2134
+ ### ('Cooling design temperature' in REF: Wright (2019))
2135
+ tcd_degree_c = stat_file.cooling_design_info[2]
2136
+ tcd_degree_f = OpenStudio.convert(tcd_degree_c.to_f, 'C', 'F').get
2137
+
2138
+ ### IGHL (Irradiance, Global, at the heating design condition) (Btu/h.ft2) in PHIUS, 2021
2139
+ average_daily_global_irradiance_w_per_m2_array = []
2140
+ model.getDesignDays.each do |design_day|
2141
+ next unless design_day.dayType == 'WinterDesignDay'
2142
+
2143
+ average_daily_global_irradiance_w_per_m2 = OpenstudioStandards::Weather.design_day_average_global_irradiance(design_day)
2144
+ average_daily_global_irradiance_w_per_m2_array << average_daily_global_irradiance_w_per_m2
2145
+ end
2146
+ solar_irradiance_on_heating_design_day_w_per_m_sq = average_daily_global_irradiance_w_per_m2_array.min
2147
+ solar_irradiance_on_heating_design_day_btu_per_hr_ft_sq = OpenStudio.convert(solar_irradiance_on_heating_design_day_w_per_m_sq.to_f, 'W/m^2', 'Btu/ft^2*h').get
2148
+
2149
+ ### IGCL (Irradiance, Global, at the cooling design condition) (Btu/h.ft2) in PHIUS, 2021
2150
+ average_daily_global_irradiance_w_per_m2_array = []
2151
+ model.getDesignDays.each do |design_day|
2152
+ next unless design_day.dayType == 'SummerDesignDay'
2153
+
2154
+ average_daily_global_irradiance_w_per_m2 = OpenstudioStandards::Weather.design_day_average_global_irradiance(design_day)
2155
+ average_daily_global_irradiance_w_per_m2_array << average_daily_global_irradiance_w_per_m2
2156
+ end
2157
+ solar_irradiance_on_cooling_design_day_w_per_m_sq = average_daily_global_irradiance_w_per_m2_array.max
2158
+ solar_irradiance_on_cooling_design_day_btu_per_hr_ft_sq = OpenStudio.convert(solar_irradiance_on_cooling_design_day_w_per_m_sq.to_f, 'W/m^2', 'Btu/ft^2*h').get
2159
+
2160
+ ### occupant density (persons per ft2 of floor area)
2161
+ sum_handle = 0.0
2162
+ @btap_data['space_type_table'].each do |space_info|
2163
+ unless space_info['occ_per_m_sq'].nil?
2164
+ sum_handle += space_info['floor_m_sq'] * space_info['occ_per_m_sq']
2165
+ end
2166
+ end
2167
+ occ_density_person_per_m_sq = sum_handle / bldg_conditioned_floor_area_m_sq
2168
+ occ_density_person_per_ft_sq = OpenStudio.convert(occ_density_person_per_m_sq, 'ft^2', 'm^2').get
2169
+
2170
+ ### marginal electricity price ($/kWh)
2171
+ ### ('Electricity price' in REF: Wright (2019))
2172
+ electricity_price_per_gj = @btap_data['cost_utility_neb_electricity_cost_per_m_sq'] / @btap_data['energy_eui_electricity_gj_per_m_sq']
2173
+ electricity_price_per_kwh = OpenStudio.convert(electricity_price_per_gj, 'kWh', 'GJ').get # note: this is not GJ to kWh since 1/GJ should be converted to 1/kWh.
2174
+
2175
+ ### Calculate annual heating and cooling energy demands based on PHIUS
2176
+ # REF: page 27 of PHIUS 2021 Passive Building Standard Standard-Setting Documentation. Available at https://www.phius.org/phius-certification-for-buildings-products/project-certification/phius-2021-emissions-down-scale-up
2177
+ annual_heating_demand_kbtu_per_ft_sq_phius = 3.2606827206 +
2178
+ 1.1634499236 * envelope_to_floor_area_ratio +
2179
+ 904.39163818 * unit_density_per_ft_sq +
2180
+ 0.000604853 * hdd65_degree_f_days +
2181
+ -0.001645777 * annual_ghi_kwh_per_m_sq +
2182
+ -11.87299596 * electricity_price_per_kwh +
2183
+ (envelope_to_floor_area_ratio - 1.766) * (envelope_to_floor_area_ratio - 1.766) * 0.8314860529 +
2184
+ (envelope_to_floor_area_ratio - 1.766) * (hdd65_degree_f_days - 5860.0833333) * 0.0002310823 +
2185
+ (hdd65_degree_f_days - 5860.0833333) * (hdd65_degree_f_days - 5860.0833333) * -5.736435e-8 +
2186
+ (hdd65_degree_f_days - 5860.0833333) * (annual_ghi_kwh_per_m_sq - 1451.0633333) * -3.260379e-7 +
2187
+ (envelope_to_floor_area_ratio - 1.766) * (electricity_price_per_kwh - -0.2029193333) * -3.851052937 +
2188
+ (hdd65_degree_f_days - 5860.0833333) * (electricity_price_per_kwh - -0.2029193333) * -0.001897043
2189
+ annual_heating_demand_kwh_per_m_sq_phius = OpenStudio.convert(annual_heating_demand_kbtu_per_ft_sq_phius, 'kBtu/ft^2', 'kWh/m^2').get
2190
+ @btap_data.merge!('phius_annual_heating_demand_kwh_per_m_sq' => annual_heating_demand_kwh_per_m_sq_phius)
2191
+
2192
+ # REF: page 28 of PHIUS 2021 Passive Building Standard Standard-Setting Documentation. Available at https://www.phius.org/phius-certification-for-buildings-products/project-certification/phius-2021-emissions-down-scale-up
2193
+ annual_cooling_demand_kbtu_per_ft_sq_phius = -6.510791255 +
2194
+ -0.749993351 * envelope_to_floor_area_ratio +
2195
+ 0.0004550801 * cdd50_degree_f_days +
2196
+ 0.004990109 * annual_ghi_kwh_per_m_sq +
2197
+ 7.9460878688 * dehumidification_degree_days +
2198
+ (envelope_to_floor_area_ratio - 1.766) * (envelope_to_floor_area_ratio - 1.766) * 1.6367059356 +
2199
+ (cdd50_degree_f_days - 4104.8333333) * (cdd50_degree_f_days - 4104.8333333) * 8.6952014e-8 +
2200
+ (envelope_to_floor_area_ratio - 1.766) * (annual_ghi_kwh_per_m_sq - 1451.0633333) * 0.001671947 +
2201
+ (cdd50_degree_f_days - 4104.8333333) * (annual_ghi_kwh_per_m_sq - 1451.0633333) * 0.0000013639 +
2202
+ (unit_density_per_ft_sq - 0.0008646735) * (dehumidification_degree_days - 0.3233057481) * 5547.7542211 +
2203
+ (dehumidification_degree_days - 0.3233057481) * (electricity_price_per_kwh - 0.2029193333) * -15.67511944 +
2204
+ 1624.6144639 * unit_density_per_ft_sq
2205
+ annual_cooling_demand_kwh_per_m_sq_phius = OpenStudio.convert(annual_cooling_demand_kbtu_per_ft_sq_phius, 'kBtu/ft^2', 'kWh/m^2').get
2206
+ @btap_data.merge!('phius_annual_cooling_demand_kwh_per_m_sq' => annual_cooling_demand_kwh_per_m_sq_phius)
2207
+
2208
+ ### Calculate peak heating and cooling loads based on PHIUS
2209
+ # REF: page 29 of PHIUS 2021 Passive Building Standard Standard-Setting Documentation. Available at https://www.phius.org/phius-certification-for-buildings-products/project-certification/phius-2021-emissions-down-scale-up
2210
+ peak_heating_load_btu_per_hr_ft_sq_phius = 4.6700403241 +
2211
+ 0.6774809481 * envelope_to_floor_area_ratio +
2212
+ 239.08369574 * occ_density_person_per_ft_sq +
2213
+ 596.681543 * unit_density_per_ft_sq +
2214
+ -0.000177742 * hdd65_degree_f_days +
2215
+ -0.076727655 * thd_degree_f +
2216
+ -0.03316804 * solar_irradiance_on_heating_design_day_btu_per_hr_ft_sq +
2217
+ -4.140193817 * electricity_price_per_kwh +
2218
+ (envelope_to_floor_area_ratio - 1.766) * (envelope_to_floor_area_ratio - 1.766) * 0.8449921713 +
2219
+ (hdd65_degree_f_days - 5860.0833333) * (hdd65_degree_f_days - 5860.0833333) * 2.8376386e-8 +
2220
+ (envelope_to_floor_area_ratio - 1.766) * (thd_degree_f - 14.7102) * -0.013821021 +
2221
+ (unit_density_per_ft_sq - 0.0008646735) * (thd_degree_f - 14.7102) * -20.10551451 +
2222
+ (hdd65_degree_f_days - 5860.0833333) * (thd_degree_f - 14.7102) * 5.1870203e-6 +
2223
+ (thd_degree_f - 14.7102) * (electricity_price_per_kwh - 0.2029193333) * 0.1264922802
2224
+ peak_heating_load_w_per_m_sq_phius = OpenStudio.convert(peak_heating_load_btu_per_hr_ft_sq_phius, 'Btu/ft^2*h', 'W/m^2').get
2225
+ @btap_data.merge!('phius_peak_heating_load_w_per_m_sq' => peak_heating_load_w_per_m_sq_phius)
2226
+
2227
+ # REF: page 30 of PHIUS 2021 Passive Building Standard Standard-Setting Documentation. Available at https://www.phius.org/phius-certification-for-buildings-products/project-certification/phius-2021-emissions-down-scale-up
2228
+ peak_cooling_load_btu_per_hr_ft_sq_phius = -7.289806442 +
2229
+ 98.245977611 * occ_density_person_per_ft_sq +
2230
+ 236.93351876 * unit_density_per_ft_sq +
2231
+ 0.0967328928 * tcd_degree_f +
2232
+ 0.010777725 * solar_irradiance_on_cooling_design_day_btu_per_hr_ft_sq +
2233
+ (cdd50_degree_f_days - 4104.8333333) * (cdd50_degree_f_days - 4104.8333333) * 1.7699655e-8 +
2234
+ (cdd50_degree_f_days - 4104.8333333) * (tcd_degree_f - 78.127) * 6.5268802e-6 +
2235
+ (tcd_degree_f - 78.127) * (envelope_to_floor_area_ratio - 1.766) * 0.0165401721 +
2236
+ (tcd_degree_f - 78.127) * (occ_density_person_per_ft_sq - 0.0027218) * 8.0465528305 +
2237
+ (cdd50_degree_f_days - 4104.8333333) * (envelope_to_floor_area_ratio - 1.766) * 0.0000322288 +
2238
+ (envelope_to_floor_area_ratio - 1.766) * (envelope_to_floor_area_ratio - 1.766) * 0.6579032913
2239
+ peak_cooling_load_w_per_m_sq_phius = OpenStudio.convert(peak_cooling_load_btu_per_hr_ft_sq_phius, 'Btu/ft^2*h', 'W/m^2').get
2240
+ @btap_data.merge!('phius_peak_cooling_load_w_per_m_sq' => peak_cooling_load_w_per_m_sq_phius)
2241
+
2242
+ ### Gather annual heating and cooling energy demands based on NECB
2243
+
2244
+ annual_heating_demand_kwh_per_m_sq_necb = OpenStudio.convert(@btap_data['energy_eui_heating_gj_per_m_sq'], 'GJ', 'kWh') unless @btap_data['energy_eui_heating_gj_per_m_sq'].nil?
2245
+ annual_cooling_demand_kwh_per_m_sq_necb = OpenStudio.convert(@btap_data['energy_eui_cooling_gj_per_m_sq'], 'GJ', 'kWh') unless @btap_data['energy_eui_cooling_gj_per_m_sq'].nil?
2246
+
2247
+ ### Gather peak heating and cooling loads based on NECB
2248
+ peak_heating_load_w_per_m_sq_necb = @btap_data['heating_peak_w_per_m_sq']
2249
+ peak_cooling_load_w_per_m_sq_necb = @btap_data['cooling_peak_w_per_m_sq']
2250
+ @btap_data.merge!('peak_heating_load_w_per_m_sq_necb' => peak_heating_load_w_per_m_sq_necb)
2251
+ @btap_data.merge!('peak_cooling_load_w_per_m_sq_necb' => peak_cooling_load_w_per_m_sq_necb)
2252
+
2253
+ ### Compare annual heating and cooling energy demands of NECB with PHIUS to see if NECB meets PHIUS
2254
+ if annual_heating_demand_kwh_per_m_sq_necb.to_f <= annual_heating_demand_kwh_per_m_sq_phius.to_f
2255
+ @btap_data.merge!('phius_necb_meet_heating_demand' => 'True')
2256
+ else
2257
+ @btap_data.merge!('phius_necb_meet_heating_demand' => 'False')
2258
+ end
2259
+ if annual_cooling_demand_kwh_per_m_sq_necb.to_f <= annual_cooling_demand_kwh_per_m_sq_phius.to_f
2260
+ @btap_data.merge!('phius_necb_meet_cooling_demand' => 'True')
2261
+ else
2262
+ @btap_data.merge!('phius_necb_meet_cooling_demand' => 'False')
2263
+ end
2264
+
2265
+ ### Compare peak heating and cooling loads of NECB with PHIUS to see if NECB meets PHIUS
2266
+ if peak_heating_load_w_per_m_sq_necb.to_f <= peak_heating_load_w_per_m_sq_phius.to_f
2267
+ @btap_data.merge!('phius_necb_meet_heating_peak_load' => 'True')
2268
+ else
2269
+ @btap_data.merge!('phius_necb_meet_heating_peak_load' => 'False')
2270
+ end
2271
+ if peak_cooling_load_w_per_m_sq_necb.to_f <= peak_cooling_load_w_per_m_sq_phius.to_f
2272
+ @btap_data.merge!('phius_necb_meet_cooling_peak_load' => 'True')
2273
+ else
2274
+ @btap_data.merge!('phius_necb_meet_cooling_peak_load' => 'False')
2275
+ end
2276
+ end
2277
+
2278
+ def get_national_ghg_cost(year:)
2279
+ raise "Nil year submitted for national ghg cost" if year.nil?
2280
+ year = year.to_i
2281
+ if year < 2019
2282
+ price = 0.0
2283
+ elsif (year >= 2019) && (year <= 2030)
2284
+ ghg_price_data = @standards_data["tables"]["canada_national_carbon_pricing"]["table"]
2285
+ year_price = ghg_price_data.detect { |item| (item["year"].to_i == year) }
2286
+ price = year_price["price_per_tonne"].to_f
2287
+ elsif (year > 2030)
2288
+ price = 170.0
2289
+ end
2290
+ return price.to_f
2291
+ end
2292
+ end
2293
+ end