Module: OpenstudioStandards::SqlFile

Defined in:
lib/openstudio-standards/sql_file/sql_file.rb,
lib/openstudio-standards/sql_file/energy_use.rb,
lib/openstudio-standards/sql_file/unmet_hours.rb,
lib/openstudio-standards/sql_file/fenestration.rb

Overview

The SqlFile module provides methods to get information from the EnergyPlus .sql file after a run

Energy Use collapse

Unmet Hours collapse

Fenestration collapse

Class Method Summary collapse

Class Method Details

.construction_calculated_fenestration_u_factor(construction) ⇒ Double

Return the calculated fenestration U-Factor based on the glass, frame, and divider performance and area from the EnergyPlus Envelope Summary report.

Parameters:

  • construction (OpenStudio:Model:Construction)

    OpenStudio Construction object

Returns:

  • (Double)

    the U-Factor in W/m^2*K



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/openstudio-standards/sql_file/fenestration.rb', line 11

def self.construction_calculated_fenestration_u_factor(construction)
  # check for sql file
  sql = construction.model.sqlFile
  unless sql.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Model has no sql file containing results, cannot lookup data.')
    return false
  end
  sql = sql.get

  u_factor_w_per_m2_k = nil
  construction_name = construction.name.get.to_s

  row_query = "SELECT RowName
              FROM tabulardatawithstrings
              WHERE ReportName='EnvelopeSummary'
              AND ReportForString='Entire Facility'
              AND TableName='Exterior Fenestration'
              AND Value='#{construction_name.upcase}'"

  row_id = sql.execAndReturnFirstString(row_query)

  if row_id.is_initialized
    row_id = row_id.get
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.SqlFile', "U-Factor row ID not found for construction: #{construction_name}.")
    row_id = 9999
  end

  # Glass U-Factor
  glass_u_factor_query = "SELECT Value
              FROM tabulardatawithstrings
              WHERE ReportName='EnvelopeSummary'
              AND ReportForString='Entire Facility'
              AND TableName='Exterior Fenestration'
              AND ColumnName='Glass U-Factor'
              AND RowName='#{row_id}'"

  glass_u_factor_w_per_m2_k = sql.execAndReturnFirstDouble(glass_u_factor_query)

  glass_u_factor_w_per_m2_k = glass_u_factor_w_per_m2_k.is_initialized ? glass_u_factor_w_per_m2_k.get : 0.0

  # Glass area
  glass_area_query = "SELECT Value
                      FROM tabulardatawithstrings
                      WHERE ReportName='EnvelopeSummary'
                      AND ReportForString='Entire Facility'
                      AND TableName='Exterior Fenestration'
                      AND ColumnName='Glass Area'
                      AND RowName='#{row_id}'"

  glass_area_m2 = sql.execAndReturnFirstDouble(glass_area_query)

  glass_area_m2 = glass_area_m2.is_initialized ? glass_area_m2.get : 0.0

  # Frame conductance
  frame_conductance_query = "SELECT Value
              FROM tabulardatawithstrings
              WHERE ReportName='EnvelopeSummary'
              AND ReportForString='Entire Facility'
              AND TableName='Exterior Fenestration'
              AND ColumnName='Frame Conductance'
              AND RowName='#{row_id}'"

  frame_conductance_w_per_m2_k = sql.execAndReturnFirstDouble(frame_conductance_query)

  frame_conductance_w_per_m2_k = frame_conductance_w_per_m2_k.is_initialized ? frame_conductance_w_per_m2_k.get : 0.0

  # Frame area
  frame_area_query = "SELECT Value
                      FROM tabulardatawithstrings
                      WHERE ReportName='EnvelopeSummary'
                      AND ReportForString='Entire Facility'
                      AND TableName='Exterior Fenestration'
                      AND ColumnName='Frame Area'
                      AND RowName='#{row_id}'"

  frame_area_m2 = sql.execAndReturnFirstDouble(frame_area_query)

  frame_area_m2 = frame_area_m2.is_initialized ? frame_area_m2.get : 0.0

  # Divider conductance
  divider_conductance_query = "SELECT Value
              FROM tabulardatawithstrings
              WHERE ReportName='EnvelopeSummary'
              AND ReportForString='Entire Facility'
              AND TableName='Exterior Fenestration'
              AND ColumnName='Divider Conductance'
              AND RowName='#{row_id}'"

  divider_conductance_w_per_m2_k = sql.execAndReturnFirstDouble(divider_conductance_query)

  divider_conductance_w_per_m2_k = divider_conductance_w_per_m2_k.is_initialized ? divider_conductance_w_per_m2_k.get : 0.0

  # Divider area
  divider_area_query = "SELECT Value
                      FROM tabulardatawithstrings
                      WHERE ReportName='EnvelopeSummary'
                      AND ReportForString='Entire Facility'
                      AND TableName='Exterior Fenestration'
                      AND ColumnName='Divder Area'
                      AND RowName='#{row_id}'"

  divider_area_m2 = sql.execAndReturnFirstDouble(divider_area_query)

  divider_area_m2 = divider_area_m2.is_initialized ? divider_area_m2.get : 0.0

  u_factor_w_per_m2_k = ((glass_u_factor_w_per_m2_k * glass_area_m2) + (frame_conductance_w_per_m2_k * frame_area_m2) + (divider_conductance_w_per_m2_k * divider_area_m2)) / (glass_area_m2 + frame_area_m2 + divider_area_m2)

  return u_factor_w_per_m2_k
