Module: OpenstudioStandards::ThermalZone

Defined in:
lib/openstudio-standards/thermal_zone/thermal_zone.rb

Class Method Summary collapse

Class Method Details

.thermal_zone_add_unconditioned_thermostat(thermal_zone) ⇒ Boolean

Adds a thermostat that heats the space to 0 F and cools to 120 F. These numbers are outside of the threshold that is considered heated or cooled by thermal_zone_cooled?() and thermal_zone_heated?()

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    returns true if successful, false if not



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 476

def self.thermal_zone_add_unconditioned_thermostat(thermal_zone)
  # Heated to 0F (below thermal_zone_heated?(thermal_zone)  threshold)
  htg_t_f = 0
  htg_t_c = OpenStudio.convert(htg_t_f, 'F', 'C').get
  htg_stpt_sch = OpenStudio::Model::ScheduleRuleset.new(thermal_zone.model)
  htg_stpt_sch.setName('Unconditioned Minimal Heating')
  htg_stpt_sch.defaultDaySchedule.setName('Unconditioned Minimal Heating Default')
  htg_stpt_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), htg_t_c)

  # Cooled to 120F (above thermal_zone_cooled?(thermal_zone)  threshold)
  clg_t_f = 120
  clg_t_c = OpenStudio.convert(clg_t_f, 'F', 'C').get
  clg_stpt_sch = OpenStudio::Model::ScheduleRuleset.new(thermal_zone.model)
  clg_stpt_sch.setName('Unconditioned Minimal Heating')
  clg_stpt_sch.defaultDaySchedule.setName('Unconditioned Minimal Heating Default')
  clg_stpt_sch.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), clg_t_c)

  # Thermostat
  thermostat = OpenStudio::Model::ThermostatSetpointDualSetpoint.new(thermal_zone.model)
  thermostat.setName("#{thermal_zone.name} Unconditioned Thermostat")
  thermostat.setHeatingSetpointTemperatureSchedule(htg_stpt_sch)
  thermostat.setCoolingSetpointTemperatureSchedule(clg_stpt_sch)
  thermal_zone.setThermostatSetpointDualSetpoint(thermostat)

  return true
end

.thermal_zone_convert_outdoor_air_to_per_area(thermal_zone) ⇒ Boolean

Convert total minimum OA requirement to a per-area value.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    returns true if successful, false if not



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

def self.thermal_zone_convert_outdoor_air_to_per_area(thermal_zone)
  # For each space in the zone, convert
  # all design OA to per-area
  # unless the "Outdoor Air Method" is "Maximum"
  thermal_zone.spaces.each do |space|
    # Find the design OA, which may be assigned at either the
    # SpaceType or directly at the Space
    dsn_oa = space.designSpecificationOutdoorAir
    next if dsn_oa.empty?

    dsn_oa = dsn_oa.get
    next if dsn_oa.outdoorAirMethod == 'Maximum'

    # Get the space properties
    floor_area = space.floorArea
    number_of_people = space.numberOfPeople
    volume = space.volume

    # Sum up the total OA from all sources
    oa_for_people = number_of_people * dsn_oa.outdoorAirFlowperPerson
    oa_for_floor_area = floor_area * dsn_oa.outdoorAirFlowperFloorArea
    oa_rate = dsn_oa.outdoorAirFlowRate
    oa_for_volume = volume * dsn_oa.outdoorAirFlowAirChangesperHour / 3600
    tot_oa = oa_for_people + oa_for_floor_area + oa_rate + oa_for_volume

    # Convert total to per-area
    tot_oa_per_area = tot_oa / floor_area

    # Check if there is another design OA object that has already
    # been converted from per-person to per-area that matches.
    # If so, reuse that instead of creating a duplicate.
    new_dsn_oa_name = "#{dsn_oa.name} to per-area"
    if thermal_zone.model.getDesignSpecificationOutdoorAirByName(new_dsn_oa_name).is_initialized
      new_dsn_oa = thermal_zone.model.getDesignSpecificationOutdoorAirByName(new_dsn_oa_name).get
    else
      new_dsn_oa = OpenStudio::Model::DesignSpecificationOutdoorAir.new(thermal_zone.model)
      new_dsn_oa.setName(new_dsn_oa_name)
    end

    # Assign this new design OA to the space
    space.setDesignSpecificationOutdoorAir(new_dsn_oa)

    # Set the method
    new_dsn_oa.setOutdoorAirMethod('Sum')
    # Set the per-area requirement
    new_dsn_oa.setOutdoorAirFlowperFloorArea(tot_oa_per_area)
    # Zero-out the per-person, ACH, and flow requirements
    new_dsn_oa.setOutdoorAirFlowperPerson(0.0)
    new_dsn_oa.setOutdoorAirFlowAirChangesperHour(0.0)
    new_dsn_oa.setOutdoorAirFlowRate(0.0)
    # Copy the orignal OA schedule, if any
    if dsn_oa.outdoorAirFlowRateFractionSchedule.is_initialized
      oa_sch = dsn_oa.outdoorAirFlowRateFractionSchedule.get
      new_dsn_oa.setOutdoorAirFlowRateFractionSchedule(oa_sch)
    end

    OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.ThermalZone', "For #{thermal_zone.name}: Converted total ventilation requirements to per-area value.")
  end

  return true
