openstudio-standards 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/data/standards/OpenStudio_Standards.xlsx +0 -0
  3. data/data/standards/OpenStudio_Standards_boilers.json +62 -4
  4. data/data/standards/OpenStudio_Standards_chillers.json +778 -68
  5. data/data/standards/OpenStudio_Standards_construction_sets.json +52 -93
  6. data/data/standards/OpenStudio_Standards_curve_biquadratics.json +36 -36
  7. data/data/standards/OpenStudio_Standards_curve_quadratics.json +3 -3
  8. data/data/standards/OpenStudio_Standards_heat_pumps.json +840 -0
  9. data/data/standards/OpenStudio_Standards_heat_pumps_heating.json +352 -0
  10. data/data/standards/OpenStudio_Standards_heat_rejection.json +48 -0
  11. data/data/standards/OpenStudio_Standards_motors.json +270 -0
  12. data/data/standards/OpenStudio_Standards_space_types.json +10390 -2824
  13. data/data/standards/OpenStudio_Standards_unitary_acs.json +794 -18
  14. data/data/weather/USA_CO_Boulder-Broomfield-Jefferson.County.AP.724699_TMY3.ddy +538 -0
  15. data/data/weather/USA_CO_Boulder-Broomfield-Jefferson.County.AP.724699_TMY3.epw +8768 -0
  16. data/data/weather/USA_CO_Boulder-Broomfield-Jefferson.County.AP.724699_TMY3.stat +493 -0
  17. data/data/weather/USA_CO_Denver.Intl.AP.725650_TMY3.ddy +536 -0
  18. data/data/weather/USA_CO_Denver.Intl.AP.725650_TMY3.epw +8768 -0
  19. data/data/weather/USA_CO_Denver.Intl.AP.725650_TMY3.stat +554 -0
  20. data/data/weather/USA_CO_Fort.Collins.AWOS.724769_TMY3.ddy +536 -0
  21. data/data/weather/USA_CO_Fort.Collins.AWOS.724769_TMY3.epw +8768 -0
  22. data/data/weather/USA_CO_Fort.Collins.AWOS.724769_TMY3.stat +554 -0
  23. data/data/weather/envelope_info.csv +6 -0
  24. data/lib/openstudio-standards.rb +10 -11
  25. data/lib/openstudio-standards/btap/compliance.rb +251 -969
  26. data/lib/openstudio-standards/btap/envelope.rb +1 -1
  27. data/lib/openstudio-standards/btap/fileio.rb +37 -5
  28. data/lib/openstudio-standards/btap/geometry.rb +27 -17
  29. data/lib/openstudio-standards/btap/hvac.rb +80 -27
  30. data/lib/openstudio-standards/hvac_sizing/{HVACSizing.CoilHeatingDXMultiSpeed.rb → Siz.CoilHeatingDXMultiSpeed.rb} +0 -0
  31. data/lib/openstudio-standards/hvac_sizing/Siz.ControllerOutdoorAir.rb +30 -4
  32. data/lib/openstudio-standards/hvac_sizing/Siz.CoolingTowerTwoSpeed.rb +61 -5
  33. data/lib/openstudio-standards/hvac_sizing/Siz.CoolingTowerVariableSpeed.rb +37 -7
  34. data/lib/openstudio-standards/hvac_sizing/Siz.DistrictCooling.rb +27 -0
  35. data/lib/openstudio-standards/hvac_sizing/Siz.DistrictHeating.rb +27 -0
  36. data/lib/openstudio-standards/hvac_sizing/Siz.HeaderedPumpsConstantSpeed.rb +55 -0
  37. data/lib/openstudio-standards/hvac_sizing/Siz.HeaderedPumpsVariableSpeed.rb +55 -0
  38. data/lib/openstudio-standards/hvac_sizing/Siz.HeatingCoolingFuels.rb +51 -9
  39. data/lib/openstudio-standards/hvac_sizing/Siz.Model.rb +99 -17
  40. data/lib/openstudio-standards/hvac_sizing/Siz.PumpConstantSpeed.rb +1 -1
  41. data/lib/openstudio-standards/hvac_sizing/Siz.ThermalZone.rb +29 -6
  42. data/lib/openstudio-standards/hvac_sizing/Siz.WaterHeaterMixed.rb +16 -0
  43. data/lib/openstudio-standards/prototypes/Prototype.AirTerminalSingleDuctVAVReheat.rb +43 -48
  44. data/lib/openstudio-standards/prototypes/Prototype.ControllerWaterCoil.rb +5 -9
  45. data/lib/openstudio-standards/prototypes/Prototype.Fan.rb +68 -0
  46. data/lib/openstudio-standards/prototypes/Prototype.FanConstantVolume.rb +39 -43
  47. data/lib/openstudio-standards/prototypes/Prototype.FanOnOff.rb +49 -51
  48. data/lib/openstudio-standards/prototypes/Prototype.FanVariableVolume.rb +55 -61
  49. data/lib/openstudio-standards/prototypes/Prototype.FanZoneExhaust.rb +8 -10
  50. data/lib/openstudio-standards/prototypes/Prototype.HeatExchangerAirToAirSensibleAndLatent.rb +15 -20
  51. data/lib/openstudio-standards/prototypes/Prototype.Model.hvac.rb +330 -322
  52. data/lib/openstudio-standards/prototypes/Prototype.Model.rb +501 -446
  53. data/lib/openstudio-standards/prototypes/Prototype.Model.swh.rb +221 -230
  54. data/lib/openstudio-standards/prototypes/Prototype.add_objects.rb +0 -2
  55. data/lib/openstudio-standards/prototypes/Prototype.full_service_restaurant.rb +130 -137
  56. data/lib/openstudio-standards/prototypes/Prototype.high_rise_apartment.rb +374 -291
  57. data/lib/openstudio-standards/prototypes/Prototype.hospital.rb +146 -193
  58. data/lib/openstudio-standards/prototypes/Prototype.hvac_systems.rb +1315 -1113
  59. data/lib/openstudio-standards/prototypes/Prototype.large_hotel.rb +65 -88
  60. data/lib/openstudio-standards/prototypes/Prototype.large_office.rb +101 -156
  61. data/lib/openstudio-standards/prototypes/Prototype.medium_office.rb +46 -96
  62. data/lib/openstudio-standards/prototypes/Prototype.mid_rise_apartment.rb +113 -123
  63. data/lib/openstudio-standards/prototypes/Prototype.outpatient.rb +356 -345
  64. data/lib/openstudio-standards/prototypes/Prototype.primary_school.rb +48 -103
  65. data/lib/openstudio-standards/prototypes/Prototype.quick_service_restaurant.rb +115 -123
  66. data/lib/openstudio-standards/prototypes/Prototype.retail_standalone.rb +30 -39
  67. data/lib/openstudio-standards/prototypes/Prototype.retail_stripmall.rb +32 -45
  68. data/lib/openstudio-standards/prototypes/Prototype.secondary_school.rb +98 -258
  69. data/lib/openstudio-standards/prototypes/Prototype.small_hotel.rb +429 -474
  70. data/lib/openstudio-standards/prototypes/Prototype.small_office.rb +28 -36
  71. data/lib/openstudio-standards/prototypes/Prototype.strip_model.rb +7 -7
  72. data/lib/openstudio-standards/prototypes/Prototype.utilities.rb +172 -146
  73. data/lib/openstudio-standards/prototypes/Prototype.warehouse.rb +46 -53
  74. data/lib/openstudio-standards/standards/Standards.AirLoopHVAC.rb +885 -707
  75. data/lib/openstudio-standards/standards/Standards.AirTerminalSingleDuctParallelPIUReheat.rb +48 -57
  76. data/lib/openstudio-standards/standards/Standards.AirTerminalSingleDuctVAVReheat.rb +24 -31
  77. data/lib/openstudio-standards/standards/Standards.BoilerHotWater.rb +80 -93
  78. data/lib/openstudio-standards/standards/Standards.BuildingStory.rb +69 -0
  79. data/lib/openstudio-standards/standards/Standards.ChillerElectricEIR.rb +60 -72
  80. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXMultiSpeed.rb +104 -108
  81. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXSingleSpeed.rb +190 -198
  82. data/lib/openstudio-standards/standards/Standards.CoilCoolingDXTwoSpeed.rb +134 -146
  83. data/lib/openstudio-standards/standards/Standards.CoilHeatingDXMultiSpeed.rb +56 -60
  84. data/lib/openstudio-standards/standards/Standards.CoilHeatingDXSingleSpeed.rb +151 -161
  85. data/lib/openstudio-standards/standards/Standards.CoilHeatingGasMultiStage.rb +30 -34
  86. data/lib/openstudio-standards/standards/Standards.Construction.rb +116 -132
  87. data/lib/openstudio-standards/standards/Standards.CoolingTower.rb +138 -0
  88. data/lib/openstudio-standards/standards/Standards.CoolingTowerSingleSpeed.rb +11 -0
  89. data/lib/openstudio-standards/standards/Standards.CoolingTowerTwoSpeed.rb +11 -0
  90. data/lib/openstudio-standards/standards/Standards.CoolingTowerVariableSpeed.rb +16 -0
  91. data/lib/openstudio-standards/standards/Standards.Fan.rb +190 -236
  92. data/lib/openstudio-standards/standards/Standards.FanConstantVolume.rb +0 -2
  93. data/lib/openstudio-standards/standards/Standards.FanOnOff.rb +0 -2
  94. data/lib/openstudio-standards/standards/Standards.FanVariableVolume.rb +168 -14
  95. data/lib/openstudio-standards/standards/Standards.FanZoneExhaust.rb +0 -2
  96. data/lib/openstudio-standards/standards/Standards.HeaderedPumpsConstantSpeed.rb +33 -0
  97. data/lib/openstudio-standards/standards/Standards.HeaderedPumpsVariableSpeed.rb +83 -0
  98. data/lib/openstudio-standards/standards/Standards.HeatExchangerSensLat.rb +22 -0
  99. data/lib/openstudio-standards/standards/Standards.Model.rb +2385 -1622
  100. data/lib/openstudio-standards/standards/Standards.PlanarSurface.rb +83 -35
  101. data/lib/openstudio-standards/standards/Standards.PlantLoop.rb +805 -395
  102. data/lib/openstudio-standards/standards/Standards.Pump.rb +139 -119
  103. data/lib/openstudio-standards/standards/Standards.PumpConstantSpeed.rb +0 -2
  104. data/lib/openstudio-standards/standards/Standards.PumpVariableSpeed.rb +16 -15
  105. data/lib/openstudio-standards/standards/Standards.ScheduleCompact.rb +35 -0
  106. data/lib/openstudio-standards/standards/Standards.ScheduleConstant.rb +7 -13
  107. data/lib/openstudio-standards/standards/Standards.ScheduleRuleset.rb +144 -59
  108. data/lib/openstudio-standards/standards/Standards.Space.rb +1509 -1326
  109. data/lib/openstudio-standards/standards/Standards.SpaceType.rb +254 -262
  110. data/lib/openstudio-standards/standards/Standards.SubSurface.rb +105 -105
  111. data/lib/openstudio-standards/standards/Standards.Surface.rb +27 -31
  112. data/lib/openstudio-standards/standards/Standards.ThermalZone.rb +882 -157
  113. data/lib/openstudio-standards/standards/Standards.WaterHeaterMixed.rb +179 -69
  114. data/lib/openstudio-standards/standards/Standards.ZoneHVACComponent.rb +75 -0
  115. data/lib/openstudio-standards/utilities/logging.rb +31 -38
  116. data/lib/openstudio-standards/utilities/simulation.rb +118 -82
  117. data/lib/openstudio-standards/version.rb +1 -1
  118. data/lib/openstudio-standards/weather/Weather.Model.rb +382 -390
  119. data/lib/openstudio-standards/weather/Weather.stat_file.rb +159 -78
  120. metadata +59 -6