end

.model_get_annual_energy_by_fuel_and_enduse(model, fuel_type, end_use) ⇒ Double

Gets the model annual energy consumption by fuel and enduse in GJ from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • fuel_type (String)

    the fuel type, e.g. ‘Electricity’

  • end_use (String)

    the end use, e.g. ‘InteriorEquipment’

Returns:

  • (Double)

    the model energy fuel type end use in Gigajoules



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/openstudio-standards/sql_file/energy_use.rb', line 12

def self.model_get_annual_energy_by_fuel_and_enduse(model, fuel_type, end_use)
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # setup the queries
  query = "SELECT Value
          FROM TabularDataWithStrings
          WHERE ReportName='AnnualBuildingUtilityPerformanceSummary'
          AND ReportForString='Entire Facility'
          AND TableName='End Uses'
          AND RowName = '#{end_use}'
          AND ColumnName='#{fuel_type}'"

  # get the info
  energy_gj = sql_file.execAndReturnFirstDouble(query)

  # make sure all the data are available
  if energy_gj.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not get energy for #{fuel_type} #{end_use}.")
    return 0.0
  end

  return energy_gj.get
end

.model_get_annual_eui_kbtu_per_ft2(model) ⇒ Double

Gets the model total annual energy use intensity in kBtu/ft^2 from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Double)

    the model total annual site energy use intensity in kBtu/ft^2, inclusive of all spaces



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/openstudio-standards/sql_file/energy_use.rb', line 152

def self.model_get_annual_eui_kbtu_per_ft2(model)
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # make sure all required data are available
  if sql_file.totalSiteEnergy.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Site energy data unavailable.')
    return false
  end

  total_site_energy_kbtu = OpenStudio.convert(sql_file.totalSiteEnergy.get, 'GJ', 'kBtu').get
  floor_area_ft2 = OpenStudio.convert(model.getBuilding.floorArea, 'm^2', 'ft^2').get
  site_eui_kbtu_per_ft2 = total_site_energy_kbtu / floor_area_ft2

  return site_eui_kbtu_per_ft2
end

.model_get_annual_eui_kbtu_per_ft2_by_fuel_and_enduse(model, fuel_type, end_use) ⇒ Double

Gets annual energy use intensity by fuel and end use in kBtu/ft^2 from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Double)

    a hash of annual energy use intensity by each fuel and end use in kBtu/ft^2, inclusive of all spaces



139
140
141
142
143
144
145
146
# File 'lib/openstudio-standards/sql_file/energy_use.rb', line 139

def self.model_get_annual_eui_kbtu_per_ft2_by_fuel_and_enduse(model, fuel_type, end_use)
  energy_gj = OpenstudioStandards::SqlFile.model_get_annual_energy_by_fuel_and_enduse(model, fuel_type, end_use)
  energy_kbtu = OpenStudio.convert(energy_gj, 'GJ', 'kBtu').get
  floor_area_ft2 = OpenStudio.convert(model.getBuilding.floorArea, 'm^2', 'ft^2').get
  eui_kbtu_per_ft2 = energy_kbtu / floor_area_ft2

  return eui_kbtu_per_ft2
end

.model_get_annual_occupied_unmet_cooling_hours(model, tolerance: nil) ⇒ Double

Gets the annual occupied unmet cooling hours from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • tolerance (Double) (defaults to: nil)

    tolerance in degrees Rankine to log an unmet hour If this is unspecified, the tolerance will be the tolerance specified in OutputControl:ReportingTolerances. If there isn’t an OutputControl:ReportingTolerances object, the EnergyPlus default is 0.2 degrees Kelvin. If a tolerance is defined and does not match the tolerance defined in OutputControl:ReportingTolerances, this method will compare the zone temperature and setpoint temperature timeseries for each zone. Generally, it is much faster to define tolerances with the OutputControl:ReportingTolerances object.