end

.thermal_zone_cooled?(thermal_zone) ⇒ Boolean

Determines cooling status. If the zone has a thermostat with a minimum cooling setpoint below 33C (91F), counts as cooled. Plenums are also assumed to be cooled.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    returns true if cooled, false if not

Author:

  • Andrew Parker, Julien Marrec



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 243

def self.thermal_zone_cooled?(thermal_zone)
  temp_f = 91.0
  temp_c = OpenStudio.convert(temp_f, 'F', 'C').get
  cld = false

  # Consider plenum zones cooled
  area_plenum = 0
  area_non_plenum = 0
  thermal_zone.spaces.each do |space|
    if OpenstudioStandards::Space.space_plenum?(space)
      area_plenum += space.floorArea
    else
      area_non_plenum += space.floorArea
    end
  end

  # Majority
  if area_plenum > area_non_plenum
    cld = true
    return cld
  end

  # Check if the zone has radiant cooling,
  # and if it does, get cooling setpoint schedule
  # directly from the radiant system to check.
  thermal_zone.equipment.each do |equip|
    clg_sch = nil
    if equip.to_ZoneHVACLowTempRadiantConstFlow.is_initialized
      equip = equip.to_ZoneHVACLowTempRadiantConstFlow.get
      clg_coil = equip.coolingCoil
      if clg_coil.to_CoilCoolingLowTempRadiantConstFlow.is_initialized
        clg_coil = clg_coil.to_CoilCoolingLowTempRadiantConstFlow.get
        if clg_coil.coolingLowControlTemperatureSchedule.is_initialized
          clg_sch = clg_coil.coolingLowControlTemperatureSchedule.get
        end
      end
    elsif equip.to_ZoneHVACLowTempRadiantVarFlow.is_initialized
      equip = equip.to_ZoneHVACLowTempRadiantVarFlow.get
      clg_coil = equip.coolingCoil
      if equip.model.version > OpenStudio::VersionString.new('3.1.0')
        if clg_coil.is_initialized
          clg_coil = clg_coil.get
        else
          clg_coil = nil
        end
      end
      if !clg_coil.nil? && clg_coil.to_CoilCoolingLowTempRadiantVarFlow.is_initialized
        clg_coil = clg_coil.to_CoilCoolingLowTempRadiantVarFlow.get
        if clg_coil.coolingControlTemperatureSchedule.is_initialized
          clg_sch = clg_coil.coolingControlTemperatureSchedule.get
        end
      end
    end
    # Move on if no cooling schedule was found
    next if clg_sch.nil?

    # Get the setpoint from the schedule
    if clg_sch.to_ScheduleRuleset.is_initialized
      clg_sch = clg_sch.to_ScheduleRuleset.get
      min_c = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(clg_sch)['min']
      if min_c < temp_c
        cld = true
      end
    elsif clg_sch.to_ScheduleConstant.is_initialized
      clg_sch = clg_sch.to_ScheduleConstant.get
      min_c = OpenstudioStandards::Schedules.schedule_constant_get_min_max(clg_sch)['min']
      if min_c < temp_c
        cld = true
      end
    elsif clg_sch.to_ScheduleCompact.is_initialized
      clg_sch = clg_sch.to_ScheduleCompact.get
      min_c = OpenstudioStandards::Schedules.schedule_compact_get_min_max(clg_sch)['min']
      if min_c < temp_c
        cld = true
      end
    else
      OpenStudio.logFree(OpenStudio::Debug, 'OpenstudioStandards::ThermalZone', "Zone #{thermal_zone.name} used an unknown schedule type for the cooling setpoint; assuming cooled.")
      cld = true
    end
  end

  # Unheated if no thermostat present
  if thermal_zone.thermostat.empty?
    return cld
  end

  # Check the cooling setpoint
  tstat = thermal_zone.thermostat.get
  if tstat.to_ThermostatSetpointDualSetpoint
    tstat = tstat.to_ThermostatSetpointDualSetpoint.get
    clg_sch = tstat.getCoolingSchedule
    if clg_sch.is_initialized
      clg_sch = clg_sch.get
      if clg_sch.to_ScheduleRuleset.is_initialized
        clg_sch = clg_sch.to_ScheduleRuleset.get
        min_c = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(clg_sch)['min']
        if min_c < temp_c
          cld = true
        end
      elsif clg_sch.to_ScheduleConstant.is_initialized
        clg_sch = clg_sch.to_ScheduleConstant.get
        min_c = OpenstudioStandards::Schedules.schedule_constant_get_min_max(clg_sch)['min']
        if min_c < temp_c
          cld = true
        end
      elsif clg_sch.to_ScheduleCompact.is_initialized
        clg_sch = clg_sch.to_ScheduleCompact.get
        min_c = OpenstudioStandards::Schedules.schedule_compact_get_min_max(clg_sch)['min']
        if min_c < temp_c
          cld = true
        end
      else
        OpenStudio.logFree(OpenStudio::Debug, 'OpenstudioStandards::ThermalZone', "Zone #{thermal_zone.name} used an unknown schedule type for the cooling setpoint; assuming cooled.")
        cld = true
      end
    end
  elsif tstat.to_ZoneControlThermostatStagedDualSetpoint
    tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get
    clg_sch = tstat.coolingTemperatureSetpointSchedule
    if clg_sch.is_initialized
      clg_sch = clg_sch.get
      if clg_sch.to_ScheduleRuleset.is_initialized
        clg_sch = clg_sch.to_ScheduleRuleset.get
        min_c = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(clg_sch)['min']
        if min_c < temp_c
          cld = true
        end
      end
    end
  elsif tstat.to_ThermostatSetpointSingleHeating
    cld = false
  end

  return cld