@@ -1,44 +1,42 @@
1
1
 
2
2
  # open the class to add methods to apply HVAC efficiency standards
3
3
  class OpenStudio::Model::SubSurface
4
-
5
4
  # Determine the component infiltration rate for this surface
6
5
  #
7
6
  # @param type [String] choices are 'baseline' and 'advanced'
8
7
  # @return [Double] infiltration rate
9
8
  # @units cubic meters per second (m^3/s)
10
9
  def component_infiltration_rate(type)
11
-
12
- comp_infil_rate_m3_per_s = 0.0
13
-
10
+ comp_infil_rate_m3_per_s = 0.0
11
+
14
12
  # Define the envelope component infiltration rates
15
13
  component_infil_rates_cfm_per_ft2 = {
16
- 'baseline'=>{
17
- 'opaque_door'=>0.40,
18
- 'loading_dock_door'=>0.40,
19
- 'swinging_or_revolving_glass_door'=>1.0,
20
- 'vestibule'=>1.0,
21
- 'sliding_glass_door'=>0.40,
22
- 'window'=>0.40,
23
- 'skylight'=>0.40
14
+ 'baseline' => {
15
+ 'opaque_door' => 0.40,
16
+ 'loading_dock_door' => 0.40,
17
+ 'swinging_or_revolving_glass_door' => 1.0,
18
+ 'vestibule' => 1.0,
19
+ 'sliding_glass_door' => 0.40,
20
+ 'window' => 0.40,
21
+ 'skylight' => 0.40
24
22
  },
25
- 'advanced'=>{
26
- 'opaque_door'=>0.20,
27
- 'loading_dock_door'=>0.20,
28
- 'swinging_or_revolving_glass_door'=>1.0,
29
- 'vestibule'=>1.0,
30
- 'sliding_glass_door'=>0.20,
31
- 'window'=>0.20,
32
- 'skylight'=>0.20
23
+ 'advanced' => {
24
+ 'opaque_door' => 0.20,
25
+ 'loading_dock_door' => 0.20,
26
+ 'swinging_or_revolving_glass_door' => 1.0,
27
+ 'vestibule' => 1.0,
28
+ 'sliding_glass_door' => 0.20,
29
+ 'window' => 0.20,
30
+ 'skylight' => 0.20
33
31
  }
34
32
  }
35
-
36
- boundary_condition = self.outsideBoundaryCondition
33
+
34
+ boundary_condition = outsideBoundaryCondition
37
35
  # Skip non-outdoor surfaces
38
- return comp_infil_rate_m3_per_s unless self.outsideBoundaryCondition == 'Outdoors' || self.outsideBoundaryCondition == 'Ground'
39
-
36
+ return comp_infil_rate_m3_per_s unless outsideBoundaryCondition == 'Outdoors' || outsideBoundaryCondition == 'Ground'
37
+
40
38
  # Per area infiltration rate for this surface
41
- surface_type = self.subSurfaceType
39
+ surface_type = subSurfaceType
42
40
  infil_rate_cfm_per_ft2 = nil
43
41
  case boundary_condition
44
42
  when 'Outdoors'
@@ -48,50 +46,84 @@ class OpenStudio::Model::SubSurface
48
46
  when 'OverheadDoor'
49
47
  infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['loading_dock_door']
50
48
  when 'GlassDoor'
51
- OpenStudio::logFree(OpenStudio::Info, "openstudio.Standards.Model", "For #{self.name}, assuming swinging_or_revolving_glass_door for infiltration calculation.")
49
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.Model', "For #{name}, assuming swinging_or_revolving_glass_door for infiltration calculation.")
52
50
  infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['swinging_or_revolving_glass_door']
53
- when 'FixedWindow','OperableWindow'
51
+ when 'FixedWindow', 'OperableWindow'
54
52
  infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['window']
55
- when 'Skylight','TubularDaylightDome','TubularDaylightDiffuser'
53
+ when 'Skylight', 'TubularDaylightDome', 'TubularDaylightDiffuser'
56
54
  infil_rate_cfm_per_ft2 = component_infil_rates_cfm_per_ft2[type]['skylight']
57
55
  end
58
56
  end
59
57
  if infil_rate_cfm_per_ft2.nil?
60
- OpenStudio::logFree(OpenStudio::Warn, "openstudio.Standards.Model", "For #{self.name}, could not determine surface type for infiltration, will not be included in calculation.")
58
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.Model', "For #{name}, could not determine surface type for infiltration, will not be included in calculation.")
61
59
  return comp_infil_rate_m3_per_s
62
60
  end
63
-
61
+
64
62
  # Area of the surface
65
- area_m2 = self.netArea
66
- area_ft2 = OpenStudio.convert(area_m2,'m^2','ft^2').get
67
-
63
+ area_m2 = netArea
64
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
65
+
68
66
  # Rate for this surface
69
67
  comp_infil_rate_cfm = area_ft2 * infil_rate_cfm_per_ft2
70
68
 
71
- comp_infil_rate_m3_per_s = OpenStudio.convert(comp_infil_rate_cfm,'cfm','m^3/s').get
69
+ comp_infil_rate_m3_per_s = OpenStudio.convert(comp_infil_rate_cfm, 'cfm', 'm^3/s').get
70
+
71
+ # OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "......#{self.name}, infil = #{comp_infil_rate_cfm.round(2)} cfm @ rate = #{infil_rate_cfm_per_ft2} cfm/ft2, area = #{area_ft2.round} ft2.")
72
72
 
73
- #OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "......#{self.name}, infil = #{comp_infil_rate_cfm.round(2)} cfm @ rate = #{infil_rate_cfm_per_ft2} cfm/ft2, area = #{area_ft2.round} ft2.")
74
-
75
73
  return comp_infil_rate_m3_per_s
76
-
77
74
  end
78
-
75
+
76
+ # Reduce the area of the subsurface by shrinking it
77
+ # toward the centroid.
78
+ # @author Julien Marrec
79
+ #
80
+ # @param percent_reduction [Double] the fractional amount
81
+ # to reduce the area.
82
+ def reduce_area_by_percent_by_shrinking_toward_centroid(percent_reduction)
83
+ mult = 1 - percent_reduction
84
+ scale_factor = mult**0.5
85
+
86
+ # Get the centroid (Point3d)
87
+ g = centroid
88
+
89
+ # Create an array to collect the new vertices
90
+ new_vertices = []
91
+
92
+ # Loop on vertices (Point3ds)
93
+ vertices.each do |vertex|
94
+ # Point3d - Point3d = Vector3d
95
+ # Vector from centroid to vertex (GA, GB, GC, etc)
96
+ centroid_vector = vertex - g
97
+
98
+ # Resize the vector (done in place) according to scale_factor
99
+ centroid_vector.setLength(centroid_vector.length * scale_factor)
100
+
101
+ # Move the vertex toward the centroid
102
+ vertex = g + centroid_vector
103
+
104
+ new_vertices << vertex
105
+ end
106
+
107
+ # Assign the new vertices to the self
108
+ setVertices(new_vertices)
109
+ end
110
+
79
111
  # Reduce the area of the subsurface by raising the
80
112
  # sill height.
81
113
  #
82
114
  # @param percent_reduction [Double] the fractional amount
83
115
  # to reduce the area.
84
116
  def reduce_area_by_percent_by_raising_sill(percent_reduction)
85
-
86
- mult = 1-percent_reduction
87
-
117
+
118
+ mult = 1 - percent_reduction
119
+
88
120
  # Calculate the original area
89
- area_original = self.netArea
121
+ area_original = netArea
90
122
 
91
123
  # Find the min and max z values
92
124
  min_z_val = 99999
93
125
  max_z_val = -99999
94
- self.vertices.each do |vertex|
126
+ vertices.each do |vertex|
95
127
  # Min z value
96
128
  if vertex.z < min_z_val
97
129
  min_z_val = vertex.z
@@ -101,16 +133,16 @@ class OpenStudio::Model::SubSurface
101
133
  max_z_val = vertex.z
102
134
  end
103
135
  end
104
-
136
+
105
137
  # Calculate the window height
106
138
  height = max_z_val - min_z_val
107
-
139
+
108
140
  # Calculate the new sill height
109
141
  new_sill_z = max_z_val - (height * mult)
110
-
142
+
111
143
  # Reset the z value of the lowest points
112
144
  new_vertices = []
113
- self.vertices.each do |vertex|
145
+ vertices.each do |vertex|
114
146
  new_x = vertex.x
115
147
  new_y = vertex.y
116
148
  new_z = vertex.z
@@ -119,69 +151,37 @@ class OpenStudio::Model::SubSurface
119
151
  end
120
152
  new_vertices << OpenStudio::Point3d.new(new_x, new_y, new_z)
121
153
  end
122
-
154
+
123
155
  # Reset the vertices
124
- self.setVertices(new_vertices)
125
-
126
- # Compare the new area to the old for validation
127
- act_pct_red = 1.0 - (self.netArea / area_original)
128
-
156
+ setVertices(new_vertices)
157
+
129
158
  return true
130
-
131
159
  end
132
160
 
133
- # Reduce the area of the subsurface by shrinking it
134
- # in the x direction. Designed to work on skylights.
135
- #
136
- # @param percent_reduction [Double] the fractional amount
137
- # to reduce the area.
138
- def reduce_area_by_percent_by_shrinking_x(percent_reduction)
139
-
140
- mult = 1-percent_reduction
141
-
142
- # Calculate the original area
143
- area_original = self.netArea
144
-
145
- # Find the min and max x values
146
- min_x_val = 99999
147
- max_x_val = -99999
148
- self.vertices.each do |vertex|
149
- # Min x value
150
- if vertex.x < min_x_val
151
- min_x_val = vertex.x
152
- end
153
- # Max x value
154
- if vertex.x > max_x_val
155
- max_x_val = vertex.x
156
- end
157
- end
158
-
159
- # Calculate the skylight width
160
- width = max_x_val - min_x_val
161
-
162
- # Calculate the new sill width
163
- new_width_x = max_x_val - (width * mult)
164
-
165
- # Reset the z value of the lowest points
166
- new_vertices = []
167
- self.vertices.each do |vertex|
168
- new_x = vertex.x
169
- if new_x == min_x_val
170
- new_x = new_width_x
171
- end
172
- new_y = vertex.y
173
- new_z = vertex.z
174
- new_vertices << OpenStudio::Point3d.new(new_x, new_y, new_z)
161
+ # Determine if the sub surface is a vertical rectangle,
162
+ # meaning a rectangle where the bottom is parallel to the ground.
163
+ def vertical_rectangle?
164
+ # Get the vertices once
165
+ verts = vertices
166
+
167
+ # Check for 4 vertices
168
+ return false unless verts.size == 4
169
+
170
+ # Check if the 2 lowest z-values
171
+ # are the same
172
+ z_vals = []
173
+ verts.each do |vertex|
174
+ z_vals << vertex.z
175
175
  end
176
-
177
- # Reset the vertices
178
- self.setVertices(new_vertices)
179
-
180
- # Compare the new area to the old for validation
181
- act_pct_red = 1.0 - (self.netArea / area_original)
182
-
176
+ z_vals = z_vals.sort
177
+ return false unless z_vals[0] = z_vals[1]
178
+
179
+ # Check if the diagonals are equal length
180
+ diag_a = verts[0] - verts[2]
181
+ diag_b = verts[1] - verts[3]
182
+ return false unless diag_a.length == diag_b.length
183
+
184
+ # If here, we have a rectangle
183
185
  return true
184
-
185
186
  end
186
-
187
187
  end
@@ -1,7 +1,6 @@
1
1
 
2
2
  # open the class to add methods to apply HVAC efficiency standards
3
3
  class OpenStudio::Model::Surface
4
-
5
4
  # Determine the component infiltration rate for this surface
6
5
  #
7
6
  # @param type [String] choices are 'baseline' and 'advanced'
@@ -9,33 +8,32 @@ class OpenStudio::Model::Surface
9
8
  # @units cubic meters per second (m^3/s)
10
9
  # @todo handle floors over unconditioned spaces
11
10
  def component_infiltration_rate(type)
12
-
13
- comp_infil_rate_m3_per_s = 0.0
14
-
11
+ comp_infil_rate_m3_per_s = 0.0
12
+
15
13
  # Define the envelope component infiltration rates
16
14
  component_infil_rates_cfm_per_ft2 = {
17
- 'baseline'=>{
18
- 'roof'=>0.12,
19
- 'exterior_wall'=>0.12,
20
- 'below_grade_wall'=>0.12,
21
- 'floor_over_unconditioned'=>0.12,
22
- 'slab_on_grade'=>0.12,
15
+ 'baseline' => {
16
+ 'roof' => 0.12,
17
+ 'exterior_wall' => 0.12,
18
+ 'below_grade_wall' => 0.12,
19
+ 'floor_over_unconditioned' => 0.12,
20
+ 'slab_on_grade' => 0.12
23
21
  },
24
- 'advanced'=>{
25
- 'roof'=>0.04,
26
- 'exterior_wall'=>0.04,
27
- 'below_grade_wall'=>0.04,
28
- 'floor_over_unconditioned'=>0.04,
29
- 'slab_on_grade'=>0.04,
22
+ 'advanced' => {
23
+ 'roof' => 0.04,
24
+ 'exterior_wall' => 0.04,
25
+ 'below_grade_wall' => 0.04,
26
+ 'floor_over_unconditioned' => 0.04,
27
+ 'slab_on_grade' => 0.04
30
28
  }
31
29
  }
32
-
33
- boundary_condition = self.outsideBoundaryCondition
30
+
31
+ boundary_condition = outsideBoundaryCondition
34
32
  # Skip non-outdoor surfaces
35
- return comp_infil_rate_m3_per_s unless self.outsideBoundaryCondition == 'Outdoors' || self.outsideBoundaryCondition == 'Ground'
36
-
33
+ return comp_infil_rate_m3_per_s unless outsideBoundaryCondition == 'Outdoors' || outsideBoundaryCondition == 'Ground'
34
+
37
35
  # Per area infiltration rate for this surface
38
- surface_type = self.surfaceType
36
+ surface_type = surfaceType
39
37
  infil_rate_cfm_per_ft2 = nil
40
38
  case boundary_condition
41
39
  when 'Outdoors'
@@ -59,23 +57,21 @@ class OpenStudio::Model::Surface
59
57
  end
60
58
  end
61
59
  if infil_rate_cfm_per_ft2.nil?
62
- OpenStudio::logFree(OpenStudio::Warn, "openstudio.Standards.Model", "For #{self.name}, could not determine surface type for infiltration, will not be included in calculation.")
60
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.Model', "For #{name}, could not determine surface type for infiltration, will not be included in calculation.")
63
61
  return comp_infil_rate_m3_per_s
64
62
  end
65
-
63
+
66
64
  # Area of the surface
67
- area_m2 = self.netArea
68
- area_ft2 = OpenStudio.convert(area_m2,'m^2','ft^2').get
69
-
65
+ area_m2 = netArea
66
+ area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get
67
+
70
68
  # Rate for this surface
71
69
  comp_infil_rate_cfm = area_ft2 * infil_rate_cfm_per_ft2
72
70
 
73
- comp_infil_rate_m3_per_s = OpenStudio.convert(comp_infil_rate_cfm,'cfm','m^3/s').get
71
+ comp_infil_rate_m3_per_s = OpenStudio.convert(comp_infil_rate_cfm, 'cfm', 'm^3/s').get
72
+
73
+ # OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "...#{self.name}, infil = #{comp_infil_rate_cfm.round(2)} cfm @ rate = #{infil_rate_cfm_per_ft2} cfm/ft2, area = #{area_ft2.round} ft2.")
74
74
 
75
- #OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "...#{self.name}, infil = #{comp_infil_rate_cfm.round(2)} cfm @ rate = #{infil_rate_cfm_per_ft2} cfm/ft2, area = #{area_ft2.round} ft2.")
76
-
77
75
  return comp_infil_rate_m3_per_s
78
-
79
76
  end
80
-
81
77
  end
@@ -1,7 +1,6 @@
1
1
 
2
2
  # Reopen the OpenStudio class to add methods to apply standards to this object
3
3
  class OpenStudio::Model::ThermalZone
4
-
5
4
  # Calculates the zone outdoor airflow requirement (Voz)
6
5
  # based on the inputs in the DesignSpecification:OutdoorAir obects
7
6
  # in all spaces in the zone.
@@ -9,9 +8,8 @@ class OpenStudio::Model::ThermalZone
9
8
  # @return [Double] the zone outdoor air flow rate
10
9
  # @units cubic meters per second (m^3/s)
11
10
  def outdoor_airflow_rate
12
-
13
11
  tot_oa_flow_rate = 0.0
14
-
12
+
15
13
  spaces = self.spaces.sort
16
14
 
17
15
  sum_floor_area = 0.0
@@ -27,7 +25,6 @@ class OpenStudio::Model::ThermalZone
27
25
 
28
26
  # Find common variables for the new space
29
27
  spaces.each do |space|
30
-
31
28
  floor_area = space.floorArea
32
29
  sum_floor_area += floor_area
33
30
 
@@ -40,10 +37,10 @@ class OpenStudio::Model::ThermalZone
40
37
  dsn_oa = space.designSpecificationOutdoorAir
41
38
  next if dsn_oa.empty?
42
39
  dsn_oa = dsn_oa.get
43
-
40
+
44
41
  # compute outdoor air rates in case we need them
45
42
  oa_for_people = number_of_people * dsn_oa.outdoorAirFlowperPerson
46
- oa_for_floor_area = floorArea * dsn_oa.outdoorAirFlowperFloorArea
43
+ oa_for_floor_area = floor_area * dsn_oa.outdoorAirFlowperFloorArea
47
44
  oa_rate = dsn_oa.outdoorAirFlowRate
48
45
  oa_for_volume = volume * dsn_oa.outdoorAirFlowAirChangesperHour
49
46
 
@@ -56,21 +53,19 @@ class OpenStudio::Model::ThermalZone
56
53
  sum_oa_rate += oa_rate
57
54
  sum_oa_for_volume += oa_for_volume
58
55
  end
59
-
60
56
  end
61
57
 
62
58
  tot_oa_flow_rate += sum_oa_for_people
63
59
  tot_oa_flow_rate += sum_oa_for_floor_area
64
60
  tot_oa_flow_rate += sum_oa_rate
65
61
  tot_oa_flow_rate += sum_oa_for_volume
66
-
62
+
67
63
  # Convert to cfm
68
- tot_oa_flow_rate_cfm = OpenStudio.convert(tot_oa_flow_rate,'m^3/s','cfm').get
69
-
70
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "For #{self.name}, design min OA = #{tot_oa_flow_rate_cfm.round} cfm.")
71
-
72
- return tot_oa_flow_rate
64
+ tot_oa_flow_rate_cfm = OpenStudio.convert(tot_oa_flow_rate, 'm^3/s', 'cfm').get
73
65
 
66
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Model', "For #{name}, design min OA = #{tot_oa_flow_rate_cfm.round} cfm.")
67
+
68
+ return tot_oa_flow_rate
74
69
  end
75
70
 
76
71
  # Calculates the zone outdoor airflow requirement and
@@ -78,26 +73,63 @@ class OpenStudio::Model::ThermalZone
78
73
  #
79
74
  # @return [Double] the zone outdoor air flow rate per area
80
75
  # @units cubic meters per second (m^3/s)
81
- def outdoor_airflow_rate_per_area()
82
-
76
+ def outdoor_airflow_rate_per_area
83
77
  tot_oa_flow_rate_per_area = 0.0
84
78
 
85
79
  # Find total area of the zone
86
80
  sum_floor_area = 0.0
87
- self.spaces.sort.each do |space|
81
+ spaces.sort.each do |space|
88
82
  sum_floor_area += space.floorArea
89
83
  end
90
84
 
91
85
  # Get the OA flow rate
92
86
  tot_oa_flow_rate = outdoor_airflow_rate
93
-
87
+
94
88
  # Calculate the per-area value
95
89
  tot_oa_flow_rate_per_area = tot_oa_flow_rate / sum_floor_area
96
90
 
97
91
  # OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "For #{self.name}, OA per area = #{tot_oa_flow_rate_per_area.round(8)} m^3/s*m^2.")
98
92
 
99
93
  return tot_oa_flow_rate_per_area
94
+ end
95
+
96
+ # Convert total minimum OA requirement to a per-area value.
97
+ #
98
+ # @return [Bool] true if successful, false if not
99
+ def convert_oa_req_to_per_area
100
100
 
101
+ # For each space in the zone, convert
102
+ # all design OA to per-area
103
+ spaces.each do |space|
104
+ dsn_oa = space.designSpecificationOutdoorAir
105
+ next if dsn_oa.empty?
106
+ dsn_oa = dsn_oa.get
107
+
108
+ # Get the space properties
109
+ floor_area = space.floorArea
110
+ number_of_people = space.numberOfPeople
111
+ volume = space.volume
112
+
113
+ # Sum up the total OA from all sources
114
+ oa_for_people = number_of_people * dsn_oa.outdoorAirFlowperPerson
115
+ oa_for_floor_area = floor_area * dsn_oa.outdoorAirFlowperFloorArea
116
+ oa_rate = dsn_oa.outdoorAirFlowRate
117
+ oa_for_volume = volume * dsn_oa.outdoorAirFlowAirChangesperHour
118
+ tot_oa = oa_for_people + oa_for_floor_area + oa_rate + oa_for_volume
119
+
120
+ # Convert total to per-area
121
+ tot_oa_per_area = tot_oa / floor_area
122
+
123
+ # Set the per-area requirement
124
+ dsn_oa.setOutdoorAirFlowperFloorArea(tot_oa_per_area)
125
+ # Zero-out the per-person, ACH, and flow requirements
126
+ dsn_oa.setOutdoorAirFlowperPerson(0.0)
127
+ dsn_oa.setOutdoorAirFlowAirChangesperHour(0.0)
128
+ dsn_oa.setOutdoorAirFlowRate(0.0)
129
+
130
+ end
131
+
132
+ return true
101
133
  end
102
134
 
103
135
  # This method creates a schedule where the value is zero when
@@ -109,16 +141,15 @@ class OpenStudio::Model::ThermalZone
109
141
  # @param occupied_percentage_threshold [Double] the minimum fraction (0 to 1) that counts as occupied
110
142
  # @return [ScheduleRuleset] a ScheduleRuleset where 0 = unoccupied, 1 = occupied
111
143
  # @todo Speed up this method. Bottleneck is ScheduleRule.getDaySchedules
112
- def get_occupancy_schedule(occupied_percentage_threshold = 0.05)
113
-
144
+ def get_occupancy_schedule(occupied_percentage_threshold = 0.05)
114
145
  # Get all the occupancy schedules in every space in the zone
115
146
  # Include people added via the SpaceType
116
147
  # in addition to people hard-assigned to the Space itself.
117
148
  occ_schedules_num_occ = {}
118
149
  max_occ_on_thermal_zone = 0
119
-
150
+
120
151
  # Get the people objects
121
- self.spaces.each do |space|
152
+ spaces.each do |space|
122
153
  # From the space type
123
154
  if space.spaceType.is_initialized
124
155
  space.spaceType.get.people.each do |people|
@@ -131,11 +162,10 @@ class OpenStudio::Model::ThermalZone
131
162
  num_ppl = people.getNumberOfPeople(space.floorArea)
132
163
  if occ_schedules_num_occ[num_ppl_sch].nil?
133
164
  occ_schedules_num_occ[num_ppl_sch] = num_ppl
134
- max_occ_on_thermal_zone += num_ppl
135
165
  else
136
166
  occ_schedules_num_occ[num_ppl_sch] += num_ppl
137
- max_occ_on_thermal_zone += num_ppl
138
167
  end
168
+ max_occ_on_thermal_zone += num_ppl
139
169
  end
140
170
  end
141
171
  end
@@ -150,32 +180,29 @@ class OpenStudio::Model::ThermalZone
150
180
  num_ppl = people.getNumberOfPeople(space.floorArea)
151
181
  if occ_schedules_num_occ[num_ppl_sch].nil?
152
182
  occ_schedules_num_occ[num_ppl_sch] = num_ppl
153
- max_occ_on_thermal_zone += num_ppl
154
183
  else
155
184
  occ_schedules_num_occ[num_ppl_sch] += num_ppl
156
- max_occ_on_thermal_zone += num_ppl
157
185
  end
186
+ max_occ_on_thermal_zone += num_ppl
158
187
  end
159
188
  end
160
189
  end
161
-
190
+
162
191
  # For each day of the year, determine
163
- #time_value_pairs = []
164
- year = self.model.getYearDescription
192
+ # time_value_pairs = []
193
+ year = model.getYearDescription
165
194
  yearly_data = []
166
195
  yearly_times = OpenStudio::DateTimeVector.new
167
196
  yearly_values = []
168
- for i in 1..365
169
-
197
+ (1..365).each do |i|
170
198
  times_on_this_day = []
171
199
  os_date = year.makeDate(i)
172
200
  day_of_week = os_date.dayOfWeek.valueName
173
-
201
+
174
202
  # Get the unique time indices and corresponding day schedules
175
203
  occ_schedules_day_schs = {}
176
204
  day_sch_num_occ = {}
177
205
  occ_schedules_num_occ.each do |occ_sch, num_occ|
178
-
179
206
  # Get the day schedules for this day
180
207
  # (there should only be one)
181
208
  day_schs = occ_sch.getDaySchedules(os_date, os_date)
@@ -183,7 +210,6 @@ class OpenStudio::Model::ThermalZone
183
210
  times_on_this_day << time.toString
184
211
  end
185
212
  day_sch_num_occ[day_schs[0]] = num_occ
186
-
187
213
  end
188
214
 
189
215
  # Determine the total fraction for the airloop at each time
@@ -192,7 +218,6 @@ class OpenStudio::Model::ThermalZone
192
218
  daily_values = []
193
219
  daily_occs = []
194
220
  times_on_this_day.uniq.sort.each do |time|
195
-
196
221
  os_time = OpenStudio::Time.new(time)
197
222
  os_date_time = OpenStudio::DateTime.new(os_date, os_time)
198
223
  # Total number of people at each time
@@ -201,7 +226,7 @@ class OpenStudio::Model::ThermalZone
201
226
  occ_frac = day_sch.getValue(os_time)
202
227
  tot_occ_at_time += occ_frac * num_occ
203
228
  end
204
-
229
+
205
230
  # Total fraction for the airloop at each time
206
231
  thermal_zone_occ_frac = tot_occ_at_time / max_occ_on_thermal_zone
207
232
  occ_status = 0 # unoccupied
@@ -214,36 +239,33 @@ class OpenStudio::Model::ThermalZone
214
239
  daily_os_times << os_time
215
240
  daily_values << occ_status
216
241
  daily_occs << thermal_zone_occ_frac.round(2)
217
-
218
242
  end
219
243
 
220
-
221
244
  # Simplify the daily times to eliminate intermediate
222
245
  # points with the same value as the following point.
223
246
  simple_daily_times = []
224
247
  simple_daily_os_times = []
225
248
  simple_daily_values = []
226
249
  simple_daily_occs = []
227
- daily_values.each_with_index do |value, i|
228
- next if value == daily_values[i+1]
229
- simple_daily_times << daily_times[i]
230
- simple_daily_os_times << daily_os_times[i]
231
- simple_daily_values << daily_values[i]
232
- simple_daily_occs << daily_occs[i]
250
+ daily_values.each_with_index do |value, j|
251
+ next if value == daily_values[j + 1]
252
+ simple_daily_times << daily_times[j]
253
+ simple_daily_os_times << daily_os_times[j]
254
+ simple_daily_values << daily_values[j]
255
+ simple_daily_occs << daily_occs[j]
233
256
  end
234
-
235
- # Store the daily values
236
- yearly_data << {'date'=>os_date,'day_of_week'=>day_of_week,'times'=>simple_daily_times,'values'=>simple_daily_values,'daily_os_times'=>simple_daily_os_times, 'daily_occs'=>simple_daily_occs}
237
257
 
258
+ # Store the daily values
259
+ yearly_data << { 'date' => os_date, 'day_of_week' => day_of_week, 'times' => simple_daily_times, 'values' => simple_daily_values, 'daily_os_times' => simple_daily_os_times, 'daily_occs' => simple_daily_occs }
238
260
  end
239
-
261
+
240
262
  # Create a TimeSeries from the data
241
- #time_series = OpenStudio::TimeSeries.new(times, values, 'unitless')
263
+ # time_series = OpenStudio::TimeSeries.new(times, values, 'unitless')
242
264
 
243
265
  # Make a schedule ruleset
244
- sch_name = "#{self.name} Occ Sch"
245
- sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(self.model)
246
- sch_ruleset.setName("#{sch_name}")
266
+ sch_name = "#{name} Occ Sch"
267
+ sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
268
+ sch_ruleset.setName(sch_name.to_s)
247
269
 
248
270
  # Default - All Occupied
249
271
  day_sch = sch_ruleset.defaultDaySchedule
@@ -251,84 +273,81 @@ class OpenStudio::Model::ThermalZone
251
273
  day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1)
252
274
 
253
275
  # Winter Design Day - All Occupied
254
- day_sch = OpenStudio::Model::ScheduleDay.new(self.model)
276
+ day_sch = OpenStudio::Model::ScheduleDay.new(model)
255
277
  sch_ruleset.setWinterDesignDaySchedule(day_sch)
256
278
  day_sch = sch_ruleset.winterDesignDaySchedule
257
279
  day_sch.setName("#{sch_name} Winter Design Day")
258
- day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1)
280
+ day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1)
259
281
 