Returns:

  • (Double)

    occupied cooling unmet hours



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 271

def self.model_get_annual_occupied_unmet_cooling_hours(model, tolerance: nil)
  reporting_tolerances = model.getOutputControlReportingTolerances
  model_tolerance = reporting_tolerances.toleranceforTimeHeatingSetpointNotMet
  model_tolerance_r = OpenStudio.convert(model_tolerance, 'K', 'R')

  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  use_detailed = false
  unless tolerance.nil?
    # check to see if input argument tolerance matches model tolerance
    tolerance_k = OpenStudio.convert(tolerance, 'R', 'K').get
    unless (model_tolerance - tolerance_k).abs < 1e-3
      # input argument tolerance does not match model tolerance; need to recalculate unmet hours
      use_detailed = true
    end
  end

  if use_detailed
    # calculate unmet hours for each zone using zone time series
    zones_unmet_hours = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_cooling_hours_detailed(model, tolerance: tolerance)
    cooling_unmet_hours = zones_unmet_hours['sum_bldg_occupied_unmet_hours']
  else
    # use default EnergyPlus unmet hours reporting
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SqlFile', "Calculating cooling unmet hours with #{model_tolerance_r} R tolerance")

    # setup the queries
    cooling_setpoint_unmet_query = "SELECT Value
                                  FROM TabularDataWithStrings
                                  WHERE ReportName='SystemSummary'
                                  AND ReportForString='Entire Facility'
                                  AND TableName='Time Setpoint Not Met'
                                  AND RowName = 'Facility'
                                  AND ColumnName='During Occupied Cooling'"
    # get the info
    cooling_setpoint_unmet = sql_file.execAndReturnFirstDouble(cooling_setpoint_unmet_query)

    # make sure all the data are available
    if cooling_setpoint_unmet.empty?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Could not get unmet cooling hours information.')
      return false
    end

    cooling_unmet_hours = cooling_setpoint_unmet.get
  end

  return cooling_unmet_hours
end

.model_get_annual_occupied_unmet_cooling_hours_detailed(model, tolerance: 1.0, occupied_percentage_threshold: 0.05) ⇒ Hash

TODO:

account for operative temperature thermostats

Gets the annual occupied unmet cooling hours from zone temperature time series in the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • occupied_percentage_threshold (Double) (defaults to: 0.05)

    the minimum fraction (0 to 1) that counts as occupied

  • tolerance (Double) (defaults to: 1.0)

    tolerance in degrees Rankine to log an unmet hour

Returns:

  • (Hash)

    Hash with ‘sum’ of cooling unmet hours and ‘zone_temperature_differences’ of all zone unmet hours data



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 111