end

.thermal_zone_district_heat?(thermal_zone) ⇒ Boolean

Determine if the thermal zone is heated by district or purchased heat. This will return true if there is any fossil heat, even if not the primary heating source. As an example, a zone served by a VRF + DOAS system will show as district heated if the DOAS ventilation air is heated by a district system

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    true if heated by district heat, false if electric or other.



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 438

def self.thermal_zone_district_heat?(thermal_zone)
  # error if HVACComponent heating fuels method is not available
  if thermal_zone.model.version < OpenStudio::VersionString.new('3.6.0')
    OpenStudio.logFree(OpenStudio::Error, 'OpenstudioStandards::ThermalZone', 'Required HVACComponent method .heatingFuelTypes is not available in pre-OpenStudio 3.6.0 versions. Use a more recent version of OpenStudio.')
  end

  is_district = false
  # Get an array of the heating fuels used by the zone
  htg_fuels = thermal_zone.heatingFuelTypes.map(&:valueName)
  if htg_fuels.include?('DistrictHeating') ||
     htg_fuels.include?('DistrictHeatingWater') ||
     htg_fuels.include?('DistrictHeatingSteam')

    is_district = true
  end

  return is_district
end

.thermal_zone_electric_heat?(thermal_zone) ⇒ Boolean

Determine if the thermal zone is heated by electricity. This will return true if there is any electric heat, even if not the primary heating source.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    true if heated by electricity, false if fossil fuel or other.



384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 384

def self.thermal_zone_electric_heat?(thermal_zone)
  # error if HVACComponent heating fuels method is not available
  if thermal_zone.model.version < OpenStudio::VersionString.new('3.6.0')
    OpenStudio.logFree(OpenStudio::Error, 'OpenstudioStandards::ThermalZone', 'Required HVACComponent method .heatingFuelTypes is not available in pre-OpenStudio 3.6.0 versions. Use a more recent version of OpenStudio.')
  end

  # Get an array of the heating fuels used by the zone
  htg_fuels = thermal_zone.heatingFuelTypes.map(&:valueName)
  is_electric = htg_fuels.include?('Electricity')

  return is_electric