260
282
  # Summer Design Day - All Occupied
261
- day_sch = OpenStudio::Model::ScheduleDay.new(self.model)
283
+ day_sch = OpenStudio::Model::ScheduleDay.new(model)
262
284
  sch_ruleset.setSummerDesignDaySchedule(day_sch)
263
285
  day_sch = sch_ruleset.summerDesignDaySchedule
264
286
  day_sch.setName("#{sch_name} Summer Design Day")
265
287
  day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1)
266
-
288
+
267
289
  # Create ruleset schedules, attempting to create
268
290
  # the minimum number of unique rules.
269
- ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'].each do |day_of_week|
291
+ ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].each do |weekday|
270
292
  end_of_prev_rule = yearly_data[0]['date']
271
- yearly_data.each_with_index do |daily_data, i|
293
+ yearly_data.each_with_index do |daily_data, k|
272
294
  # Skip unless it is the day of week
273
295
  # currently under inspection
274
296
  day = daily_data['day_of_week']
275
- next unless day == day_of_week
297
+ next unless day == weekday
276
298
  date = daily_data['date']
277
299
  times = daily_data['times']
278
300
  values = daily_data['values']
279
301
  daily_occs = daily_data['daily_occs']
280
-
302
+
281
303
  # If the next (Monday, Tuesday, etc.)