def self.model_get_annual_occupied_unmet_cooling_hours_detailed(model, tolerance: 1.0, occupied_percentage_threshold: 0.05)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SqlFile', "Calculating zone cooling occupied unmet hours with #{tolerance} R tolerance. This may take some time.")
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # std to access thermal zone methods. Replace when thermal zone methods are moved to modules
  std = Standard.build('90.1-2013')

  # convert tolerance to Kelvin
  tolerance_k = OpenStudio.convert(tolerance, 'R', 'K').get

  ann_env_pd = OpenstudioStandards::SqlFile.model_get_weather_run_period(model)
  unless ann_env_pd
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Could not get annual run period.')
    return false
  end

  # for each zone calculate unmet hours and store in array
  bldg_unmet_hours = []
  bldg_occ_unmet_hours = []
  zone_data = []
  model.getThermalZones.each do |zone|
    # skip zones that aren't cooled
    next unless OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zone)

    # get zone air temperatures
    zone_temp_timeseries = sql_file.timeSeries(ann_env_pd, 'Hourly', 'Zone Air Temperature', zone.name.get)
    if zone_temp_timeseries.empty?
      # try mean air temperature instead
      zone_temp_timeseries = sql_file.timeSeries(ann_env_pd, 'Hourly', 'Zone Mean Air Temperature', zone.name.get)
      if zone_temp_timeseries.empty?
        # no air temperature found
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not find zone air temperature timeseries for zone '#{zone.name.get}'")
        return false
      end
    end
    zone_temp_timeseries = zone_temp_timeseries.get.values

    # get zone thermostat heating setpoint temperatures
    zone_setpoint_temp_timeseries = sql_file.timeSeries(ann_env_pd, 'Hourly', 'Zone Thermostat Cooling Setpoint Temperature', zone.name.get)
    if zone_setpoint_temp_timeseries.empty?
      # no setpoint temperature found
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not find cooling setpoint temperature timeseries for zone '#{zone.name.get}'")
      return false
    end
    zone_setpoint_temp_timeseries = zone_setpoint_temp_timeseries.get.values

    # calculate zone occupancy by making a new ruleset schedule
    occ_schedule_ruleset = OpenstudioStandards::ThermalZone.thermal_zone_get_occupancy_schedule(zone)
    occ_values = OpenstudioStandards::Schedules.schedule_ruleset_get_hourly_values(occ_schedule_ruleset)

    # calculate difference accounting for unmet hours tolerance
    zone_temperature_diff = zone_setpoint_temp_timeseries.map.with_index { |t, x| (t - zone_temp_timeseries[x]) }
    zone_unmet_hours = zone_temperature_diff.map { |x| (x - tolerance_k) > 0 ? 1 : 0 }
    zone_occ_unmet_hours = []
    for i in (0..zone_unmet_hours.size - 1)
      bldg_unmet_hours[i] = 0 if bldg_unmet_hours[i].nil?
      bldg_occ_unmet_hours[i] = 0 if bldg_occ_unmet_hours[i].nil?
      bldg_unmet_hours[i] += zone_unmet_hours[i]
      if occ_values[i] >= occupied_percentage_threshold
        zone_occ_unmet_hours[i] = zone_unmet_hours[i]
        bldg_occ_unmet_hours[i] += zone_unmet_hours[i]
      else
        zone_occ_unmet_hours[i] = 0
      end
    end

    # log information for zone
    # could reduce the number of returned variables if this poses a storage or data transfer problem
    zone_data << {
      'zone_name' => zone.name,
      'zone_area' => zone.floorArea,
      'zone_air_temperatures' => zone_temp_timeseries.map { |t| t.round(3) },
      'zone_air_setpoint_temperatures' => zone_setpoint_temp_timeseries.map { |t| t.round(3) },
      'zone_air_temperature_differences' => zone_temperature_diff.map { |d| d.round(3) },
      'zone_occupancy' => occ_values.map { |x| x.round(3) },
      'zone_unmet_hours' => zone_unmet_hours,
      'zone_occupied_unmet_hours' => zone_occ_unmet_hours,
      'sum_zone_unmet_hours' => zone_unmet_hours.count { |x| x > 0 },
      'sum_zone_occupied_unmet_hours' => zone_occ_unmet_hours.count { |x| x > 0 }
    }
  end

  occupied_unmet_cooling_hours_detailed = { 'sum_bldg_unmet_hours' => bldg_unmet_hours.count { |x| x > 0 },
                                            'sum_bldg_occupied_unmet_hours' => bldg_occ_unmet_hours.count { |x| x > 0 },
                                            'bldg_unmet_hours' => bldg_unmet_hours,
                                            'bldg_occupied_unmet_hours' => bldg_occ_unmet_hours,
                                            'zone_data' => zone_data }
  return occupied_unmet_cooling_hours_detailed
end

.model_get_annual_occupied_unmet_heating_hours(model, tolerance: nil) ⇒ Double

Gets the annual occupied unmet heating hours from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • tolerance (Double) (defaults to: nil)

    tolerance in degrees Rankine to log an unmet hour If this is unspecified, the tolerance will be the tolerance specified in OutputControl:ReportingTolerances. If there isn’t an OutputControl:ReportingTolerances object, the EnergyPlus default is 0.2 degrees Kelvin. If a tolerance is defined and does not match the tolerance defined in OutputControl:ReportingTolerances, this method will compare the zone temperature and setpoint temperature timeseries for each zone. Generally, it is much faster to define tolerances with the OutputControl:ReportingTolerances object.

Returns:

  • (Double)

    occupied heating unmet hours



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 212