end

.thermal_zone_fossil_heat?(thermal_zone) ⇒ Boolean

Determine if the thermal zone is heated by a fossil fuel. This will return true if there is any fossil heat, even if not the primary heating source. As an example, a zone served by a VRF + DOAS system will show as fossil heated if the DOAS ventilation air is fossil heated

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    true if heated by fossil fuel, false if electric or other.



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 404

def self.thermal_zone_fossil_heat?(thermal_zone)
  # error if HVACComponent heating fuels method is not available
  if thermal_zone.model.version < OpenStudio::VersionString.new('3.6.0')
    OpenStudio.logFree(OpenStudio::Error, 'OpenstudioStandards::ThermalZone', 'Required HVACComponent method .heatingFuelTypes is not available in pre-OpenStudio 3.6.0 versions. Use a more recent version of OpenStudio.')
  end

  is_fossil = false
  # Get an array of the heating fuels used by the zone
  htg_fuels = thermal_zone.heatingFuelTypes.map(&:valueName)
  if htg_fuels.include?('Gas') ||
     htg_fuels.include?('NaturalGas') ||
     htg_fuels.include?('Propane') ||
     htg_fuels.include?('PropaneGas') ||
     htg_fuels.include?('FuelOil_1') ||
     htg_fuels.include?('FuelOilNo1') ||
     htg_fuels.include?('FuelOil_2') ||
     htg_fuels.include?('FuelOilNo2') ||
     htg_fuels.include?('Coal') ||
     htg_fuels.include?('Diesel') ||
     htg_fuels.include?('Gasoline')

    is_fossil = true
  end

  return is_fossil
end

.thermal_zone_get_building_type(thermal_zone) ⇒ String

Returns the standards building type that represents the majority of floor area.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (String)

    the standards building type



548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 548

def self.thermal_zone_get_building_type(thermal_zone)
  # determine areas of each building type
  building_type_areas = {}
  thermal_zone.spaces.each do |space|
    # ignore space if not part of total area
    next unless space.partofTotalFloorArea

    if space.spaceType.is_initialized
      space_type = space.spaceType.get
      if space_type.standardsBuildingType.is_initialized
        building_type = space_type.standardsBuildingType.get
        if building_type_areas[building_type].nil?
          building_type_areas[building_type] = space.floorArea
        else
          building_type_areas[building_type] += space.floorArea
        end
      end
    end
  end

  # return largest building type area
  building_type = building_type_areas.key(building_type_areas.values.max)

  if building_type.nil?
    OpenStudio.logFree(OpenStudio::Info, 'OpenstudioStandards::ThermalZone', "Thermal zone #{thermal_zone.name} does not have standards building type.")
  end

  return building_type
end

.thermal_zone_get_design_internal_load(thermal_zone) ⇒ Double

Determine the design internal load (W) for this zone without space multipliers. This include People, Lights, Electric Equipment, and Gas Equipment in all spaces in this zone. It assumes 100% of the wattage is converted to heat, and that the design peak schedule value is 1 (100%).

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Double)

    the design internal load, in watts



509
510
511
512
513
514
515
516
517
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 509

def self.thermal_zone_get_design_internal_load(thermal_zone)
  load_w = 0.0

  thermal_zone.spaces.each do |space|
    load_w += OpenstudioStandards::Space.space_get_design_internal_load(space)
  end

  return load_w
end

.thermal_zone_get_occupancy_schedule(thermal_zone, sch_name: nil, occupied_percentage_threshold: nil) ⇒ <OpenStudio::Model::ScheduleRuleset>

This method creates a new fractional schedule ruleset. If occupied_percentage_threshold is set, this method will return a discrete on/off fractional schedule with a value of one when occupancy across all spaces is greater than or equal to the occupied_percentage_threshold, and zero all other times. Otherwise the method will return the weighted fractional occupancy schedule.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

  • sch_name (String) (defaults to: nil)

    the name of the generated occupancy schedule

  • occupied_percentage_threshold (Double) (defaults to: nil)

    the minimum fraction (0 to 1) that counts as occupied if this parameter is set, the returned ScheduleRuleset will be 0 = unoccupied, 1 = occupied otherwise the ScheduleRuleset will be the weighted fractional occupancy schedule

