lib/osut/utils.rb in osut-0.2.8 vs lib/osut/utils.rb in osut-0.3.0

- old
+ new

@@ -39,10 +39,12 @@ INF = OSut::INFO # not currently used in OSut WRN = OSut::WARN # WARN users of 'iffy' .osm inputs (yet not critical) ERR = OSut::ERROR # flag invalid .osm inputs (then exit via 'return') FTL = OSut::FATAL # not currently used in OSut NS = "nameString" # OpenStudio IdfObject nameString method + HEAD = 2.032 # standard 80" door + SILL = 0.762 # standard 30" window sill # This first set of utilities (~750 lines) help distinguishing spaces that # are directly vs indirectly CONDITIONED, vs SEMI-HEATED. The solution here # relies as much as possible on space conditioning categories found in # standards like ASHRAE 90.1 and energy codes like the Canadian NECB editions. @@ -1445,10 +1447,13 @@ return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty? area = area.get return false if area < TOL + delta = (area - area1 - area2).abs + return false if delta < TOL + true end ## # Generate offset vertices (by width) for a 3- or 4-sided, convex polygon. @@ -1458,11 +1463,11 @@ # @param v [Integer] OpenStudio SDK version, eg '321' for 'v3.2.1' (optional) # # @return [OpenStudio::Point3dVector] offset points if successful # @return [OpenStudio::Point3dVector] original points if invalid input def offset(p1 = [], w = 0, v = 0) - mth = "TBD::#{__callee__}" + mth = "OSut::#{__callee__}" cl = OpenStudio::Point3d vrsn = OpenStudio.openStudioVersion.split(".").map(&:to_i).join.to_i valid = p1.is_a?(OpenStudio::Point3dVector) || p1.is_a?(Array) return mismatch("pts", p1, cl1, mth, DBG, p1) unless valid @@ -1497,11 +1502,11 @@ offset = ft * offset if cw offset = (ft * offset).reverse unless cw pz = OpenStudio::Point3dVector.new offset.each { |o| pz << OpenStudio::Point3d.new(o.x, o.y, o.z ) } - + return pz else # brute force approach pz = {} pz[:A] = {} pz[:B] = {} @@ -1680,9 +1685,592 @@ vec << OpenStudio::Point3d.new(pzCp.x, pzCp.y, pzCp.z) vec << OpenStudio::Point3d.new(pzDp.x, pzDp.y, pzDp.z) if iv return vec end + end + + ## + # Validate whether an OpenStudio planar surface is safe to process. + # + # @param s [OpenStudio::Model::PlanarSurface] a surface + # + # @return [Bool] true if valid surface + def surface_valid?(s = nil) + mth = "OSut::#{__callee__}" + cl = OpenStudio::Model::PlanarSurface + + return mismatch("surface", s, cl, mth, DBG, false) unless s.is_a?(cl) + + id = s.nameString + size = s.vertices.size + last = size - 1 + + log(ERR, "#{id} #{size} vertices? need +3 (#{mth})") unless size > 2 + return false unless size > 2 + + [0, last].each do |i| + v1 = s.vertices[i] + v2 = s.vertices[i + 1] unless i == last + v2 = s.vertices.first if i == last + vec = v2 - v1 + bad = vec.length < TOL + + # As is, this comparison also catches collinear vertices (< 10mm apart) + # along an edge. Should avoid red-flagging such cases. TO DO. + log(ERR, "#{id}: < #{TOL}m (#{mth})") if bad + return false if bad + end + + # Add as many extra tests as needed ... + true + end + + ## + # Add sub surfaces (e.g. windows, doors, skylights) to surface. + # + # @param model [OpenStudio::Model::Model] a model + # @param s [OpenStudio::Model::Surface] a model surface + # @param subs [Array] requested sub surface attributes + # @param clear [Bool] remove current sub surfaces if true + # @param bfr [Double] safety buffer (m), when ~aligned along other edges + # + # @return [Bool] true if successful (check for logged messages if failures) + def addSubs(model = nil, s = nil, subs = [], clear = false, bfr = 0.005) + mth = "OSut::#{__callee__}" + v = OpenStudio.openStudioVersion.split(".").join.to_i + cl1 = OpenStudio::Model::Model + cl2 = OpenStudio::Model::Surface + cl3 = Array + cl4 = Hash + cl5 = Numeric + min = 0.050 # minimum ratio value ( 5%) + max = 0.950 # maximum ratio value (95%) + no = false + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Exit if mismatched or invalid argument classes. + return mismatch("model", model, cl1, mth, DBG, no) unless model.is_a?(cl1) + return mismatch("surface", s, cl2, mth, DBG, no) unless s.is_a?(cl2) + return mismatch("subs", subs, cl3, mth, DBG, no) unless subs.is_a?(cl3) + return no unless surface_valid?(s) + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Clear existing sub surfaces if requested. + nom = s.nameString + + unless clear == true || clear == false + log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})") + clear = false + end + + s.subSurfaces.map(&:remove) if clear + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Allowable sub surface types ... & Frame&Divider enabled + # - "FixedWindow" | true + # - "OperableWindow" | true + # - "Door" | false + # - "GlassDoor" | true + # - "OverheadDoor" | false + # - "Skylight" | false if v < 321 + # - "TubularDaylightDome" | false + # - "TubularDaylightDiffuser" | false + type = "FixedWindow" + types = OpenStudio::Model::SubSurface.validSubSurfaceTypeValues + stype = s.surfaceType # Wall, RoofCeiling or Floor + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Fetch transform, as if host surface vertices were to "align", i.e.: + # - rotated/tilted ... then flattened along XY plane + # - all Z-axis coordinates ~= 0 + # - vertices with the lowest X-axis values are "aligned" along X-axis (0) + # - vertices with the lowest Z-axis values are "aligned" along Y-axis (0) + # - Z-axis values are represented as Y-axis values + tr = OpenStudio::Transformation.alignFace(s.vertices) + + # Aligned vertices of host surface, and fetch attributes. + aligned = tr.inverse * s.vertices + max_x = aligned.max_by(&:x).x + max_y = aligned.max_by(&:y).y + mid_x = max_x / 2 + mid_y = max_y / 2 + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Assign default values to certain sub keys (if missing), +more validation. + subs.each_with_index do |sub, index| + return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl4) + + # Required key:value pairs (either set by the user or defaulted). + sub[:id ] = "" unless sub.key?(:id ) # "Window 007" + sub[:type ] = type unless sub.key?(:type ) # "FixedWindow" + sub[:count ] = 1 unless sub.key?(:count ) # for an array + sub[:multiplier] = 1 unless sub.key?(:multiplier) + sub[:frame ] = nil unless sub.key?(:frame ) # frame/divider + sub[:assembly ] = nil unless sub.key?(:assembly ) # construction + + # Optional key:value pairs. + # sub[:ratio ] # e.g. %FWR + # sub[:head ] # e.g. std 80" door + frame/buffers (+ m) + # sub[:sill ] # e.g. std 30" sill + frame/buffers (+ m) + # sub[:height ] # any sub surface height, below "head" (+ m) + # sub[:width ] # e.g. 1.200 m + # sub[:offset ] # if array (+ m) + # sub[:centreline] # left or right of base surface centreline (+/- m) + # sub[:r_buffer ] # buffer between sub/array and right-side corner (+ m) + # sub[:l_buffer ] # buffer between sub/array and left-side corner (+ m) + + sub[:id] = "#{nom}|#{index}" if sub[:id].empty? + id = sub[:id] + + # If sub surface type is invalid, log/reset. Additional corrections may + # be enabled once a sub surface is actually instantiated. + unless types.include?(sub[:type]) + log(WRN, "Reset invalid '#{id}' type to '#{type}' (#{mth})") + sub[:type] = type + end + + # Log/ignore (optional) frame & divider object. + unless sub[:frame].nil? + if sub[:frame].respond_to?(:frameWidth) + sub[:frame] = nil if sub[:type] == "Skylight" && v < 321 + sub[:frame] = nil if sub[:type] == "Door" + sub[:frame] = nil if sub[:type] == "OverheadDoor" + sub[:frame] = nil if sub[:type] == "TubularDaylightDome" + sub[:frame] = nil if sub[:type] == "TubularDaylightDiffuser" + log(WRN, "Skip '#{id}' FrameDivider (#{mth})") if sub[:frame].nil? + else + sub[:frame] = nil + log(WRN, "Skip '#{id}' invalid FrameDivider object (#{mth})") + end + end + + # The (optional) "assembly" must reference a valid OpenStudio + # construction base, to explicitly assign to each instantiated sub + # surface. If invalid, log/reset/ignore. Additional checks are later + # activated once a sub surface is actually instantiated. + unless sub[:assembly].nil? + unless sub[:assembly].respond_to?(:isFenestration) + log(WRN, "Skip invalid '#{id}' construction (#{mth})") + sub[:assembly] = nil + end + end + + # Log/reset negative numerical values. Set ~0 values to 0. + sub.each do |key, value| + next if key == :id + next if key == :type + next if key == :frame + next if key == :assembly + + return mismatch(key, value, cl5, mth, DBG, no) unless value.is_a?(cl5) + next if key == :centreline + + negative(key, mth, WRN) if value < 0 + value = 0.0 if value.abs < TOL + end + end + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Log/reset (or abandon) conflicting user-set geometry key:value pairs: + # :head e.g. std 80" door + frame/buffers (+ m) + # :sill e.g. std 30" sill + frame/buffers (+ m) + # :height any sub surface height, below "head" (+ m) + # :width e.g. 1.200 m + # :offset if array (+ m) + # :centreline left or right of base surface centreline (+/- m) + # :r_buffer buffer between sub/array and right-side corner (+ m) + # :l_buffer buffer between sub/array and left-side corner (+ m) + # + # If successful, this will generate sub surfaces and add them to the model. + subs.each do |sub| + # Set-up unique sub parameters: + # - Frame & Divider "width" + # - minimum "clear glazing" limits + # - buffers, etc. + id = sub[:id] + frame = 0 + frame = sub[:frame].frameWidth unless sub[:frame].nil? + frames = 2 * frame + buffer = frame + bfr + buffers = 2 * buffer + dim = 0.200 unless (3 * frame) > 0.200 + dim = 3 * frame if (3 * frame) > 0.200 + glass = dim - frames + min_sill = buffer + min_head = buffers + glass + max_head = max_y - buffer + max_sill = max_head - (buffers + glass) + min_ljamb = buffer + max_ljamb = max_x - (buffers + glass) + min_rjamb = buffers + glass + max_rjamb = max_x - buffer + max_height = max_y - buffers + max_width = max_x - buffers + + # Default sub surface "head" & "sill" height (unless user-specified). + typ_head = HEAD # standard 80" door + typ_sill = SILL # standard 30" window sill + + if sub.key?(:ratio) + typ_head = mid_y * (1 + sub[:ratio]) if sub[:ratio] > 0.75 + typ_head = mid_y * (1 + sub[:ratio]) unless stype.downcase == "wall" + typ_sill = mid_y * (1 - sub[:ratio]) if sub[:ratio] > 0.75 + typ_sill = mid_y * (1 - sub[:ratio]) unless stype.downcase == "wall" + end + + # Log/reset "height" if beyond min/max. + if sub.key?(:height) + unless sub[:height].between?(glass, max_height) + sub[:height] = glass if sub[:height] < glass + sub[:height] = max_height if sub[:height] > max_height + log(WRN, "Reset '#{id}' height to #{sub[:height]} m (#{mth})") + end + end + + # Log/reset "head" height if beyond min/max. + if sub.key?(:head) + unless sub[:head].between?(min_head, max_head) + sub[:head] = max_head if sub[:head] > max_head + sub[:head] = min_head if sub[:head] < min_head + log(WRN, "Reset '#{id}' head height to #{sub[:head]} m (#{mth})") + end + end + + # Log/reset "sill" height if beyond min/max. + if sub.key?(:sill) + unless sub[:sill].between?(min_sill, max_sill) + sub[:sill] = max_sill if sub[:sill] > max_sill + sub[:sill] = min_sill if sub[:sill] < min_sill + log(WRN, "Reset '#{id}' sill height to #{sub[:sill]} m (#{mth})") + end + end + + # At this point, "head", "sill" and/or "height" have been tentatively + # validated (and/or have been corrected) independently from one another. + # Log/reset "head" & "sill" heights if conflicting. + if sub.key?(:head) && sub.key?(:sill) && sub[:head] < sub[:sill] + glass + sill = sub[:head] - glass + + if sill < min_sill + sub[:ratio ] = 0 if sub.key?(:ratio) + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: invalid '#{id}' head/sill combo (#{mth})") + next + else + sub[:sill] = sill + log(WRN, "(Re)set '#{id}' sill height to #{sub[:sill]} m (#{mth})") + end + end + + # Attempt to reconcile "head", "sill" and/or "height". If successful, + # all 3x parameters are set (if missing), or reset if invalid. + if sub.key?(:head) && sub.key?(:sill) + height = sub[:head] - sub[:sill] + + if sub.key?(:height) && (sub[:height] - height).abs > TOL + log(WRN, "(Re)set '#{id}' height to #{height} m (#{mth})") + end + + sub[:height] = height + elsif sub.key?(:head) # no "sill" + if sub.key?(:height) + sill = sub[:head] - sub[:height] + + if sill < min_sill + sill = min_sill + height = sub[:head] - sill + + if height < glass + sub[:ratio ] = 0 if sub.key?(:ratio) + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: invalid '#{id}' head/height combo (#{mth})") + next + else + sub[:sill ] = sill + sub[:height] = height + log(WRN, "(Re)set '#{id}' height to #{sub[:height]} m (#{mth})") + end + else + sub[:sill] = sill + end + else + sub[:sill ] = typ_sill + sub[:height] = sub[:head] - sub[:sill] + end + elsif sub.key?(:sill) # no "head" + if sub.key?(:height) + head = sub[:sill] + sub[:height] + + if head > max_head + head = max_head + height = head - sub[:sill] + + if height < glass + sub[:ratio ] = 0 if sub.key?(:ratio) + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: invalid '#{id}' sill/height combo (#{mth})") + next + else + sub[:head ] = head + sub[:height] = height + log(WRN, "(Re)set '#{id}' height to #{sub[:height]} m (#{mth})") + end + else + sub[:head] = head + end + else + sub[:head ] = typ_head + sub[:height] = sub[:head] - sub[:sill] + end + elsif sub.key?(:height) # neither "head" nor "sill" + head = typ_head + sill = head - sub[:height] + + if sill < min_sill + sill = min_sill + head = sill + sub[:height] + end + + sub[:head] = head + sub[:sill] = sill + else + sub[:head ] = typ_head + sub[:sill ] = typ_sill + sub[:height] = sub[:head] - sub[:sill] + end + + # Log/reset "width" if beyond min/max. + if sub.key?(:width) + unless sub[:width].between?(glass, max_width) + sub[:width] = glass if sub[:width] < glass + sub[:width] = max_width if sub[:width] > max_width + log(WRN, "Reset '#{id}' width to #{sub[:width]} m (#{mth})") + end + end + + # Log/reset "count" if < 1. + if sub.key?(:count) + if sub[:count] < 1 + sub[:count] = 1 + log(WRN, "Reset '#{id}' count to #{sub[:count]} (#{mth})") + end + end + + sub[:count] = 1 unless sub.key?(:count) + + # Log/reset if left-sided buffer under min jamb position. + if sub.key?(:l_buffer) + if sub[:l_buffer] < min_ljamb + sub[:l_buffer] = min_ljamb + log(WRN, "Reset '#{id}' left buffer to #{sub[:l_buffer]} m (#{mth})") + end + end + + # Log/reset if right-sided buffer beyond max jamb position. + if sub.key?(:r_buffer) + if sub[:r_buffer] > max_rjamb + sub[:r_buffer] = min_rjamb + log(WRN, "Reset '#{id}' right buffer to #{sub[:r_buffer]} m (#{mth})") + end + end + + centre = mid_x + centre += sub[:centreline] if sub.key?(:centreline) + n = sub[:count ] + h = sub[:height ] + frames + w = 0 # overall width of sub(s) bounding box (to calculate) + x0 = 0 # left-side X-axis coordinate of sub(s) bounding box + xf = 0 # right-side X-axis coordinate of sub(s) bounding box + + # Log/reset "offset", if conflicting vs "width". + if sub.key?(:ratio) + if sub[:ratio] < TOL + sub[:ratio ] = 0 + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: '#{id}' ratio ~0 (#{mth})") + next + end + + # Log/reset if "ratio" beyond min/max? + unless sub[:ratio].between?(min, max) + sub[:ratio] = min if sub[:ratio] < min + sub[:ratio] = max if sub[:ratio] > max + log(WRN, "Reset ratio (min/max) to #{sub[:ratio]} (#{mth})") + end + + # Log/reset "count" unless 1. + unless sub[:count] == 1 + sub[:count] = 1 + log(WRN, "Reset count (ratio) to 1 (#{mth})") + end + + area = s.grossArea * sub[:ratio] # sub m2, including (optional) frames + w = area / h + width = w - frames + x0 = centre - w/2 + xf = centre + w/2 + + if sub.key?(:l_buffer) + if sub.key?(:centreline) + log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})") + else + x0 = sub[:l_buffer] - frame + xf = x0 + w + centre = x0 + w/2 + end + elsif sub.key?(:r_buffer) + if sub.key?(:centreline) + log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})") + else + xf = max_x - sub[:r_buffer] + frame + x0 = xf - w + centre = x0 + w/2 + end + end + + # Too wide? + if x0 < min_ljamb || xf > max_rjamb + sub[:ratio ] = 0 if sub.key?(:ratio) + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: invalid (ratio) width/centreline (#{mth})") + next + end + + if sub.key?(:width) && (sub[:width] - width).abs > TOL + sub[:width] = width + log(WRN, "Reset width (ratio) to #{sub[:width]} (#{mth})") + end + + sub[:width] = width unless sub.key?(:width) + else + unless sub.key?(:width) + sub[:ratio ] = 0 if sub.key?(:ratio) + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: missing '#{id}' width (#{mth})") + next + end + + width = sub[:width] + frames + gap = (max_x - n * width) / (n + 1) + gap = sub[:offset] - width if sub.key?(:offset) + gap = 0 if gap < bfr + offset = gap + width + + if sub.key?(:offset) && (offset - sub[:offset]).abs > TOL + sub[:offset] = offset + log(WRN, "Reset sub offset to #{sub[:offset]} m (#{mth})") + end + + sub[:offset] = offset unless sub.key?(:offset) + + # Overall width (including frames) of bounding box around array. + w = n * width + (n - 1) * gap + x0 = centre - w/2 + xf = centre + w/2 + + if sub.key?(:l_buffer) + if sub.key?(:centreline) + log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})") + else + x0 = sub[:l_buffer] - frame + xf = x0 + w + centre = x0 + w/2 + end + elsif sub.key?(:r_buffer) + if sub.key?(:centreline) + log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})") + else + xf = max_x - sub[:r_buffer] + frame + x0 = xf - w + centre = x0 + w/2 + end + end + + # Too wide? + if x0 < bfr || xf > max_x - bfr + sub[:ratio ] = 0 if sub.key?(:ratio) + sub[:count ] = 0 + sub[:multiplier] = 0 + sub[:height ] = 0 if sub.key?(:height) + sub[:width ] = 0 if sub.key?(:width) + log(ERR, "Skip: invalid array width/centreline (#{mth})") + next + end + end + + # Initialize left-side X-axis coordinate of only/first sub. + pos = x0 + frame + + # Generate sub(s). + sub[:count].times do |i| + name = "#{id}:#{i}" + fr = 0 + fr = sub[:frame].frameWidth if sub[:frame] + + vec = OpenStudio::Point3dVector.new + vec << OpenStudio::Point3d.new(pos, sub[:head], 0) + vec << OpenStudio::Point3d.new(pos, sub[:sill], 0) + vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:sill], 0) + vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:head], 0) + vec = tr * vec + + # Log/skip if conflict between individual sub and base surface. + vc = vec + vc = offset(vc, fr, 300) if fr > 0 + ok = fits?(vc, s.vertices, name, nom) + log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})") unless ok + break unless ok + + # Log/skip if conflicts with existing subs (even if same array). + s.subSurfaces.each do |sb| + nome = sb.nameString + fd = sb.windowPropertyFrameAndDivider + fr = 0 if fd.empty? + fr = fd.get.frameWidth unless fd.empty? + vk = sb.vertices + vk = offset(vk, fr, 300) if fr > 0 + oops = overlaps?(vc, vk, name, nome) + log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})") if oops + ok = false if oops + break if oops + end + + break unless ok + + sb = OpenStudio::Model::SubSurface.new(vec, model) + sb.setName(name) + sb.setSubSurfaceType(sub[:type]) + sb.setConstruction(sub[:assembly]) if sub[:assembly] + ok = sb.allowWindowPropertyFrameAndDivider + sb.setWindowPropertyFrameAndDivider(sub[:frame]) if sub[:frame] && ok + sb.setMultiplier(sub[:multiplier]) if sub[:multiplier] > 1 + sb.setSurface(s) + + # Reset "pos" if array. + pos += sub[:offset] if sub.key?(:offset) + end + end + + true end ## # Callback when other modules extend OSlg #