def self.model_get_annual_occupied_unmet_heating_hours(model, tolerance: nil)
  reporting_tolerances = model.getOutputControlReportingTolerances
  model_tolerance = reporting_tolerances.toleranceforTimeHeatingSetpointNotMet
  model_tolerance_r = OpenStudio.convert(model_tolerance, 'K', 'R')

  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  use_detailed = false
  unless tolerance.nil?
    # check to see if input argument tolerance matches model tolerance
    tolerance_k = OpenStudio.convert(tolerance, 'R', 'K').get
    unless (model_tolerance - tolerance_k).abs < 1e-3
      # input argument tolerance does not match model tolerance; need to recalculate unmet hours
      use_detailed = true
    end
  end

  if use_detailed
    # calculate unmet hours for each zone using zone time series
    zones_unmet_hours = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_heating_hours_detailed(model, tolerance: tolerance)
    heating_unmet_hours = zones_unmet_hours['sum_bldg_occupied_unmet_hours']
  else
    # use default EnergyPlus unmet hours reporting
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SqlFile', "Calculating heating unmet hours with #{model_tolerance_r} R tolerance")

    # setup the queries
    heating_setpoint_unmet_query = "SELECT Value
                                  FROM TabularDataWithStrings
                                  WHERE ReportName='SystemSummary'
                                  AND ReportForString='Entire Facility'
                                  AND TableName='Time Setpoint Not Met'
                                  AND RowName = 'Facility'
                                  AND ColumnName='During Occupied Heating'"
    # get the info
    heating_setpoint_unmet = sql_file.execAndReturnFirstDouble(heating_setpoint_unmet_query)

    # make sure all the data are available
    if heating_setpoint_unmet.empty?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Could not get unmet heating hours information.')
      return false
    end

    heating_unmet_hours = heating_setpoint_unmet.get
  end

  return heating_unmet_hours
end

.model_get_annual_occupied_unmet_heating_hours_detailed(model, tolerance: 1.0, occupied_percentage_threshold: 0.05) ⇒ Hash

TODO:

account for operative temperature thermostats

Gets the annual occupied unmet heating hours from zone temperature time series in the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • tolerance (Double) (defaults to: 1.0)

    tolerance in degrees Rankine to log an unmet hour

  • occupied_percentage_threshold (Double) (defaults to: 0.05)

    the minimum fraction (0 to 1) that counts as occupied

Returns:

  • (Hash)

    Hash with ‘sum’ of heating unmet hours and ‘zone_temperature_differences’ of all zone unmet hours data



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 13

def self.model_get_annual_occupied_unmet_heating_hours_detailed(model, tolerance: 1.0, occupied_percentage_threshold: 0.05)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.SqlFile', "Calculating zone heating occupied unmet hours with #{tolerance} R tolerance.  This may take some time.")
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # std to access thermal zone methods. Replace when thermal zone methods are moved to modules
  std = Standard.build('90.1-2013')

  # convert tolerance to Kelvin
  tolerance_k = OpenStudio.convert(tolerance, 'R', 'K').get

  ann_env_pd = OpenstudioStandards::SqlFile.model_get_weather_run_period(model)
  unless ann_env_pd
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Could not get annual run period.')
    return false
  end

  # for each zone calculate unmet hours and store in array
  bldg_unmet_hours = []
  bldg_occ_unmet_hours = []
  zone_data = []
  model.getThermalZones.each do |zone|
    # skip zones that aren't heated
    next unless OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone)

    # get zone air temperatures
    zone_temp_timeseries = sql_file.timeSeries(ann_env_pd, 'Hourly', 'Zone Air Temperature', zone.name.get)
    if zone_temp_timeseries.empty?
      # try mean air temperature instead
      zone_temp_timeseries = sql_file.timeSeries(ann_env_pd, 'Hourly', 'Zone Mean Air Temperature', zone.name.get)
      if zone_temp_timeseries.empty?
        # no air temperature found
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not find zone air temperature timeseries for zone '#{zone.name.get}'")
        return false
      end
    end
    zone_temp_timeseries = zone_temp_timeseries.get.values

    # get zone thermostat heating setpoint temperatures
    zone_setpoint_temp_timeseries = sql_file.timeSeries(ann_env_pd, 'Hourly', 'Zone Thermostat Heating Setpoint Temperature', zone.name.get)
    if zone_setpoint_temp_timeseries.empty?
      # no setpoint temperature found
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not find heating setpoint temperature timeseries for zone '#{zone.name.get}'")
      return false
    end
    zone_setpoint_temp_timeseries = zone_setpoint_temp_timeseries.get.values

    # calculate zone occupancy by making a new ruleset schedule
    occ_schedule_ruleset = OpenstudioStandards::ThermalZone.thermal_zone_get_occupancy_schedule(zone)
    occ_values = OpenstudioStandards::Schedules.schedule_ruleset_get_hourly_values(occ_schedule_ruleset)

    # calculate difference accounting for unmet hours tolerance
    zone_temperature_diff = zone_setpoint_temp_timeseries.map.with_index { |t, x| (zone_temp_timeseries[x] - t) }
    zone_unmet_hours = zone_temperature_diff.map { |x| (x + tolerance_k) < 0 ? 1 : 0 }
    zone_occ_unmet_hours = []
    for i in (0..zone_unmet_hours.size - 1)
      bldg_unmet_hours[i] = 0 if bldg_unmet_hours[i].nil?
      bldg_occ_unmet_hours[i] = 0 if bldg_occ_unmet_hours[i].nil?
      bldg_unmet_hours[i] += zone_unmet_hours[i]
      if occ_values[i] >= occupied_percentage_threshold
        zone_occ_unmet_hours[i] = zone_unmet_hours[i]
        bldg_occ_unmet_hours[i] += zone_unmet_hours[i]
      else
        zone_occ_unmet_hours[i] = 0
      end
    end

    # log information for zone
    # could reduce the number of returned variables if this poses a storage or data transfer problem
    zone_data << {
      'zone_name' => zone.name,
      'zone_area' => zone.floorArea,
      'zone_air_temperatures' => zone_temp_timeseries.map { |t| t.round(3) },
      'zone_air_setpoint_temperatures' => zone_setpoint_temp_timeseries.map { |t| t.round(3) },
      'zone_air_temperature_differences' => zone_temperature_diff.map { |d| d.round(3) },
      'zone_occupancy' => occ_values.map { |x| x.round(3) },
      'zone_unmet_hours' => zone_unmet_hours,
      'zone_occupied_unmet_hours' => zone_occ_unmet_hours,
      'sum_zone_unmet_hours' => zone_unmet_hours.count { |x| x > 0 },
      'sum_zone_occupied_unmet_hours' => zone_occ_unmet_hours.count { |x| x > 0 }
    }
  end

  occupied_unmet_heating_hours_detailed = { 'sum_bldg_unmet_hours' => bldg_unmet_hours.count { |x| x > 0 },
                                            'sum_bldg_occupied_unmet_hours' => bldg_occ_unmet_hours.count { |x| x > 0 },
                                            'bldg_unmet_hours' => bldg_unmet_hours,
                                            'bldg_occupied_unmet_hours' => bldg_occ_unmet_hours,
                                            'zone_data' => zone_data }
  return occupied_unmet_heating_hours_detailed