Returns:

  • (<OpenStudio::Model::ScheduleRuleset>)

    OpenStudio ScheduleRuleset of fractional or discrete occupancy



589
590
591
592
593
594
595
596
597
598
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 589

def self.thermal_zone_get_occupancy_schedule(thermal_zone, sch_name: nil, occupied_percentage_threshold: nil)
  if sch_name.nil?
    sch_name = "#{thermal_zone.name} Occ Sch"
  end
  # Get the occupancy schedule for all spaces in thermal_zone
  sch_ruleset = OpenstudioStandards::Space.spaces_get_occupancy_schedule(thermal_zone.spaces,
                                                                         sch_name: sch_name,
                                                                         occupied_percentage_threshold: occupied_percentage_threshold)
  return sch_ruleset
end

.thermal_zone_get_outdoor_airflow_rate(thermal_zone) ⇒ Double

Calculates the zone outdoor airflow requirement (Voz) based on the inputs in the DesignSpecification:OutdoorAir objects in all spaces in the zone.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Double)

    the zone outdoor air flow rate in cubic meters per second (m^3/s)



633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 633

def self.thermal_zone_get_outdoor_airflow_rate(thermal_zone)
  tot_oa_flow_rate = 0.0

  spaces = thermal_zone.spaces.sort

  sum_floor_area = 0.0
  sum_number_of_people = 0.0
  sum_volume = 0.0

  # Variables for merging outdoor air
  any_max_oa_method = false
  sum_oa_for_people = 0.0
  sum_oa_for_floor_area = 0.0
  sum_oa_rate = 0.0
  sum_oa_for_volume = 0.0

  # Find common variables for the new space
  spaces.each do |space|
    floor_area = space.floorArea
    sum_floor_area += floor_area

    number_of_people = space.numberOfPeople
    sum_number_of_people += number_of_people

    volume = space.volume
    sum_volume += volume

    dsn_oa = space.designSpecificationOutdoorAir
    next if dsn_oa.empty?

    dsn_oa = dsn_oa.get

    # compute outdoor air rates in case we need them
    oa_for_people = number_of_people * dsn_oa.outdoorAirFlowperPerson
    oa_for_floor_area = floor_area * dsn_oa.outdoorAirFlowperFloorArea
    oa_rate = dsn_oa.outdoorAirFlowRate
    oa_for_volume = volume * dsn_oa.outdoorAirFlowAirChangesperHour / 3600

    # First check if this space uses the Maximum method and other spaces do not
    if dsn_oa.outdoorAirMethod == 'Maximum'
      sum_oa_rate += [oa_for_people, oa_for_floor_area, oa_rate, oa_for_volume].max
    elsif dsn_oa.outdoorAirMethod == 'Sum'
      sum_oa_for_people += oa_for_people
      sum_oa_for_floor_area += oa_for_floor_area
      sum_oa_rate += oa_rate
      sum_oa_for_volume += oa_for_volume
    end
  end

  tot_oa_flow_rate += sum_oa_for_people
  tot_oa_flow_rate += sum_oa_for_floor_area
  tot_oa_flow_rate += sum_oa_rate
  tot_oa_flow_rate += sum_oa_for_volume

  # Convert to cfm
  tot_oa_flow_rate_cfm = OpenStudio.convert(tot_oa_flow_rate, 'm^3/s', 'cfm').get

  OpenStudio.logFree(OpenStudio::Debug, 'openstudio.Standards.ThermalZone', "For #{thermal_zone.name}, design min OA = #{tot_oa_flow_rate_cfm.round} cfm.")

  return tot_oa_flow_rate
end

.thermal_zone_get_outdoor_airflow_rate_per_area(thermal_zone) ⇒ Double

Calculates the zone outdoor airflow requirement and divides by the zone area.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Double)

    the zone outdoor air flow rate per area in cubic meters per second (m^3/s)



699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 699