282
304
  # is the same as today, keep going.
283
305
  # If the next is different, or if
284
306
  # we've reached the end of the year,
285
307
  # create a new rule
286
- if !yearly_data[i+7].nil?
287
- next_day_times = yearly_data[i+7]['times']
288
- next_day_values = yearly_data[i+7]['values']
308
+ unless yearly_data[k + 7].nil?
309
+ next_day_times = yearly_data[k + 7]['times']
310
+ next_day_values = yearly_data[k + 7]['values']
289
311
  next if times == next_day_times && values == next_day_values
290
312
  end
291
-
313
+
292
314
  daily_os_times = daily_data['daily_os_times']
293
315
  daily_occs = daily_data['daily_occs']
294
-
316
+
295
317
  # If here, we need to make a rule to cover from the previous
296
318
  # rule to today
297
-
319
+
298
320
  sch_rule = OpenStudio::Model::ScheduleRule.new(sch_ruleset)
299
- sch_rule.setName("#{sch_name} #{day_of_week} Rule")
321
+ sch_rule.setName("#{sch_name} #{weekday} Rule")
300
322
  day_sch = sch_rule.daySchedule
301
- day_sch.setName("#{sch_name} #{day_of_week}")
302
- daily_os_times.each_with_index do |time, i|
303
- value = values[i]
304
- next if value == values[i+1] # Don't add breaks if same value
323
+ day_sch.setName("#{sch_name} #{weekday}")
324
+ daily_os_times.each_with_index do |time, l|
325
+ value = values[l]
326
+ next if value == values[l + 1] # Don't add breaks if same value
305
327
  day_sch.addValue(time, value)
