Class: SetWindowToWallRatioByFacade

Inherits:
OpenStudio::Measure::ModelMeasure
  • Object
show all
Defined in:
lib/measures/SetWindowToWallRatioByFacade/measure.rb

Instance Method Summary collapse

Instance Method Details

#arguments(model) ⇒ Object

return a vector of arguments



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
# File 'lib/measures/SetWindowToWallRatioByFacade/measure.rb', line 15

def arguments(model)
  args = OpenStudio::Measure::OSArgumentVector.new

  # make double argument for wwr
  wwr = OpenStudio::Measure::OSArgument.makeDoubleArgument('wwr', true)
  wwr.setDisplayName('Window to Wall Ratio (fraction).')
  wwr.setDefaultValue(0.4)
  args << wwr

  # make double argument for sillHeight
  sillHeight = OpenStudio::Measure::OSArgument.makeDoubleArgument('sillHeight', true)
  sillHeight.setDisplayName('Sill Height (in).')
  sillHeight.setDefaultValue(30.0)
  args << sillHeight

  # make choice argument for facade
  choices = OpenStudio::StringVector.new
  choices << 'North'
  choices << 'East'
  choices << 'South'
  choices << 'West'
  choices << 'All'
  facade = OpenStudio::Measure::OSArgument.makeChoiceArgument('facade', choices, true)
  facade.setDisplayName('Cardinal Direction.')
  facade.setDefaultValue('South')
  args << facade

  # bool to not apply windows to spaces that are not included in the building floor area
  exl_spaces_not_incl_fl_area = OpenStudio::Measure::OSArgument.makeBoolArgument('exl_spaces_not_incl_fl_area', true)
  exl_spaces_not_incl_fl_area.setDisplayName("Don't alter spaces that are not included in the building floor area")
  exl_spaces_not_incl_fl_area.setDefaultValue(true)
  args << exl_spaces_not_incl_fl_area

  # make an argument for splitting base surfaces at doors
  choices = OpenStudio::StringVector.new
  choices << 'Do nothing to Doors'
  choices << 'Split Walls at Doors'
  choices << 'Remove Doors'
  split_at_doors = OpenStudio::Measure::OSArgument.makeChoiceArgument('split_at_doors', choices, true)
  split_at_doors.setDisplayName('Exterior Door Logic')
  split_at_doors.setDescription('This will only impact exterior surfaces with specified orientation. Can do nothing, split all, or remove doors.')
  split_at_doors.setDefaultValue('Split Walls at Doors')
  args << split_at_doors

  # bool to create inset windows for triangular base surfaces
  inset_tri_sub = OpenStudio::Measure::OSArgument.makeBoolArgument('inset_tri_sub', true)
  inset_tri_sub.setDisplayName('Inset windows for triangular surfaces')
  inset_tri_sub.setDefaultValue(true)
  args << inset_tri_sub

  # triangulate non rectangular base surfaces
  triangulate = OpenStudio::Measure::OSArgument.makeBoolArgument('triangulate', true)
  triangulate.setDisplayName('Triangulate non-Rectangular surfaces')
  triangulate.setDescription('This will only impact exterior surfaces with specified orientation')
  triangulate.setDefaultValue(true)
  args << triangulate

  # triangulation minimum area
  triangulation_min_area = OpenStudio::Measure::OSArgument.makeDoubleArgument('triangulation_min_area', true)
  triangulation_min_area.setDisplayName('Triangulation Minimum Area (m^2)')
  triangulation_min_area.setDescription('Triangulated surfaces less than this will not be created.')
  triangulation_min_area.setDefaultValue(0.001) # Two vertices < 0.01 meters apart are coincident in EnergyPlus (SurfaceGeometry.cc).
  args << triangulation_min_area

  return args
end

#nameObject

override name to return the name of your script



10
11
12
# File 'lib/measures/SetWindowToWallRatioByFacade/measure.rb', line 10

def name
  return 'Set Window to Wall Ratio by Facade'
end

#run(model, runner, user_arguments) ⇒ Object

define what happens when the measure is run



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/measures/SetWindowToWallRatioByFacade/measure.rb', line 83