def self.thermal_zone_get_outdoor_airflow_rate_per_area(thermal_zone)
  tot_oa_flow_rate_per_area = 0.0

  # Find total area of the zone
  sum_floor_area = 0.0
  thermal_zone.spaces.sort.each do |space|
    sum_floor_area += space.floorArea
  end

  # Get the OA flow rate
  tot_oa_flow_rate = OpenstudioStandards::ThermalZone.thermal_zone_get_outdoor_airflow_rate(thermal_zone)

  # Calculate the per-area value
  tot_oa_flow_rate_per_area = tot_oa_flow_rate / sum_floor_area

  # 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.")

  return tot_oa_flow_rate_per_area
end

.thermal_zone_get_space_type(thermal_zone) ⇒ Boost::Optional<OpenStudio::Model::SpaceType>

Returns the space type that represents a majority of the floor area.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boost::Optional<OpenStudio::Model::SpaceType>)

    An OptionalSpaceType



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 523

def self.thermal_zone_get_space_type(thermal_zone)
  space_type_to_area = Hash.new(0.0)

  thermal_zone.spaces.each do |space|
    if space.spaceType.is_initialized
      space_type = space.spaceType.get
      space_type_to_area[space_type] += space.floorArea
    end
  end

  # If no space types, return empty optional SpaceType
  if space_type_to_area.empty?
    return OpenStudio::Model::OptionalSpaceType.new
  end

  # Sort by area
  biggest_space_type = space_type_to_area.sort_by { |st, area| area }.reverse[0][0]

  return OpenStudio::Model::OptionalSpaceType.new(biggest_space_type)
end

.thermal_zone_heated?(thermal_zone) ⇒ Boolean

Determines heating status. If the zone has a thermostat with a maximum heating setpoint above 5C (41F), counts as heated. Plenums are also assumed to be heated.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    returns true if heated, false if not

Author:

  • Andrew Parker, Julien Marrec



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 94