306
328
  end
307
-
329
+
308
330
  # Set the dates when the rule applies
309
331
  sch_rule.setStartDate(end_of_prev_rule)
310
332
  sch_rule.setEndDate(date)
311
333
 
312
334
  # Individual Days
313
- sch_rule.setApplyMonday(true) if day_of_week == 'Monday'
314
- sch_rule.setApplyTuesday(true) if day_of_week == 'Tuesday'
315
- sch_rule.setApplyWednesday(true) if day_of_week == 'Wednesday'
316
- sch_rule.setApplyThursday(true) if day_of_week == 'Thursday'
317
- sch_rule.setApplyFriday(true) if day_of_week == 'Friday'
318
- sch_rule.setApplySaturday(true) if day_of_week == 'Saturday'
319
- sch_rule.setApplySunday(true) if day_of_week == 'Sunday'
320
-
335
+ sch_rule.setApplyMonday(true) if weekday == 'Monday'
336
+ sch_rule.setApplyTuesday(true) if weekday == 'Tuesday'
337
+ sch_rule.setApplyWednesday(true) if weekday == 'Wednesday'
338
+ sch_rule.setApplyThursday(true) if weekday == 'Thursday'
339
+ sch_rule.setApplyFriday(true) if weekday == 'Friday'
340
+ sch_rule.setApplySaturday(true) if weekday == 'Saturday'
341
+ sch_rule.setApplySunday(true) if weekday == 'Sunday'
342
+
321
343
  # Reset the previous rule end date
322
344
  end_of_prev_rule = date + OpenStudio::Time.new(0, 24, 0, 0)
323
-
324
345
  end
325
-
326
346
  end
327
347
 
328
348
  return sch_ruleset
329
-
330
- end
331
-
349
+ end
350
+
332
351
  # Determine if the thermal zone is residential based on the
333
352
  # space type properties for the spaces in the zone.
334
353
  # If there are both residential and nonresidential spaces
@@ -337,52 +356,49 @@ class OpenStudio::Model::ThermalZone
337
356
  # it will be assumed nonresidential.
338
357
  #
339
358
  # return [Bool] true if residential, false if nonresidential
340
- def is_residential(standard)
341
-
359
+ def residential?(template)
342
360
  # Determine the respective areas
343
361
  res_area_m2 = 0
344
362
  nonres_area_m2 = 0
345
- self.spaces.each do |space|
363
+ spaces.each do |space|
346
364
  # Ignore space if not part of total area
347
- next if !space.partofTotalFloorArea
348
- if space.is_residential(standard)
365
+ next unless space.partofTotalFloorArea
366
+ if space.residential?(template)
349
367
  res_area_m2 += space.floorArea
350
368
  else
351
369
  nonres_area_m2 += space.floorArea
352
370
  end
353
371
  end
354
-
372
+
355
373
  # Determine which is larger
356
374
  is_res = false
357
375
  if res_area_m2 > nonres_area_m2
358
376
  is_res = true
359
- end
377
+ end
360
378
 
361
379
  return is_res
362
-
363
- end
364
-
365
- # Determine if the thermal zone is a Fossil Fuel,
380
+ end
381
+
382
+ # Determine if the thermal zone is a Fossil Fuel,
366
383
  # Fossil/Electric Hybrid, and Purchased Heat zone.
367
384
  # If not, it is an Electric or Other Zone.
368
385
  # This is as-defined by 90.1 Appendix G.
369
386
  #
370
- # return [Bool] true if Fossil Fuel,
371
- # Fossil/Electric Hybrid, and Purchased Heat zone,
387
+ # return [Bool] true if Fossil Fuel,
388
+ # Fossil/Electric Hybrid, and Purchased Heat zone,
372
389
  # false if Electric or Other.
373
390
  # To-do: It's not doing it properly right now. If you have a zone with a VRF + a DOAS (via an ATU SingleDUct Uncontrolled)
374
391
  # it'll pick up both natural gas and electricity and classify it as fossil fuel, when I would definitely classify it as electricity
375
- def is_fossil_hybrid_or_purchased_heat
376
-
392
+ def fossil_hybrid_or_purchased_heat?
377
393
  is_fossil = false
378
-
394
+
379
395
  # Get an array of the heating fuels
380
396
  # used by the zone. Possible values are
381
397
  # Electricity, NaturalGas, PropaneGas, FuelOil#1, FuelOil#2,
382
- # Coal, Diesel, Gasoline, DistrictHeating,
398
+ # Coal, Diesel, Gasoline, DistrictHeating,
383
399
  # and SolarEnergy.
384
- htg_fuels = self.heating_fuels
385
-
400
+ htg_fuels = heating_fuels
401
+
386
402
  if htg_fuels.include?('NaturalGas') ||
387
403
  htg_fuels.include?('PropaneGas') ||
388
404
  htg_fuels.include?('FuelOil#1') ||
@@ -391,46 +407,167 @@ class OpenStudio::Model::ThermalZone
391
407
  htg_fuels.include?('Diesel') ||
392
408
  htg_fuels.include?('Gasoline') ||
393
409
  htg_fuels.include?('DistrictHeating')
394
-
410
+
395
411
  is_fossil = true
396
412
  end
397
-
398
- #OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "For #{self.name}, heating fuels = #{htg_fuels.join(', ')}; is_fossil_hybrid_or_purchased_heat = #{is_fossil}.")
399
-
413
+
414
+ # OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.Model", "For #{self.name}, heating fuels = #{htg_fuels.join(', ')}; fossil_hybrid_or_purchased_heat? = #{is_fossil}.")
415
+
400
416
  return is_fossil
401
-
402
417
  end
403
-
418
+
419
+ # Determine if the thermal zone's fuel type category.
420
+ # Options are:
421
+ # fossil, electric, unconditioned
422
+ # If a customization is passed, additional categories may
423
+ # be returned.
424
+ # If 'Xcel Energy CO EDA', the type fossilandelectric is added.
425
+ # DistrictHeating is considered a fossil fuel since it is
426
+ # typically created by natural gas boilers.
427
+ #
428
+ # @return [String] the fuel type category
429
+ def fossil_or_electric_type(custom)
430
+ fossil = false
431
+ electric = false
432
+
433
+ # Fossil heating
434
+ htg_fuels = heating_fuels
435
+ if htg_fuels.include?('NaturalGas') ||
436
+ htg_fuels.include?('PropaneGas') ||
437
+ htg_fuels.include?('FuelOil#1') ||
438
+ htg_fuels.include?('FuelOil#2') ||
439
+ htg_fuels.include?('Coal') ||
440
+ htg_fuels.include?('Diesel') ||
441
+ htg_fuels.include?('Gasoline') ||
442
+ htg_fuels.include?('DistrictHeating')
443
+ fossil = true
444
+ end
445
+
446
+ # Electric heating
447
+ if htg_fuels.include?('Electricity')
448
+ electric = true
449
+ end
450
+
451
+ # Cooling fuels, for determining
452
+ # unconditioned zones
453
+ clg_fuels = cooling_fuels
454
+
455
+ # Categorize
456
+ fuel_type = nil
457
+ if fossil
458
+ # If uses any fossil, counts as fossil even if electric is present too
459
+ fuel_type = 'fossil'
460
+ elsif electric
461
+ fuel_type = 'electric'
462
+ elsif htg_fuels.size.zero? && clg_fuels.size.zero?
463
+ fuel_type = 'unconditioned'
464
+ else
465
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.Model', "For #{name}, could not determine fuel type, assuming fossil. Heating fuels = #{htg_fuels.join(', ')}; cooling fuels = #{clg_fuels.join(', ')}.")
466
+ fuel_type = 'fossil'
467
+ end
468
+
469
+ # Customization for Xcel.
470
+ # Likely useful for other utility
471
+ # programs where fuel switching is important.
472
+ # This is primarily for systems where Gas is
473
+ # used at the central AHU and electric is
474
+ # used at the terminals/zones. Examples
475
+ # include zone VRF/PTHP with gas-heated DOAS,
476
+ # and gas VAV with electric reheat
477
+ case custom
478
+ when 'Xcel Energy CO EDA'
479
+ if fossil && electric
480
+ fuel_type = 'fossilandelectric'
481
+ end
482
+ end
483
+
484
+ # OpenStudio::logFree(OpenStudio::Info, "openstudio.Standards.Model", "For #{self.name}, fuel type = #{fuel_type}.")
485
+
486
+ return fuel_type
487
+ end
488
+
489
+ # Determine if the thermal zone is
490
+ # Fossil/Purchased Heat/Electric Hybrid
491
+ #
492
+ # return [Bool] true if mixed
493
+ # Fossil/Electric Hybrid, and Purchased Heat zone
494
+ def mixed_heating_fuel?
495
+ is_mixed = false
496
+
497
+ # Get an array of the heating fuels
498
+ # used by the zone. Possible values are
499
+ # Electricity, NaturalGas, PropaneGas, FuelOil#1, FuelOil#2,
500
+ # Coal, Diesel, Gasoline, DistrictHeating,
501
+ # and SolarEnergy.
502
+ htg_fuels = heating_fuels
503
+
504
+ # Includes fossil
505
+ fossil = false
506
+ if htg_fuels.include?('NaturalGas') ||
507
+ htg_fuels.include?('PropaneGas') ||
508
+ htg_fuels.include?('FuelOil#1') ||
509
+ htg_fuels.include?('FuelOil#2') ||
510
+ htg_fuels.include?('Coal') ||
511
+ htg_fuels.include?('Diesel') ||
512
+ htg_fuels.include?('Gasoline')
513
+
514
+ fossil = true
515
+ end
516
+
517
+ # Electric and fossil and district
518
+ if htg_fuels.include?('Electricity') && htg_fuels.include?('DistrictHeating') && fossil
519
+ is_mixed = true
520
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Model', "For #{name}, heating mixed electricity, fossil, and district.")
521
+ end
522
+
523
+ # Electric and fossil
524
+ if htg_fuels.include?('Electricity') && fossil
525
+ is_mixed = true
526
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Model', "For #{name}, heating mixed electricity and fossil.")
527
+ end
528
+
529
+ # Electric and district
530
+ if htg_fuels.include?('Electricity') && htg_fuels.include?('DistrictHeating')
531
+ is_mixed = true
532
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Model', "For #{name}, heating mixed electricity and district.")
533
+ end
534
+
535
+ # Fossil and district
536
+ if fossil && htg_fuels.include?('DistrictHeating')
537
+ is_mixed = true
538
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.Model', "For #{name}, heating mixed fossil and district.")
539
+ end
540
+
541
+ return is_mixed
542
+ end
543
+
404
544
  # Determine the net area of the zone
405
545
  # Loops on each space, and checks if part of total floor area or not