end

.model_get_annual_occupied_unmet_hours(model) ⇒ Double

Gets the annual occupied unmet hours from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Double)

    the total number of unmet heating or cooling hours



324
325
326
327
328
329
330
331
332
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 324

def self.model_get_annual_occupied_unmet_hours(model)
  heating_setpoint_unmet = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_heating_hours(model)
  cooling_setpoint_unmet = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_cooling_hours(model)

  # aggregate heating and cooling hrs
  heating_or_cooling_setpoint_unmet = heating_setpoint_unmet + cooling_setpoint_unmet

  return heating_or_cooling_setpoint_unmet
end

.model_get_annual_results_by_end_use_and_fuel_type(model) ⇒ Hash

TODO:

update for fuel type changes

Gets all annual energy consumption by enduse and fuel type from the sql file

e.g. Heating|Electricity, Exterior Equipment|Water. All end use/fuel type combos are present, with values of 0.0 if none of this end use/fuel type combo was used by the simulation.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Hash)

    a hash of results for each fuel, where the keys are in the form ‘End Use|Fuel Type’,



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/openstudio-standards/sql_file/energy_use.rb', line 84

def self.model_get_annual_results_by_end_use_and_fuel_type(model)
  energy_values = {}

  # List of all fuel types
  fuel_types = ['Electricity', 'Natural Gas', 'Additional Fuel', 'District Cooling', 'District Heating', 'Water']

  # List of all end uses
  end_uses = ['Heating', 'Cooling', 'Interior Lighting', 'Exterior Lighting', 'Interior Equipment', 'Exterior Equipment', 'Fans', 'Pumps', 'Heat Rejection', 'Humidification', 'Heat Recovery', 'Water Systems', 'Refrigeration', 'Generators']

  # Get the value for each end use/ fuel type combination
  end_uses.each do |end_use|
    fuel_types.each do |fuel_type|
      energy_values["#{end_use}|#{fuel_type}"] = OpenstudioStandards::SqlFile.model_get_annual_energy_by_fuel_and_enduse(model, fuel_type, end_use)
    end
  end

  return energy_values
end

.model_get_dd_energy_by_fuel_and_enduse(model, fuel_type, end_use) ⇒ Double

Gets the model design day energy consumption by fuel and enduse in J from the sql file Uses the meter data dictionary instead of annual building utility performance summary

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • fuel_type (String)

    the fuel type, e.g. ‘Electricity’

  • end_use (String)

    the end use, e.g. ‘InteriorEquipment’