def self.thermal_zone_heated?(thermal_zone)
  temp_f = 41.0
  temp_c = OpenStudio.convert(temp_f, 'F', 'C').get
  htd = false

  # Consider plenum zones heated
  area_plenum = 0
  area_non_plenum = 0
  thermal_zone.spaces.each do |space|
    if OpenstudioStandards::Space.space_plenum?(space)
      area_plenum += space.floorArea
    else
      area_non_plenum += space.floorArea
    end
  end

  # Majority
  if area_plenum > area_non_plenum
    htd = true
    return htd
  end

  # Check if the zone has radiant heating,
  # and if it does, get heating setpoint schedule
  # directly from the radiant system to check.
  thermal_zone.equipment.each do |equip|
    htg_sch = nil
    if equip.to_ZoneHVACHighTemperatureRadiant.is_initialized
      equip = equip.to_ZoneHVACHighTemperatureRadiant.get
      if equip.heatingSetpointTemperatureSchedule.is_initialized
        htg_sch = equip.heatingSetpointTemperatureSchedule.get
      end
    elsif equip.to_ZoneHVACLowTemperatureRadiantElectric.is_initialized
      equip = equip.to_ZoneHVACLowTemperatureRadiantElectric.get
      htg_sch = equip.heatingSetpointTemperatureSchedule
    elsif equip.to_ZoneHVACLowTempRadiantConstFlow.is_initialized
      equip = equip.to_ZoneHVACLowTempRadiantConstFlow.get
      htg_coil = equip.heatingCoil
      if htg_coil.to_CoilHeatingLowTempRadiantConstFlow.is_initialized
        htg_coil = htg_coil.to_CoilHeatingLowTempRadiantConstFlow.get
        if htg_coil.heatingHighControlTemperatureSchedule.is_initialized
          htg_sch = htg_coil.heatingHighControlTemperatureSchedule.get
        end
      end
    elsif equip.to_ZoneHVACLowTempRadiantVarFlow.is_initialized
      equip = equip.to_ZoneHVACLowTempRadiantVarFlow.get
      htg_coil = equip.heatingCoil
      if equip.model.version > OpenStudio::VersionString.new('3.1.0')
        if htg_coil.is_initialized
          htg_coil = htg_coil.get
        else
          htg_coil = nil
        end
      end
      if !htg_coil.nil? && htg_coil.to_CoilHeatingLowTempRadiantVarFlow.is_initialized
        htg_coil = htg_coil.to_CoilHeatingLowTempRadiantVarFlow.get
        if htg_coil.heatingControlTemperatureSchedule.is_initialized
          htg_sch = htg_coil.heatingControlTemperatureSchedule.get
        end
      end
    end
    # Move on if no heating schedule was found
    next if htg_sch.nil?

    # Get the setpoint from the schedule
    if htg_sch.to_ScheduleRuleset.is_initialized
      htg_sch = htg_sch.to_ScheduleRuleset.get
      max_c = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(htg_sch)['max']
      if max_c > temp_c
        htd = true
      end
    elsif htg_sch.to_ScheduleConstant.is_initialized
      htg_sch = htg_sch.to_ScheduleConstant.get
      max_c = OpenstudioStandards::Schedules.schedule_constant_get_min_max(htg_sch)['max']
      if max_c > temp_c
        htd = true
      end
    elsif htg_sch.to_ScheduleCompact.is_initialized
      htg_sch = htg_sch.to_ScheduleCompact.get
      max_c = OpenstudioStandards::Schedules.schedule_compact_get_min_max(htg_sch)['max']
      if max_c > temp_c
        htd = true
      end
    else
      OpenStudio.logFree(OpenStudio::Debug, 'OpenstudioStandards::ThermalZone', "Zone #{thermal_zone.name} used an unknown schedule type for the heating setpoint; assuming heated.")
      htd = true
    end
  end

  # Unheated if no thermostat present
  if thermal_zone.thermostat.empty?
    return htd
  end

  # Check the heating setpoint
  tstat = thermal_zone.thermostat.get
  if tstat.to_ThermostatSetpointDualSetpoint
    tstat = tstat.to_ThermostatSetpointDualSetpoint.get
    htg_sch = tstat.getHeatingSchedule
    if htg_sch.is_initialized
      htg_sch = htg_sch.get
      if htg_sch.to_ScheduleRuleset.is_initialized
        htg_sch = htg_sch.to_ScheduleRuleset.get
        max_c = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(htg_sch)['max']
        if max_c > temp_c
          htd = true
        end
      elsif htg_sch.to_ScheduleConstant.is_initialized
        htg_sch = htg_sch.to_ScheduleConstant.get
        max_c = OpenstudioStandards::Schedules.schedule_constant_get_min_max(htg_sch)['max']
        if max_c > temp_c
          htd = true
        end
      elsif htg_sch.to_ScheduleCompact.is_initialized
        htg_sch = htg_sch.to_ScheduleCompact.get
        max_c = OpenstudioStandards::Schedules.schedule_compact_get_min_max(htg_sch)['max']
        if max_c > temp_c
          htd = true
        end
      else
        OpenStudio.logFree(OpenStudio::Debug, 'OpenstudioStandards::ThermalZone', "Zone #{thermal_zone.name} used an unknown schedule type for the heating setpoint; assuming heated.")
        htd = true
      end
    end
  elsif tstat.to_ZoneControlThermostatStagedDualSetpoint
    tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get
    htg_sch = tstat.heatingTemperatureSetpointSchedule
    if htg_sch.is_initialized
      htg_sch = htg_sch.get
      if htg_sch.to_ScheduleRuleset.is_initialized
        htg_sch = htg_sch.to_ScheduleRuleset.get
        max_c = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(htg_sch)['max']
        if max_c > temp_c
          htd = true
        end
      end
    end
  end

  return htd
end

.thermal_zone_mixed_heat?(thermal_zone) ⇒ Boolean

Determine if the thermal zone is heated by two or more of electricity, fossil fuel, and district or purchased heat.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    true if mixed fossil, electric, and district/purchased heat, false if not



461
462
463
464
465
466
467
468
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 461

def self.thermal_zone_mixed_heat?(thermal_zone)
  electric_heat = OpenstudioStandards::ThermalZone.thermal_zone_electric_heat?(thermal_zone)
  fossil_heat = OpenstudioStandards::ThermalZone.thermal_zone_fossil_heat?(thermal_zone)
  district_heat = OpenstudioStandards::ThermalZone.thermal_zone_district_heat?(thermal_zone)
  is_mixed = [electric_heat, fossil_heat, district_heat].count(true) > 1

  return is_mixed
end

.thermal_zone_plenum?(thermal_zone) ⇒ Boolean

Determine if the thermal zone is a plenum based on whether a majority of the spaces in the zone are plenums or not.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    returns true if majority plenum, false if not



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 8