406
546
  # If not part of total floor area, it is not added to the zone floor area
407
547
  # Will multiply it by the ZONE MULTIPLIER as well!
408
548
  #
409
549
  # @return [Double] the zone net floor area in m^2 (with multiplier taken into account)
410
- # @units square meters (m^2)
411
- def get_net_area
550
+ def floor_area_with_zone_multipliers
412
551
  area_m2 = 0
413
- zone_mult = self.multiplier
414
- self.spaces.each do |space|
552
+ zone_mult = multiplier
553
+ spaces.each do |space|
415
554
  # If space is not part of floor area, we don't add it
416
- next if !space.partofTotalFloorArea
555
+ next unless space.partofTotalFloorArea
417
556
  area_m2 += space.floorArea
418
557
  end
419
-
558
+
420
559
  return area_m2 * zone_mult
421
-
422
560
  end
423
-
561
+
424
562
  # Infers the baseline system type based on the equipment
425
563
  # serving the zone and their heating/cooling fuels.
426
564
  # Only does a high-level inference; does not look for the
427
565
  # presence/absence of required controls, etc.
428
566
  #
429
- # @return [String] Possible system types are
430
- # PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes,
567
+ # @return [String] Possible system types are
568
+ # PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes,
431
569
  # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace
432
570
  def infer_system_type
433
-
434
571
  # Determine the characteristics
435
572
  # of the equipment serving the zone
436
573
  has_air_loop = false
@@ -440,7 +577,7 @@ class OpenStudio::Model::ThermalZone
440
577
  has_ptac = false
441
578
  has_pthp = false
442
579
  has_unitheater = false
443
- self.equipment.each do |equip|
580
+ equipment.each do |equip|
444
581
  # Skip HVAC components
445
582
  next unless equip.to_HVACComponent.is_initialized
446
583
  equip = equip.to_HVACComponent.get
@@ -463,15 +600,15 @@ class OpenStudio::Model::ThermalZone
463
600
  has_unitheater = true
464
601
  end
465
602
  end
466
-
603
+
467
604
  # Get the zone heating and cooling fuels
468
- htg_fuels = self.heating_fuels
469
- clg_fuels = self.cooling_fuels
470
- is_fossil = self.is_fossil_hybrid_or_purchased_heat
471
-
605
+ htg_fuels = heating_fuels
606
+ clg_fuels = cooling_fuels
607
+ is_fossil = fossil_hybrid_or_purchased_heat?
608
+
472
609
  # Infer the HVAC type
473
610
  sys_type = 'Unknown'
474
-
611
+
475
612
  # Single zone
476
613
  if air_loop_num_zones < 2
477
614
  # Gas
@@ -479,12 +616,12 @@ class OpenStudio::Model::ThermalZone
479
616
  # Air Loop
480
617
  if has_air_loop
481
618
  # Gas_Furnace (as air loop)
482
- if cooling_fuels.size == 0
483
- sys_type = 'Gas_Furnace'
484
- # PSZ_AC
485
- else
486
- sys_type = 'PSZ_AC'
487
- end
619
+ sys_type = if cooling_fuels.size.zero?
620
+ 'Gas_Furnace'
621
+ # PSZ_AC
622
+ else
623
+ 'PSZ_AC'
624
+ end
488
625
  # Zone Equipment
489
626
  else
490
627
  # Gas_Furnace (as unit heater)
@@ -495,18 +632,18 @@ class OpenStudio::Model::ThermalZone
495
632
  if has_ptac
496
633
  sys_type = 'PTAC'
497
634
  end
498
- end
635
+ end
499
636
  # Electric
500
637
  else
501
638
  # Air Loop
502
639
  if has_air_loop
503
640
  # Electric_Furnace (as air loop)
504
- if cooling_fuels.size == 0
505
- sys_type = 'Electric_Furnace'
506
- # PSZ_HP
507
- else
508
- sys_type = 'PSZ_HP'
509
- end
641
+ sys_type = if cooling_fuels.size.zero?
642
+ 'Electric_Furnace'
643
+ # PSZ_HP
644
+ else
645
+ 'PSZ_HP'
646
+ end
510
647
  # Zone Equipment
511
648
  else
512
649
  # Electric_Furnace (as unit heater)
@@ -520,7 +657,7 @@ class OpenStudio::Model::ThermalZone
520
657
  end
521
658
  end
522
659
  # Multi-zone
523
- else
660
+ else
524
661
  # Gas
525
662
  if is_fossil
526
663
  # VAV_Reheat
@@ -543,27 +680,615 @@ class OpenStudio::Model::ThermalZone
543
680
  end
544
681
  end
545
682
  end
546
-
683
+
547
684
  # Report out the characteristics for debugging if
548
685
  # the system type cannot be inferred.
549
686
  if sys_type == 'Unknown'
550
- OpenStudio::logFree(OpenStudio::Warn, "openstudio.Standards.ThermalZone", "For #{self.name}, the baseline system type could not be inferred.")
551
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "***#{self.name}***")
552
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "system type = #{sys_type}")
553
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "has_air_loop = #{has_air_loop}")
554
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "air_loop_num_zones = #{air_loop_num_zones}")
555
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "air_loop_is_vav = #{air_loop_is_vav}")
556
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "air_loop_has_chw = #{air_loop_has_chw}")
557
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "has_ptac = #{has_ptac}")
558
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "has_pthp = #{has_pthp}")
559
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "has_unitheater = #{has_unitheater}")
560
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "htg_fuels = #{htg_fuels}")
561
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "clg_fuels = #{clg_fuels}")
562
- OpenStudio::logFree(OpenStudio::Debug, "openstudio.Standards.ThermalZone", "is_fossil = #{is_fossil}")
563
- end
564
-
687
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.ThermalZone', "For #{name}, the baseline system type could not be inferred.")
688
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "***#{name}***")
689
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "system type = #{sys_type}")
690
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "has_air_loop = #{has_air_loop}")
691
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "air_loop_num_zones = #{air_loop_num_zones}")
692
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "air_loop_is_vav = #{air_loop_is_vav}")
693
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "air_loop_has_chw = #{air_loop_has_chw}")
694
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "has_ptac = #{has_ptac}")
695
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "has_pthp = #{has_pthp}")
696
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "has_unitheater = #{has_unitheater}")
697
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "htg_fuels = #{htg_fuels}")
698
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "clg_fuels = #{clg_fuels}")
699
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "is_fossil = #{is_fossil}")
700
+ end
701
+
565
702
  return sys_type