def run(model, runner, user_arguments)
  super(model, runner, user_arguments)

  # use the built-in error checking
  if !runner.validateUserArguments(arguments(model), user_arguments)
    return false
  end

  # assign the user inputs to variables
  wwr = runner.getDoubleArgumentValue('wwr', user_arguments)
  sillHeight = runner.getDoubleArgumentValue('sillHeight', user_arguments)
  facade = runner.getStringArgumentValue('facade', user_arguments)
  exl_spaces_not_incl_fl_area = runner.getBoolArgumentValue('exl_spaces_not_incl_fl_area', user_arguments)
  split_at_doors = runner.getStringArgumentValue('split_at_doors', user_arguments)
  inset_tri_sub = runner.getBoolArgumentValue('inset_tri_sub', user_arguments)
  triangulate = runner.getBoolArgumentValue('triangulate', user_arguments)
  triangulation_min_area = runner.getDoubleArgumentValue('triangulation_min_area', user_arguments)

  # check reasonableness of fraction
  if wwr == 0
    runner.registerInfo('Target window to wall ratio is 0. Windows for selected surfaces will be removed, no new windows will be added.')
  elsif (wwr < 0) || (wwr >= 1)
    runner.registerError('Window to Wall Ratio must be greater than or equal to 0 and less than 1.')
    return false
  end

  # check reasonableness of fraction
  if sillHeight <= 0
    runner.registerError('Sill height must be > 0.')
    return false
  elsif sillHeight > 360
    runner.registerWarning("#{sillHeight} inches seems like an unusually high sill height.")
  elsif sillHeight > 9999
    runner.registerError("#{sillHeight} inches is above the measure limit for sill height.")
    return false
  end

  # setup OpenStudio units that we will need
  unit_sillHeight_ip = OpenStudio.createUnit('ft').get
  unit_sillHeight_si = OpenStudio.createUnit('m').get
  unit_area_ip = OpenStudio.createUnit('ft^2').get
  unit_area_si = OpenStudio.createUnit('m^2').get
  unit_cost_per_area_ip = OpenStudio.createUnit('1/ft^2').get # $/ft^2 does not work
  unit_cost_per_area_si = OpenStudio.createUnit('1/m^2').get

  # define starting units
  sillHeight_ip = OpenStudio::Quantity.new(sillHeight / 12, unit_sillHeight_ip)

  # unit conversion
  sillHeight_si = OpenStudio.convert(sillHeight_ip, unit_sillHeight_si).get

  # hold data for initial condition
  starting_gross_ext_wall_area = 0.0 # includes windows and doors
  starting_ext_window_area = 0.0

  # hold data for final condition
  final_gross_ext_wall_area = 0.0 # includes windows and doors
  final_ext_window_area = 0.0

  # flag for not applicable
  exterior_walls = false
  window_confirmed = false

  # flag to track notifications of zone multipliers
  space_warning_issued = []

  # flag to track warning for new windows without construction
  facade_const_warning = false
  bldg_const_warning = false
  empty_const_warning = false

  # flag for catchall glazing to be made only once
  catchall_glazing_const = nil

  # calculate initial envelope cost as negative value
  envelope_cost = 0
  constructions = model.getConstructions.sort
  constructions.each do |construction|
    const_llcs = construction.lifeCycleCosts
    const_llcs.each do |const_llc|
      if const_llc.category == 'Construction'
        envelope_cost += const_llc.totalCost * -1
      end
    end
  end

  # loop through surfaces finding exterior walls with proper orientation
  if exl_spaces_not_incl_fl_area
    # loop through spaces to gather surfaces.
    surfaces = []
    model.getSpaces.sort.each do |space|
      next if !space.partofTotalFloorArea

      space.surfaces.sort.each do |surface|
        surfaces << surface
      end
    end
  else
    surfaces = model.getSurfaces.sort
  end

  # used for new sub surfaces to find target construction
  subsurfaces_by_facade = Functions.get_surfaces_or_subsurfaces_by_facade(model.getSubSurfaces, facade)
  orig_sub_surf_const_for_target_facade = Functions.get_orig_sub_surf_const_for_target(subsurfaces_by_facade)
  orig_sub_surf_const_for_target_all_ext = Functions.get_orig_sub_surf_const_for_target(model.getSubSurfaces)

  # hash for sub surfaces removed from non rectangular surfaces
  non_rect_parent = {}

  Functions.get_surfaces_or_subsurfaces_by_facade(model.getSurfaces, facade).sort.each do |s|
    exterior_walls = true

    # get surface area adjusting for zone multiplier
    space = s.space
    if !space.empty?
      zone = space.get.thermalZone
    end
    if !zone.empty?
      zone_multiplier = zone.get.multiplier
      if (zone_multiplier > 1) && !space_warning_issued.include?(space.get.name.to_s)
        runner.registerInfo("Space #{space.get.name} in thermal zone #{zone.get.name} has a zone multiplier of #{zone_multiplier}. Adjusting area calculations.")
        space_warning_issued << space.get.name.to_s
      end
    else
      zone_multiplier = 1 # space is not in a thermal zone
      runner.registerWarning("Space #{space.get.name} is not in a thermal zone and won't be included in in the simulation. Windows will still be altered with an assumed zone multiplier of 1")
    end
    surface_gross_area = s.grossArea * zone_multiplier

    # loop through sub surfaces and add area including multiplier
    ext_window_area = 0
    has_doors = false
    s.subSurfaces.sort.each do |subSurface|
      # stop if non window or glass door
      if subSurface.subSurfaceType == 'Door' || subSurface.subSurfaceType == 'OverheadDoor'
        if split_at_doors == 'Remove Doors'
          subSurface.remove
        else
          has_doors = true
        end
        next
      end
      ext_window_area += subSurface.grossArea * subSurface.multiplier * zone_multiplier
      if subSurface.multiplier > 1
        runner.registerInfo("Sub-surface #{subSurface.name} in space #{space.get.name} has a sub-surface multiplier of #{subSurface.multiplier}. Adjusting area calculations.")
      end
    end

    starting_gross_ext_wall_area += surface_gross_area
    starting_ext_window_area += ext_window_area

    all_surfaces = [s]
    if split_at_doors == 'Split Walls at Doors' && has_doors

      # remove windows before split at doors
      s.subSurfaces.each do |sub_surface|
        next if ['Door', 'OverheadDoor'].include? sub_surface.subSurfaceType

        sub_surface.remove
      end

      # split base surfaces at doors to create  multiple base surfaces
      split_surfaces = s.splitSurfaceForSubSurfaces.to_a # frozen array

      # add original surface to new surfaces
      split_surfaces.sort.each do |ss|
        all_surfaces << ss
      end
    end

    if wwr > 0 && triangulate

      all_surfaces2 = []
      all_surfaces.sort.each do |ss|
        # see if surface is rectangular (only checking non rotated on vertical wall)
        # todo - add in more robust rectangle check that can look for rotate and tilted rectangles
        rect_tri = Functions.rectangle?(ss)

        has_doors = false
        ss.subSurfaces.sort.each do |subSurface|
          if subSurface.subSurfaceType == 'Door' || subSurface.subSurfaceType == 'OverheadDoor'
            has_doors = true
          end
        end

        if has_doors || rect_tri
          all_surfaces2 << ss
          next
        end

        # add triangulated surfaces
        # todo - bring in more attributes

        # get construction from sub-surfaces and then delete them
        pre_tri_sub_const = {}
        ss.subSurfaces.sort.each do |subSurface|
          if subSurface.construction.is_initialized && !subSurface.isConstructionDefaulted
            if pre_tri_sub_const.key?(subSurface.construction.get)
              pre_tri_sub_const[subSurface.construction.get] = subSurface.grossArea
            else
              pre_tri_sub_const[subSurface.construction.get] = + subSurface.grossArea
            end
          end
          subSurface.remove
        end

        # triangulate surface
        ss.triangulation.each do |tri|
          new_surface = OpenStudio::Model::Surface.new(tri, model)
          if new_surface.grossArea < triangulation_min_area
            runner.registerWarning("triangulation produced a surface with area less than minimum for surface = #{ss.name}")
            new_surface.remove
            next
          end
          new_surface.setSpace(ss.space.get)
          if ss.construction.is_initialized && !ss.isConstructionDefaulted
            new_surface.setConstruction(ss.construction.get)
          end
          if !pre_tri_sub_const.empty?
            non_rect_parent[new_surface] = pre_tri_sub_const.key(pre_tri_sub_const.values.max)
          end
          all_surfaces2 << new_surface
        end

        # remove orig surface
        ss.remove
      end

    else
      all_surfaces2 = all_surfaces
    end

    # add windows
    all_surfaces2.sort.each do |ss|
      orig_sub_surf_constructions = {}
      ss.subSurfaces.sort.each do |sub_surf|
        next if sub_surf.subSurfaceType == 'Door' || sub_surf.subSurfaceType == 'OverheadDoor'

        if sub_surf.construction.is_initialized
          if orig_sub_surf_constructions.key?(sub_surf.construction.get)
            orig_sub_surf_constructions[sub_surf.construction.get] += 1
          else
            orig_sub_surf_constructions[sub_surf.construction.get] = 1
          end
        end
      end

      # remove windows if ratio 0 or add in other cases
      if wwr == 0
        # remove all sub surfaces
        ss.subSurfaces.sort.each(&:remove)
        new_window = []
        window_confirmed = true
      else
        new_window = ss.setWindowToWallRatio(wwr, sillHeight_si.value, true)
        window_confirmed = false
      end

      if wwr > 0 && new_window.empty?

        # if new window is empty then inset base surface to add window (check may need to skip on base surfaces with doors)
        # skip of surface already has sub-surfaces or if not triangle
        if inset_tri_sub && (ss.subSurfaces.empty? && ss.vertices.size <= 3)
          # get centroid
          vertices = ss.vertices
          centroid = OpenStudio.getCentroid(vertices).get
          x_cent = centroid.x
          y_cent = centroid.y
          z_cent = centroid.z

          # reduce vertices towards centroid
          scale = Math.sqrt(wwr)
          new_vertices = OpenStudio::Point3dVector.new
          vertices.each do |vertex|
            x = (vertex.x * scale + x_cent * (1.0 - scale))
            y = (vertex.y * scale + y_cent * (1.0 - scale))
            z = (vertex.z * scale + z_cent * (1.0 - scale))
            new_vertices << OpenStudio::Point3d.new(x, y, z)
          end

          # create inset window
          new_window = OpenStudio::Model::SubSurface.new(new_vertices, model)
          new_window.setSurface(ss)
          new_window.setSubSurfaceType('FixedWindow')
          if non_rect_parent.key?(ss)
            new_window.setConstruction(non_rect_parent[ss])
          end
          window_confirmed = true
        end

      else
        if wwr > 0
          new_window = new_window.get
        end
        window_confirmed = true
      end

      if !window_confirmed
        case ss.vertices.size
        when 3
          if !inset_tri_sub
            runner.registerWarning("window could not be added because the surface has 3 sides, but the inset_tri_sub argument is false = #{ss.name}")
          end
        when 4
          case Functions.rectangle?(ss)
          when true
            # if Functions.requested_window_area_greater_than_max?(ss, wwr)
            #   runner.registerWarning("window could not be added because the surface has 4 sides and is rectangular, but the WWR exceeds the maximum = #{ss.name}")
            # end
            # HACK until requested_window_area_greater_than_max? works. reduce by 0.01 to find the "maximum".
            (1..(wwr / 0.1).floor).each do |i|
              wwr_adj = wwr - (i / 100.0)
              new_window = ss.setWindowToWallRatio(wwr_adj, sillHeight_si.value, true)
              unless new_window.empty?
                new_window = new_window.get
                window_confirmed = true
                runner.registerWarning("window-to-wall ratio exceeds maximum and was reduced to #{wwr_adj.round(3)} for surface = #{ss.name}")
                break
              end
            end
          when false
            if !triangulate
              runner.registerWarning("window could not be added because the surface has 4 sides, but is not rectangular and the triangulate argument is false = #{ss.name}")
            end
          end
        else
          if !triangulate
            runner.registerWarning("window could not be added because the surface has more than 4 sides and the triangulate argument is false = #{ss.name}")
          end
        end
      end

      # warn user if resulting window doesn't have a construction, as it will result in failed simulation. In the future may use logic from starting windows to apply construction to new window.
      if wwr > 0 && window_confirmed && new_window.construction.empty?
        # construction search order (orig window on this base surface, window in this orientation, andy window in building)
        if !orig_sub_surf_constructions.empty?
          new_window.setConstruction(orig_sub_surf_constructions.key(orig_sub_surf_constructions.values.max))
        elsif !orig_sub_surf_const_for_target_facade.empty?
          new_window.setConstruction(orig_sub_surf_const_for_target_facade.key(orig_sub_surf_const_for_target_facade.values.max))
          facade_const_warning = true
        elsif !orig_sub_surf_const_for_target_all_ext.empty?
          new_window.setConstruction(orig_sub_surf_const_for_target_all_ext.key(orig_sub_surf_const_for_target_all_ext.values.max))
          bldg_const_warning = true
        else
          empty_const_warning = true
          if catchall_glazing_const.nil?
            material = OpenStudio::Model::SimpleGlazing.new(model)
            material.setUFactor(2.556)
            material.setSolarHeatGainCoefficient(0.764)
            material.setVisibleTransmittance(0.812)
            catchall_glazing_const = OpenStudio::Model::Construction.new(model)
            catchall_glazing_const.insertLayer(0, material)
            catchall_glazing_const.setName('Dbl Clr 3mm/13mm Air') # from E+ dataset
          end
          new_window.setConstruction(catchall_glazing_const)
        end
      end
    end
  end

  # warn if some constructions do not have sub-surfaces
  if facade_const_warning
    runner.registerInfo('One or more new sub-surfaces did not have construction, using most commonly used construction for this facade.')
  end
  if bldg_const_warning
    runner.registerInfo('One or more new sub-surfaces did not have construction, using most commonly used construction across the entire building.')
  end
  if empty_const_warning
    # TODO: - add in catchall like something equiv to double glazed of glass with new simple glazing construction
    runner.registerWarning("Could not find existing window with construction as guide for new windows. Using a catchall glazing of #{catchall_glazing_const.name}.")
  end

  # report initial condition wwr
  # the initial and final ratios does not currently account for either sub-surface or zone multipliers.
  starting_wwr = format('%.02f', (starting_ext_window_area / starting_gross_ext_wall_area))
  runner.registerInitialCondition("The model's initial window to wall ratio for #{facade} facing exterior walls was #{starting_wwr}.")

  if !exterior_walls
    runner.registerAsNotApplicable("The model has no exterior #{facade.downcase} walls and was not altered")
    return true
  elsif !window_confirmed
    runner.registerAsNotApplicable("The model has exterior #{facade.downcase} walls, but no windows could be added with the requested window to wall ratio")
    return true
  end

  # data for final condition wwr
  Functions.get_surfaces_or_subsurfaces_by_facade(model.getSurfaces, facade).sort.each do |s|
    # get surface area adjusting for zone multiplier
    space = s.space
    if !space.empty?
      zone = space.get.thermalZone
    end
    if !zone.empty?
      zone_multiplier = zone.get.multiplier
      if zone_multiplier > 1
      end
    else
      zone_multiplier = 1 # space is not in a thermal zone
    end
    surface_gross_area = s.grossArea * zone_multiplier

    # loop through sub surfaces and add area including multiplier
    ext_window_area = 0
    s.subSurfaces.sort.each do |subSurface| # onlky one and should have multiplier of 1
      ext_window_area += subSurface.grossArea * subSurface.multiplier * zone_multiplier
    end

    final_gross_ext_wall_area += surface_gross_area
    final_ext_window_area += ext_window_area
  end

  # get delta in ft^2 for final - starting window area
  increase_window_area_si = OpenStudio::Quantity.new(final_ext_window_area - starting_ext_window_area, unit_area_si)
  increase_window_area_ip = OpenStudio.convert(increase_window_area_si, unit_area_ip).get

  # calculate final envelope cost as positive value
  constructions = model.getConstructions.sort
  constructions.each do |construction|
    const_llcs = construction.lifeCycleCosts
    const_llcs.sort.each do |const_llc|
      if const_llc.category == 'Construction'
        envelope_cost += const_llc.totalCost
      end
    end
  end

  # report final condition
  final_wwr = format('%.02f', (final_ext_window_area / final_gross_ext_wall_area))
  runner.registerFinalCondition("The model's final window to wall ratio for #{facade} facing exterior walls is #{final_wwr}. Window area increased by #{OpenStudio.toNeatString(increase_window_area_ip.value, 0)} (ft^2). The material and construction costs increased by $#{OpenStudio.toNeatString(envelope_cost, 0)}.")

  return true
end