Returns:

  • (Double)

    the model energy fuel type end use in Joules



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/openstudio-standards/sql_file/energy_use.rb', line 44

def self.model_get_dd_energy_by_fuel_and_enduse(model, fuel_type, end_use)
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # setup the end use index query
  get_rpt_mtr_data_dic_idx = "SELECT ReportMeterDataDictionaryIndex
                              FROM ReportMeterDataDictionary
                              WHERE VariableName='#{end_use}:#{fuel_type}'"

  # get the end use index
  idx = sql_file.execAndReturnFirstDouble(get_rpt_mtr_data_dic_idx)

  # if no index it means that the end use isn't used in the model
  if idx.empty?
    return 0.0
  end

  # setup the energy use retrieval queries for the design days
  get_energy_j = "SELECT SUM (VariableValue)
                  FROM ReportMeterData
                  WHERE ReportMeterDataDictionaryIndex='#{idx}'"

  # get the end use energy value
  energy_j = sql_file.execAndReturnFirstDouble(get_energy_j)

  # no energy value, means that something isn't right, set it to 0 as a safeguard
  if energy_j.empty?
    return 0.0
  end

  return energy_j.get
end

.model_get_dd_results_by_end_use_and_fuel_type(model) ⇒ Hash

Gets all design day energy consumption by enduse and fuel type from the sql file

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Hash)

    a hash of results for each fuel, where the keys are in the form ‘EndUse|FuelType’, # e.g. Heating|Electricity, ExteriorEquipment|Water. All end use/fuel type combos are present, with # values of 0.0 if none of this end use/fuel type combo was used by the simulation.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/openstudio-standards/sql_file/energy_use.rb', line 109

def self.model_get_dd_results_by_end_use_and_fuel_type(model)
  energy_values = {}

  # List of all fuel types, based on Table 5.1 of EnergyPlus' Input Output Reference manual
  if model.version < OpenStudio::VersionString.new('3.7.0')
    fuel_types = ['Electricity', 'Gas', 'Gasoline', 'Diesel', 'Coal', 'FuelOilNo1', 'FuelOilNo2', 'Propane', 'OtherFuel1', 'OtherFuel2', 'Water', 'Steam', 'DistrictCooling',
                  'DistrictHeating', 'ElectricityPurchased', 'ElectricitySurplusSold', 'ElectricityNet']
  else
    fuel_types = ['Electricity', 'Gas', 'Gasoline', 'Diesel', 'Coal', 'FuelOilNo1', 'FuelOilNo2', 'Propane', 'OtherFuel1', 'OtherFuel2', 'Water', 'DistrictCooling',
                  'DistrictHeatingWater', 'DistrictHeatingSteam', 'ElectricityPurchased', 'ElectricitySurplusSold', 'ElectricityNet']
  end

  # List of all end uses, based on Table 5.3 of EnergyPlus' Input Output Reference manual
  end_uses = ['InteriorLights', 'ExteriorLights', 'InteriorEquipment', 'ExteriorEquipment', 'Fans', 'Pumps', 'Heating', 'Cooling', 'HeatRejection', 'Humidifier',
              'HeatRecovery', 'DHW', 'Cogeneration', 'Refrigeration', 'WaterSystems']

  # Get the value for each end use/ fuel type combination
  end_uses.each do |end_use|
    fuel_types.each do |fuel_type|
      energy_values["#{end_use}|#{fuel_type}"] = OpenstudioStandards::SqlFile.model_get_dd_energy_by_fuel_and_enduse(model, fuel_type, end_use)
    end
  end

  return energy_values
end

.model_get_sql_file(model) ⇒ OpenStudio::SqlFile

Gets the sql file for the model, erroring if not found

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (OpenStudio::SqlFile)

    OpenStudio sqlFile associated with the model, boolean false if not found



82
83
84
85
86
87
88
89
90
# File 'lib/openstudio-standards/sql_file/sql_file.rb', line 82

def self.model_get_sql_file(model)
  # Ensure that the model has a sql file associated with it
  if model.sqlFile.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Failed to retrieve data because the sql file containing results is missing.')
    return false
  end

  return model.sqlFile.get
end

.model_get_weather_run_period(model) ⇒ <OpenStudio::EnvironmentType>

Get the weather run period for the model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (<OpenStudio::EnvironmentType>)

    the weather run period environment type



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/openstudio-standards/sql_file/sql_file.rb', line 54