703
+ end
704
+
705
+ # Determines heating status. If the zone has a thermostat
706
+ # with a maximum heating setpoint above 5C (41F),
707
+ # counts as heated. Plenums are also assumed to be heated.
708
+ #
709
+ # @author Andrew Parker, Julien Marrec
710
+ # @return [Bool] true if heated, false if not
711
+ def heated?
712
+ temp_f = 41
713
+ temp_c = OpenStudio.convert(temp_f, 'F', 'C').get
714
+
715
+ htd = false
716
+
717
+ # Consider plenum zones heated
718
+ area_plenum = 0
719
+ area_non_plenum = 0
720
+ spaces.each do |space|
721
+ if space.plenum?
722
+ area_plenum += space.floorArea
723
+ else
724
+ area_non_plenum += space.floorArea
725
+ end
726
+ end
727
+
728
+ # Majority
729
+ if area_plenum > area_non_plenum
730
+ htd = true
731
+ return htd
732
+ end
733
+
734
+ # Unheated if no thermostat present
735
+ if thermostat.empty?
736
+ return htd
737
+ end
738
+
739
+ # Check the heating setpoint
740
+ tstat = thermostat.get
741
+ if tstat.to_ThermostatSetpointDualSetpoint
742
+ tstat = tstat.to_ThermostatSetpointDualSetpoint.get
743
+ htg_sch = tstat.getHeatingSchedule
744
+ if htg_sch.is_initialized
745
+ htg_sch = htg_sch.get
746
+ if htg_sch.to_ScheduleRuleset.is_initialized
747
+ htg_sch = htg_sch.to_ScheduleRuleset.get
748
+ max_c = htg_sch.annual_min_max_value['max']
749
+ if max_c > temp_c
750
+ htd = true
751
+ end
752
+ elsif htg_sch.to_ScheduleConstant.is_initialized
753
+ htg_sch = htg_sch.to_ScheduleConstant.get
754
+ max_c = htg_sch.annual_min_max_value['max']
755
+ if max_c > temp_c
756
+ htd = true
757
+ end
758
+ elsif htg_sch.to_ScheduleCompact.is_initialized
759
+ htg_sch = htg_sch.to_ScheduleCompact.get
760
+ max_c = htg_sch.annual_min_max_value['max']
761
+ if max_c > temp_c
762
+ htd = true
763
+ end
764
+ else
765
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "Zone #{name} used an unknown schedule type for the heating setpoint; assuming heated.")
766
+ htd = true
767
+ end
768
+ end
769
+ elsif tstat.to_ZoneControlThermostatStagedDualSetpoint
770
+ tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get
771
+ htg_sch = tstat.heatingTemperatureSetpointSchedule
772
+ if htg_sch.is_initialized
773
+ htg_sch = htg_sch.get
774
+ if htg_sch.to_ScheduleRuleset.is_initialized
775
+ htg_sch = htg_sch.to_ScheduleRuleset.get
776
+ max_c = htg_sch.annual_min_max_value['max']
777
+ if max_c > temp_c
778
+ htd = true
779
+ end
780
+ end
781
+ end
782
+ end
783
+
784
+ return htd
785
+ end
786
+
787
+ # Determines cooling status. If the zone has a thermostat
788
+ # with a minimum cooling setpoint below 33C (91F),
789
+ # counts as cooled. Plenums are also assumed to be cooled.
790
+ #
791
+ # @author Andrew Parker, Julien Marrec
792
+ # @return [Bool] true if cooled, false if not
793
+ def cooled?
794
+ temp_f = 91
795
+ temp_c = OpenStudio.convert(temp_f, 'F', 'C').get
796
+
797
+ cld = false
798
+
799
+ # Consider plenum zones cooled
800
+ area_plenum = 0
801
+ area_non_plenum = 0
802
+ spaces.each do |space|
803
+ if space.plenum?
804
+ area_plenum += space.floorArea
805
+ else
806
+ area_non_plenum += space.floorArea
807
+ end
808
+ end
809
+
810
+ # Majority
811
+ if area_plenum > area_non_plenum
812
+ cld = true
813
+ return cld
814
+ end
815
+
816
+ # Unheated if no thermostat present
817
+ if thermostat.empty?
818
+ return cld
819
+ end
820
+
821
+ # Check the cooling setpoint
822
+ tstat = thermostat.get
823
+ if tstat.to_ThermostatSetpointDualSetpoint
824
+ tstat = tstat.to_ThermostatSetpointDualSetpoint.get
825
+ clg_sch = tstat.getCoolingSchedule
826
+ if clg_sch.is_initialized
827
+ clg_sch = clg_sch.get
828
+ if clg_sch.to_ScheduleRuleset.is_initialized
829
+ clg_sch = clg_sch.to_ScheduleRuleset.get
830
+ min_c = clg_sch.annual_min_max_value['min']
831
+ if min_c < temp_c
832
+ cld = true
833
+ end
834
+ elsif clg_sch.to_ScheduleConstant.is_initialized
835
+ clg_sch = clg_sch.to_ScheduleConstant.get
836
+ min_c = clg_sch.annual_min_max_value['min']
837
+ if min_c < temp_c
838
+ cld = true
839
+ end
840
+ elsif clg_sch.to_ScheduleCompact.is_initialized
841
+ clg_sch = clg_sch.to_ScheduleCompact.get
842
+ min_c = clg_sch.annual_min_max_value['min']
843
+ if min_c < temp_c
844
+ cld = true
845
+ end
846
+ else
847
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "Zone #{name} used an unknown schedule type for the cooling setpoint; assuming cooled.")
848
+ cld = true
849
+ end
850
+ end
851
+ elsif tstat.to_ZoneControlThermostatStagedDualSetpoint
852
+ tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get
853
+ clg_sch = tstat.coolingTemperatureSetpointSchedule
854
+ if clg_sch.is_initialized
855
+ clg_sch = clg_sch.get
856
+ if clg_sch.to_ScheduleRuleset.is_initialized
857
+ clg_sch = clg_sch.to_ScheduleRuleset.get
858
+ min_c = clg_sch.annual_min_max_value['min']
859
+ if min_c < temp_c
860
+ cld = true
861
+ end
862
+ end
863
+ end
864
+ end
865
+
866
+ return cld
867
+ end
868
+
869
+ # Determine if the thermal zone is a plenum
870
+ # based on whether a majority of the spaces
871
+ # in the zone are plenums or not.
872
+ # @return [Bool] true if majority plenum, false if not
873
+ def plenum?
874
+ plenum_status = false
875
+
876
+ area_plenum = 0
877
+ area_non_plenum = 0
878
+ spaces.each do |space|
879
+ if space.plenum?
880
+ area_plenum += space.floorArea
881
+ else
882
+ area_non_plenum += space.floorArea
883
+ end
884
+ end
885
+
886
+ # Majority
887
+ if area_plenum > area_non_plenum
888
+ plenum_status = true
889
+ end
890
+
891
+ return plenum_status
892
+ end
893
+
894
+ # Determines whether the zone is conditioned per 90.1,
895
+ # which is based on heating and cooling loads.
896
+ #
897
+ # @param climate_zone [String] climate zone
898
+ # @return [String] NonResConditioned, ResConditioned, Semiheated, Unconditioned
899
+ # @todo add logic to detect indirectly-conditioned spaces
900
+ def conditioning_category(template, climate_zone)
901
+ # Get the heating load
902
+ htg_load_btu_per_ft2 = 0.0
903
+ htg_load_w_per_m2 = heatingDesignLoad
904
+ if htg_load_w_per_m2.is_initialized
905
+ htg_load_btu_per_ft2 = OpenStudio.convert(htg_load_w_per_m2.get, 'W/m^2', 'Btu/hr*ft^2').get
906
+ end
907
+
908
+ # Get the cooling load
909
+ clg_load_btu_per_ft2 = 0.0
910
+ clg_load_w_per_m2 = coolingDesignLoad
911
+ if clg_load_w_per_m2.is_initialized
912
+ clg_load_btu_per_ft2 = OpenStudio.convert(clg_load_w_per_m2.get, 'W/m^2', 'Btu/hr*ft^2').get
913
+ end
914
+
915
+ # Determine the heating limit based on climate zone
916
+ # From Table 3.1 Heated Space Criteria
917
+ htg_lim_btu_per_ft2 = 0.0
918
+ case climate_zone
919
+ when 'ASHRAE 169-2006-1A',
920
+ 'ASHRAE 169-2006-1B',
921
+ 'ASHRAE 169-2006-2A',
922
+ 'ASHRAE 169-2006-2B'
923
+ htg_lim_btu_per_ft2 = 5
924
+ when 'ASHRAE 169-2006-3A',
925
+ 'ASHRAE 169-2006-3B',
926
+ 'ASHRAE 169-2006-3C'
927
+ htg_lim_btu_per_ft2 = 10
928
+ when 'ASHRAE 169-2006-4A',
929
+ 'ASHRAE 169-2006-4B',
930
+ 'ASHRAE 169-2006-4C',
931
+ 'ASHRAE 169-2006-5A',
932
+ 'ASHRAE 169-2006-5B',
933
+ 'ASHRAE 169-2006-5C',
934
+ htg_lim_btu_per_ft2 = 15
935
+ when 'ASHRAE 169-2006-6A',
936
+ 'ASHRAE 169-2006-6B',
937
+ 'ASHRAE 169-2006-7A',
938
+ 'ASHRAE 169-2006-7B',
939
+ htg_lim_btu_per_ft2 = 20
940
+ when
941
+ 'ASHRAE 169-2006-8A',
942
+ 'ASHRAE 169-2006-8B'
943
+ htg_lim_btu_per_ft2 = 25
944
+ end
945
+
946
+ # Cooling limit is climate-independent
947
+ clg_lim_btu_per_ft2 = 5
948
+
949
+ # Semiheated limit is climate-independent
950
+ semihtd_lim_btu_per_ft2 = 3.4
951
+
952
+ # Determine if residential
953
+ res = false
954
+ if residential?(template)
955
+ res = true
956
+ end
957
+
958
+ cond_cat = 'Unconditioned'
959
+ if htg_load_btu_per_ft2 > htg_lim_btu_per_ft2
960
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "Zone #{name} is conditioned because heating load of #{htg_load_btu_per_ft2.round} Btu/hr*ft^2 exceeds minimum of #{htg_lim_btu_per_ft2.round} Btu/hr*ft^2.")
961
+ cond_cat = if res
962
+ 'ResConditioned'
963
+ else
964
+ 'NonResConditioned'
965
+ end
966
+ elsif clg_load_btu_per_ft2 > clg_lim_btu_per_ft2
967
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "Zone #{name} is conditioned because cooling load of #{clg_load_btu_per_ft2.round} Btu/hr*ft^2 exceeds minimum of #{clg_lim_btu_per_ft2.round} Btu/hr*ft^2.")
968
+ cond_cat = if res
969
+ 'ResConditioned'
970
+ else
971
+ 'NonResConditioned'
972
+ end
973
+ elsif htg_load_btu_per_ft2 > semihtd_lim_btu_per_ft2
974
+ cond_cat = 'Semiheated'
975
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "Zone #{name} is semiheated because heating load of #{htg_load_btu_per_ft2.round} Btu/hr*ft^2 exceeds minimum of #{semihtd_lim_btu_per_ft2.round} Btu/hr*ft^2.")
976
+ end
977
+
978
+ return cond_cat
979
+ end
980
+
981
+ # Calculate the heating supply temperature based on the
982
+ # specified delta-T. Delta-T is calculated based on the
983
+ # highest value found in the heating setpoint schedule.
984
+ #
985
+ # @return [Double] the design heating supply temperature, in C
986
+ # @todo Exception: 17F delta-T for labs
987
+ def prm_baseline_heating_design_supply_temperature
988
+ setpoint_c = nil
989
+
990
+ # Setpoint schedule
991
+ tstat = thermostatSetpointDualSetpoint
992
+ if tstat.is_initialized
993
+ tstat = tstat.get
994
+ setpoint_sch = tstat.heatingSetpointTemperatureSchedule
995
+ if setpoint_sch.is_initialized
996
+ setpoint_sch = setpoint_sch.get
997
+ if setpoint_sch.to_ScheduleRuleset.is_initialized
998
+ setpoint_sch = setpoint_sch.to_ScheduleRuleset.get
999
+ setpoint_c = setpoint_sch.annual_min_max_value['max']
1000
+ elsif setpoint_sch.to_ScheduleConstant.is_initialized
1001
+ setpoint_sch = setpoint_sch.to_ScheduleConstant.get
1002
+ setpoint_c = setpoint_sch.annual_min_max_value['max']
1003
+ elsif setpoint_sch.to_ScheduleCompact.is_initialized
1004
+ setpoint_sch = setpoint_sch.to_ScheduleCompact.get
1005
+ setpoint_c = setpoint_sch.annual_min_max_value['max']
1006
+ end
1007
+ end
1008
+ end
1009
+
1010
+ # If the heating setpoint could not be determined
1011
+ # return the current design heating temperature
1012
+ if setpoint_c.nil?
1013
+ setpoint_c = sizingZone.zoneHeatingDesignSupplyAirTemperature
1014
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.ThermalZone', "For #{name}: could not determine max heating setpoint. Design heating SAT will be #{OpenStudio.convert(setpoint_c, 'C', 'F').get.round} F from proposed model.")
1015
+ return setpoint_c
1016
+ end
1017
+
1018
+ # If the heating setpoint was set very low so that
1019
+ # heating equipment never comes on
1020
+ # return the current design heating temperature
1021
+ if setpoint_c < OpenStudio.convert(41, 'F', 'C').get
1022
+ setpoint_f = OpenStudio.convert(setpoint_c, 'C', 'F').get
1023
+ new_setpoint_c = sizingZone.zoneHeatingDesignSupplyAirTemperature
1024
+ new_setpoint_f = OpenStudio.convert(new_setpoint_c, 'C', 'F').get
1025
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.ThermalZone', "For #{name}: max heating setpoint in proposed model was #{setpoint_f.round} F. 20 F SAT delta-T from this point is unreasonable. Design heating SAT will be #{new_setpoint_f.round} F from proposed model.")
1026
+ return new_setpoint_c
1027
+ end
1028
+
1029
+ # Add 20F delta-T
1030
+ delta_t_r = 20
1031
+ delta_t_k = OpenStudio.convert(delta_t_r, 'R', 'K').get
1032
+
1033
+ sat_c = setpoint_c + delta_t_k # Add for heating
566
1034
 
1035
+ return sat_c
567
1036
  end
568
1037
 