def self.thermal_zone_plenum?(thermal_zone)
  plenum_status = false

  area_plenum = 0
  area_non_plenum = 0
  thermal_zone.spaces.each do |space|
    if OpenstudioStandards::Space.space_plenum?(space)
      area_plenum += space.floorArea
    else
      area_non_plenum += space.floorArea
    end
  end

  # Majority
  if area_plenum > area_non_plenum
    plenum_status = true
  end

  return plenum_status
end

.thermal_zone_residential?(thermal_zone) ⇒ Boolean

Determine if the thermal zone is residential based on the space type properties for the spaces in the zone. If there are both residential and nonresidential spaces in the zone, the result will be whichever type has more floor area. In the event that they are equal, it will be assumed nonresidential.

return [Boolean] true if residential, false if nonresidential

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 36

def self.thermal_zone_residential?(thermal_zone)
  # Determine the respective areas
  res_area_m2 = 0
  nonres_area_m2 = 0
  thermal_zone.spaces.each do |space|
    # Ignore space if not part of total area
    next unless space.partofTotalFloorArea

    if OpenstudioStandards::Space.space_residential?(space)
      res_area_m2 += space.floorArea
    else
      nonres_area_m2 += space.floorArea
    end
  end

  # Determine which is larger
  is_res = false
  if res_area_m2 > nonres_area_m2
    is_res = true
  end

  return is_res
end

.thermal_zone_vestibule?(thermal_zone) ⇒ Boolean

Determine if this zone is a vestibule. Zone must be less than 200 ft^2 and also have an infiltration object specified using Flow/Zone.

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

Returns:

  • (Boolean)

    returns true if vestibule, false if not



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 65

def self.thermal_zone_vestibule?(thermal_zone)
  is_vest = false

  # Check area
  unless thermal_zone.floorArea < OpenStudio.convert(200, 'ft^2', 'm^2').get
    return is_vest
  end

  # Check presence of infiltration
  thermal_zone.spaces.each do |space|
    space.spaceInfiltrationDesignFlowRates.each do |infil|
      if infil.designFlowRate.is_initialized
        is_vest = true
        OpenStudio.logFree(OpenStudio::Info, 'OpenstudioStandards::ThermalZone', "For #{thermal_zone.name}: This zone is considered a vestibule.")
        break
      end
    end
  end

  return is_vest
end

.thermal_zones_get_occupancy_schedule(thermal_zones, sch_name: nil, occupied_percentage_threshold: nil) ⇒ <OpenStudio::Model::ScheduleRuleset>

This method creates a new fractional schedule ruleset. If occupied_percentage_threshold is set, this method will return a discrete on/off fractional schedule with a value of one when occupancy across all spaces is greater than or equal to the occupied_percentage_threshold, and zero all other times. Otherwise the method will return the weighted fractional occupancy schedule.

Parameters:

  • thermal_zones (Array<OpenStudio::Model::ThermalZone>)

    Array of OpenStudio ThermalZone objects

  • sch_name (String) (defaults to: nil)

    the name of the generated occupancy schedule

  • occupied_percentage_threshold (Double) (defaults to: nil)

    the minimum fraction (0 to 1) that counts as occupied if this parameter is set, the returned ScheduleRuleset will be 0 = unoccupied, 1 = occupied otherwise the ScheduleRuleset will be the weighted fractional occupancy schedule

Returns:

  • (<OpenStudio::Model::ScheduleRuleset>)

    OpenStudio ScheduleRuleset of fractional or discrete occupancy



611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
# File 'lib/openstudio-standards/thermal_zone/thermal_zone.rb', line 611

def self.thermal_zones_get_occupancy_schedule(thermal_zones, sch_name: nil, occupied_percentage_threshold: nil)
  if sch_name.nil?
    sch_name = "#{thermal_zones.size} zone Occ Sch"
  end
  # Get the occupancy schedule for all spaces in thermal_zones
  spaces = []
  thermal_zones.each do |thermal_zone|
    thermal_zone.spaces.each do |space|
      spaces << space
    end
  end
  sch_ruleset = OpenstudioStandards::Space.spaces_get_occupancy_schedule(spaces,
                                                                         sch_name: sch_name,
                                                                         occupied_percentage_threshold: occupied_percentage_threshold)
  return sch_ruleset
end