def self.model_get_weather_run_period(model)
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # get the weather file run period
  ann_env_pd = nil
  sql_file.availableEnvPeriods.each do |env_pd|
    env_type = sql_file.environmentType(env_pd)
    next unless env_type.is_initialized

    if env_type.get == OpenStudio::EnvironmentType.new('WeatherRunPeriod')
      ann_env_pd = env_pd
    end
  end

  # make sure the annual run exists
  unless ann_env_pd
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', 'Cannot find the annual simulation run period.')
    return false
  end

  return ann_env_pd
end

.model_tabular_data_query(model, report_name, table_name, row_name, column_name, units = '*') ⇒ String, Double

Write out a SQL query to retrieve simulation outputs from the TabularDataWithStrings table in the SQL database produced by OpenStudio/EnergyPlus after running a simulation.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • report_name (String)

    Name of the report as defined in the HTM simulation output file

  • table_name (String)

    Name of the table as defined in the HTM simulation output file

  • row_name (String)

    Name of the row as defined in the HTM simulation output file

  • column_name (String)

    Name of the column as defined in the HTM simulation output file

  • units (String) (defaults to: '*')

    Unit of the value to be retrieved

Returns:

  • (String, Double)

    Result of the query



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/openstudio-standards/sql_file/sql_file.rb', line 31

def self.model_tabular_data_query(model, report_name, table_name, row_name, column_name, units = '*')
  # get model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(model)

  # Define the query
  query = "Select Value FROM TabularDataWithStrings WHERE
  ReportName = '#{report_name}' AND
  TableName = '#{table_name}' AND
  RowName = '#{row_name}' AND
  ColumnName = '#{column_name}' AND
  Units = '#{units}'"

  # Run the query if the expected output is a string
  return sql_file.execAndReturnFirstString(query).get if units.empty?

  # Run the query if the expected output is a double
  return sql_file.execAndReturnFirstDouble(query).get
end

.sql_file_safe_load(sql_file_path) ⇒ OpenStudio::SqlFile

Load and return an sql file, or error if not found

Parameters:

  • sql_file_path (String)

    path to the SQL file

Returns:

  • (OpenStudio::SqlFile)

    An OpenStudio SqlFile object, boolean false if not found



8
9
10
11
12
13
14
15
16
17
# File 'lib/openstudio-standards/sql_file/sql_file.rb', line 8

def self.sql_file_safe_load(sql_file_path)
  sql_path = OpenStudio::Path.new(sql_file_path)
  if OpenStudio.exists(sql_path)
    sql = OpenStudio::SqlFile.new(sql_path)
  else
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Invalid file path #{sql_path}.")
    return false
  end
  return sql
end

.thermal_zone_get_annual_occupied_unmet_cooling_hours(thermal_zone) ⇒ Double

Determine the number of unmet occupied cooling load hours for a thermal zone

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Double)

    occupied cooling unmet hours



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 364

def self.thermal_zone_get_annual_occupied_unmet_cooling_hours(thermal_zone)
  # get the model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(thermal_zone.model)

  # run unmet load hours query for the specific thermal zone
  query = "SELECT Value
          FROM tabulardatawithstrings
          WHERE ReportName='SystemSummary'
          AND ReportForString='Entire Facility'
          AND TableName='Time Setpoint Not Met'
          AND ColumnName='During Occupied Cooling'
          AND RowName='#{thermal_zone.name.to_s.upcase}'
          AND Units='hr'"
  umlh = sql_file.execAndReturnFirstDouble(query)
  if umlh.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not get unmet occupied cooling hours for thermal zone #{thermal_zone.name}.")
    return false
  end

  return umlh.get
end

.thermal_zone_get_annual_occupied_unmet_heating_hours(thermal_zone) ⇒ Double

Determine the number of unmet occupied heating load hours for a thermal zone

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Double)

    occupied heating unmet hours



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/openstudio-standards/sql_file/unmet_hours.rb', line 338

def self.thermal_zone_get_annual_occupied_unmet_heating_hours(thermal_zone)
  # get the model sql file
  sql_file = OpenstudioStandards::SqlFile.model_get_sql_file(thermal_zone.model)

  # run unmet load hours query for the specific thermal zone
  query = "SELECT Value
          FROM tabulardatawithstrings
          WHERE ReportName='SystemSummary'
          AND ReportForString='Entire Facility'
          AND TableName='Time Setpoint Not Met'
          AND ColumnName='During Occupied Heating'
          AND RowName='#{thermal_zone.name.to_s.upcase}'
          AND Units='hr'"
  umlh = sql_file.execAndReturnFirstDouble(query)
  if umlh.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.SqlFile', "Could not get unmet occupied heating hours for thermal zone #{thermal_zone.name}.")
    return false
  end

  return umlh.get
end