1038
+ # Calculate the cooling supply temperature based on the
1039
+ # specified delta-T. Delta-T is calculated based on the
1040
+ # highest value found in the cooling setpoint schedule.
1041
+ #
1042
+ # @return [Double] the design heating supply temperature, in C
1043
+ # @todo Exception: 17F delta-T for labs
1044
+ def prm_baseline_cooling_design_supply_temperature
1045
+ setpoint_c = nil
1046
+
1047
+ # Setpoint schedule
1048
+ tstat = thermostatSetpointDualSetpoint
1049
+ if tstat.is_initialized
1050
+ tstat = tstat.get
1051
+ setpoint_sch = tstat.coolingSetpointTemperatureSchedule
1052
+ if setpoint_sch.is_initialized
1053
+ setpoint_sch = setpoint_sch.get
1054
+ if setpoint_sch.to_ScheduleRuleset.is_initialized
1055
+ setpoint_sch = setpoint_sch.to_ScheduleRuleset.get
1056
+ setpoint_c = setpoint_sch.annual_min_max_value['min']
1057
+ elsif setpoint_sch.to_ScheduleConstant.is_initialized
1058
+ setpoint_sch = setpoint_sch.to_ScheduleConstant.get
1059
+ setpoint_c = setpoint_sch.annual_min_max_value['min']
1060
+ elsif setpoint_sch.to_ScheduleCompact.is_initialized
1061
+ setpoint_sch = setpoint_sch.to_ScheduleCompact.get
1062
+ setpoint_c = setpoint_sch.annual_min_max_value['min']
1063
+ end
1064
+ end
1065
+ end
1066
+
1067
+ # If the cooling setpoint could not be determined
1068
+ # return the current design cooling temperature
1069
+ if setpoint_c.nil?
1070
+ setpoint_c = sizingZone.zoneCoolingDesignSupplyAirTemperature
1071
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.ThermalZone', "For #{name}: could not determine min cooling setpoint. Design cooling SAT will be #{OpenStudio.convert(setpoint_c, 'C', 'F').get.round} F from proposed model.")
1072
+ return setpoint_c
1073
+ end
1074
+
1075
+ # If the cooling setpoint was set very high so that
1076
+ # cooling equipment never comes on
1077
+ # return the current design cooling temperature
1078
+ if setpoint_c > OpenStudio.convert(91, 'F', 'C').get
1079
+ setpoint_f = OpenStudio.convert(setpoint_c, 'C', 'F').get
1080
+ new_setpoint_c = sizingZone.zoneCoolingDesignSupplyAirTemperature
1081
+ new_setpoint_f = OpenStudio.convert(new_setpoint_c, 'C', 'F').get
1082
+ OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.ThermalZone', "For #{name}: max cooling setpoint in proposed model was #{setpoint_f.round} F. 20 F SAT delta-T from this point is unreasonable. Design cooling SAT will be #{new_setpoint_f.round} F from proposed model.")
1083
+ return new_setpoint_c
1084
+ end
1085
+
1086
+ # Subtract 20F delta-T
1087
+ delta_t_r = 20
1088
+ delta_t_k = OpenStudio.convert(delta_t_r, 'R', 'K').get
1089
+
1090
+ sat_c = setpoint_c - delta_t_k # Subtract for cooling
1091
+
1092
+ return sat_c
1093
+ end
1094
+
1095
+ # Set the design delta-T for zone heating and cooling sizing
1096
+ # supply air temperatures. This value determines zone
1097
+ # air flows, which will be summed during system
1098
+ # design airflow calculation.
1099
+ #
1100
+ # @return [Bool] true if successful, false if not
1101
+ def apply_prm_baseline_supply_temperatures
1102
+ # Skip spaces that aren't heated or cooled
1103
+ return true unless heated? || cooled?
1104
+
1105
+ # Heating
1106
+ htg_sat_c = prm_baseline_heating_design_supply_temperature
1107
+ htg_success = sizingZone.setZoneHeatingDesignSupplyAirTemperature(htg_sat_c)
1108
+
1109
+ # Cooling
1110
+ clg_sat_c = prm_baseline_cooling_design_supply_temperature
1111
+ clg_success = sizingZone.setZoneCoolingDesignSupplyAirTemperature(clg_sat_c)
1112
+
1113
+ htg_sat_f = OpenStudio.convert(htg_sat_c, 'C', 'F').get
1114
+ clg_sat_f = OpenStudio.convert(clg_sat_c, 'C', 'F').get
1115
+ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "For #{name}, Htg SAT = #{htg_sat_f.round(1)}F, Clg SAT = #{clg_sat_f.round(1)}F.")
1116
+
1117
+ result = false
1118
+ if htg_success && clg_success
1119
+ result = true
1120
+ end
1121
+
1122
+ return result
1123
+ end
1124
+
1125
+ def add_unconditioned_thermostat
1126
+ # Heated to 0F (below heated? threshold)
1127
+ htg_t_f = 0
1128
+ htg_t_c = OpenStudio.convert(htg_t_f, 'F', 'C').get
1129
+ htg_stpt_sch = OpenStudio::Model::ScheduleRuleset.new(model)
1130
+ htg_stpt_sch.setName('Unconditioned Minimal Heating')
1131
+ htg_stpt_sch.defaultDaySchedule.setName('Unconditioned Minimal Heating Default')
1132
+ htg_stpt_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), htg_t_c)
1133
+
1134
+ # Cooled to 120F (above cooled? threshold)
1135
+ clg_t_f = 120
1136
+ clg_t_c = OpenStudio.convert(clg_t_f, 'F', 'C').get
1137
+ clg_stpt_sch = OpenStudio::Model::ScheduleRuleset.new(model)
1138
+ clg_stpt_sch.setName('Unconditioned Minimal Heating')
1139
+ clg_stpt_sch.defaultDaySchedule.setName('Unconditioned Minimal Heating Default')
1140
+ clg_stpt_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), clg_t_c)
1141
+
1142
+ # Thermostat
1143
+ thermostat = OpenStudio::Model::ThermostatSetpointDualSetpoint.new(model)
1144
+ thermostat.setName("#{name} Unconditioned Thermostat")
1145
+ thermostat.setHeatingSetpointTemperatureSchedule(htg_stpt_sch)
1146
+ thermostat.setCoolingSetpointTemperatureSchedule(clg_stpt_sch)
1147
+
1148
+ return true
1149
+ end
1150
+
1151
+ # Determine the design internal load (W) for
1152
+ # this zone without space multipliers.
1153
+ # This include People, Lights, Electric Equipment,
1154
+ # and Gas Equipment in all spaces in this zone.
1155
+ # It assumes 100% of the wattage
1156
+ # is converted to heat, and that the design peak
1157
+ # schedule value is 1 (100%).
1158
+ #
1159
+ # @return [Double] the design internal load, in W
1160
+ def design_internal_load
1161
+ load_w = 0.0
1162
+
1163
+ spaces.each do |space|
1164
+ load_w += space.design_internal_load
1165
+ end
1166
+
1167
+ return load_w
1168
+ end
1169
+
1170
+ # Returns the space type that represents a majority
1171
+ # of the floor area.
1172
+ #
1173
+ # @return [Boost::Optional<OpenStudio::Model::SpaceType>] an optional SpaceType
1174
+ def majority_space_type
1175
+ space_type_to_area = Hash.new(0.0)
1176
+
1177
+ spaces.each do |space|
1178
+ if space.spaceType.is_initialized
1179
+ space_type = space.spaceType.get
1180
+ space_type_to_area[space_type] += space.floorArea
1181
+ end
1182
+ end
1183
+
1184
+ # If no space types, return empty optional SpaceType
1185
+ if space_type_to_area.size.zero?
1186
+ return OpenStudio::Model::OptionalSpaceType.new
1187
+ end
1188
+
1189
+ # Sort by area
1190
+ biggest_space_type = space_type_to_area.sort_by { |st, area| area }.reverse[0][0]
1191
+
1192
+ return OpenStudio::Model::OptionalSpaceType.new(biggest_space_type)
1193
+ end
1194
+
1195
+ # Determine if the thermal zone's occupancy type category.
1196
+ # Options are:
1197
+ # residential, nonresidential
1198
+ # 90.1-2013 adds additional Options:
1199
+ # publicassembly, retail
1200
+ #
1201
+ # @return [String] the occupancy type category
1202
+ # @todo Add public assembly building types
1203
+ def occupancy_type(template)
1204
+ occ_type = if residential?(template)
1205
+ 'residential'
1206
+ else
1207
+ 'nonresidential'
1208
+ end
1209
+
1210
+ # Based on the space type that
1211
+ # represents a majority of the zone.
1212
+ if template == '90.1-2013'
1213
+ space_type = majority_space_type
1214
+ if space_type.is_initialized
1215
+ space_type = space_type.get
1216
+ bldg_type = space_type.standardsBuildingType
1217
+ if bldg_type.is_initialized
1218
+ bldg_type = bldg_type.get
1219
+ case bldg_type
1220
+ when 'Retail', 'StripMall', 'SuperMarket'
1221
+ occ_type = 'retail'
1222
+ # when 'SomeBuildingType' # TODO add publicassembly building types
1223
+ # occ_type = 'publicassembly'
1224
+ end
1225
+ end
1226
+ end
1227
+ end
1228
+
1229
+ # OpenStudio::logFree(OpenStudio::Info, "openstudio.Standards.ThermalZone", "For #{self.name}, occupancy type = #{occ_type}.")
1230
+
1231
+ return occ_type
1232
+ end
1233
+
1234
+ # Determine if demand control ventilation (DCV) is
1235
+ # required for this zone based on area and occupant density.
1236
+ # Does not account for System requirements like ERV, economizer, etc.
1237
+ # Those are accounted for in the AirLoopHVAC method of the same name.
1238
+ #
1239
+ # @return [Bool] Returns true if required, false if not.
1240
+ # @todo Add exception logic for 90.1-2013
1241
+ # for cells, sickrooms, labs, barbers, salons, and bowling alleys
1242
+ def demand_control_ventilation_required?(template, climate_zone)
1243
+ dcv_required = false
1244
+
1245
+ # Not required by the old vintages
1246
+ if template == 'DOE Ref Pre-1980' || template == 'DOE Ref 1980-2004' || template == 'NECB 2011'
1247
+ return dcv_required
1248
+ end
1249
+
1250
+ # Area and occupant density limits
1251
+ min_area_ft2 = 0
1252
+ min_occ_per_1000_ft2 = 0
1253
+ case template
1254
+ when '90.1-2004'
1255
+ min_area_ft2 = 0
1256
+ min_occ_per_1000_ft2 = 100
1257
+ when '90.1-2007', '90.1-2010'
1258
+ min_area_ft2 = 500
1259
+ min_occ_per_1000_ft2 = 40
1260
+ when '90.1-2013'
1261
+ min_area_ft2 = 500
1262
+ min_occ_per_1000_ft2 = 25
1263
+ end
1264
+
1265
+ # Get the area served and the number of occupants
1266
+ area_served_m2 = 0
1267
+ num_people = 0
1268
+ spaces.each do |space|
1269
+ area_served_m2 += space.floorArea
1270
+ num_people += space.numberOfPeople
1271
+ end
1272
+
1273
+ # Check the minimum area
1274
+ area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get
1275
+ if area_served_ft2 < min_area_ft2
1276
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ThermalZone', "For #{name}: DCV is not required since the area is #{area_served_ft2.round} ft2, but the minimum size is #{min_area_ft2.round} ft2.")
1277
+ return dcv_required
1278
+ end
1279
+
1280
+ # Check the minimum occupancy density
1281
+ occ_per_ft2 = num_people / area_served_ft2
1282
+ occ_per_1000_ft2 = occ_per_ft2 * 1000
1283
+
1284
+ if occ_per_1000_ft2 < min_occ_per_1000_ft2
1285
+ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ThermalZone', "For #{name}: DCV is not required since the occupant density is #{occ_per_1000_ft2.round} people/1000 ft2, but the minimum occupant density is #{min_occ_per_1000_ft2.round} people/1000 ft2.")
1286
+ return dcv_required
1287
+ end
1288
+
1289
+ # If here, DCV is required
1290
+ dcv_required = true
1291
+
1292
+ return dcv_required
1293
+ end
569
1294
  end