From 0114e2838aa5dc93f55b1c9f8242677c9cf833b6 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 5 Aug 2025 11:12:17 -0400 Subject: [PATCH 01/15] Tweaks 'genConstruction' (tested) --- lib/osut/utils.rb | 125 ++++++++++++++++++++++------------------ spec/osut_tests_spec.rb | 40 +++++++++++-- 2 files changed, 103 insertions(+), 62 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 4e560d8..253de46 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -220,13 +220,14 @@ def genConstruction(model = nil, specs = {}) chk = @@uo.keys.include?(specs[:type]) return invalid("surface type", mth, 2, ERR) unless chk - specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo) + specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo) # can be nil u = specs[:uo] if u - return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric) - return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678 - return negative("#{id} Uo" , mth, ERR) if u < 0 + return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric) + return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678 + return zero("#{id} Uo", mth, ERR) if u.round(2) == 0.00 + return negative("#{id} Uo", mth, ERR) if u < 0 end # Optional specs. Log/reset if invalid. @@ -466,14 +467,14 @@ def genConstruction(model = nil, specs = {}) a[:compo ][:d ] = d a[:compo ][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}" when :window - a[:glazing][:u ] = specs[:uo ] + a[:glazing][:u ] = u ? u : @@uo[:window] a[:glazing][:shgc] = 0.450 a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc) a[:glazing][:id ] = "OSut|window" a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}" a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}" when :skylight - a[:glazing][:u ] = specs[:uo ] + a[:glazing][:u ] = u ? u : @@uo[:skylight] a[:glazing][:shgc] = 0.450 a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc) a[:glazing][:id ] = "OSut|skylight" @@ -482,25 +483,11 @@ def genConstruction(model = nil, specs = {}) end # Initiate layers. - glazed = true - glazed = false if a[:glazing].empty? - layers = OpenStudio::Model::OpaqueMaterialVector.new unless glazed - layers = OpenStudio::Model::FenestrationMaterialVector.new if glazed + unglazed = a[:glazing].empty? ? true : false - if glazed - u = a[:glazing][:u ] - shgc = a[:glazing][:shgc] - lyr = model.getSimpleGlazingByName(a[:glazing][:id]) - - if lyr.empty? - lyr = OpenStudio::Model::SimpleGlazing.new(model, u, shgc) - lyr.setName(a[:glazing][:id]) - else - lyr = lyr.get - end + if unglazed + layers = OpenStudio::Model::OpaqueMaterialVector.new - layers << lyr - else # Loop through each layer spec, and generate construction. a.each do |i, l| next if l.empty? @@ -524,44 +511,68 @@ def genConstruction(model = nil, specs = {}) layers << lyr end + else + layers = OpenStudio::Model::FenestrationMaterialVector.new + + u0 = a[:glazing][:u ] + shgc = a[:glazing][:shgc] + lyr = model.getSimpleGlazingByName(a[:glazing][:id]) + + if lyr.empty? + lyr = OpenStudio::Model::SimpleGlazing.new(model, u0, shgc) + lyr.setName(a[:glazing][:id]) + else + lyr = lyr.get + end + + layers << lyr end c = OpenStudio::Model::Construction.new(layers) c.setName(id) # Adjust insulating layer thickness or conductivity to match requested Uo. - unless glazed - ro = 0 - ro = 1 / specs[:uo] - @@film[ specs[:type] ] if specs[:uo] - - if specs[:type] == :door # 1x layer, adjust conductivity - layer = c.getLayer(0).to_StandardOpaqueMaterial - return invalid("#{id} standard material?", mth, 0) if layer.empty? - - layer = layer.get - k = layer.thickness / ro - layer.setConductivity(k) - elsif ro > 0 # multiple layers, adjust insulating layer thickness - lyr = insulatingLayer(c) - return invalid("#{id} construction", mth, 0) if lyr[:index].nil? - return invalid("#{id} construction", mth, 0) if lyr[:type ].nil? - return invalid("#{id} construction", mth, 0) if lyr[:r ].zero? - - index = lyr[:index] - layer = c.getLayer(index).to_StandardOpaqueMaterial - return invalid("#{id} material @#{index}", mth, 0) if layer.empty? - - layer = layer.get - k = layer.conductivity - d = (ro - rsi(c) + lyr[:r]) * k - return invalid("#{id} adjusted m", mth, 0) if d < 0.03 - - nom = "OSut|" - nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "") - nom += "|" - nom += format("%03d", d*1000)[-3..-1] - layer.setName(nom) if model.getStandardOpaqueMaterialByName(nom).empty? - layer.setThickness(d) + if u and unglazed + ro = 1 / u - film + + if ro > 0 + if specs[:type] == :door # 1x layer, adjust conductivity + layer = c.getLayer(0).to_StandardOpaqueMaterial + return invalid("#{id} standard material?", mth, 0) if layer.empty? + + layer = layer.get + k = layer.thickness / ro + layer.setConductivity(k) + else # multiple layers, adjust insulating layer thickness + lyr = insulatingLayer(c) + return invalid("#{id} construction", mth, 0) if lyr[:index].nil? + return invalid("#{id} construction", mth, 0) if lyr[:type ].nil? + return invalid("#{id} construction", mth, 0) if lyr[:r ].zero? + + index = lyr[:index] + layer = c.getLayer(index).to_StandardOpaqueMaterial + return invalid("#{id} material @#{index}", mth, 0) if layer.empty? + + layer = layer.get + k = layer.conductivity + d = (ro - rsi(c) + lyr[:r]) * k + return invalid("#{id} adjusted m", mth, 0) if d < 0.03 + + nom = "OSut|" + nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "") + nom += "|" + nom += format("%03d", d*1000)[-3..-1] + + lyr = model.getStandardOpaqueMaterialByName(nom) + + if lyr.empty? + layer.setName(nom) + layer.setThickness(d) + else + omat = lyr.get + c.setLayer(index, omat) + end + end end end @@ -746,7 +757,7 @@ def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0) # Validates if a default construction set holds a base construction. # # @param set [OpenStudio::Model::DefaultConstructionSet] a default set - # @param bse [OpensStudio::Model::ConstructionBase] a construction base + # @param bse [OpenStudio::Model::ConstructionBase] a construction base # @param gr [Bool] if ground-facing surface # @param ex [Bool] if exterior-facing surface # @param tp [#to_s] a surface type @@ -1048,7 +1059,7 @@ def insulatingLayer(lc = nil) return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS) id = lc.nameString - return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl) + return mismatch(id, lc, cl, mth, DBG, res) unless lc.is_a?(cl) lc.layers.each do |m| unless m.to_MasslessOpaqueMaterial.empty? diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 5ae387a..52b6dc1 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -291,6 +291,22 @@ u = 1 / cls1::rsi(surface) expect(u).to be_within(TOL).of(uo9) + # Invalid Uo (here, skylights and windows inherit default Uo values) + specs = {type: :skylight, uo: nil} + surface = cls1::genConstruction(model, specs) + expect(surface).to be_a(OpenStudio::Model::LayeredConstruction) + expect(surface.layers.size).to eq(1) + u = 1 / cls1::rsi(surface) + expect(u).to be_within(TOL).of(uo[:skylight]) + + # Invalid Uo (here, Uo-adjustments are ignored altogether) + specs = {type: :wall, uo: nil} + surface = cls1::genConstruction(model, specs) + expect(surface).to be_a(OpenStudio::Model::LayeredConstruction) + expect(surface.layers.size).to eq(4) + u = 1 / cls1::rsi(surface) + expect(u).to be_within(TOL).of(2.23) # not matching any defaults + expect(cls1.status.zero?).to be true end @@ -790,8 +806,12 @@ expect(model).to_not be_empty model = model.get - m = "OSut::insulatingLayer" - m1 = "Invalid 'lc' arg #1 (#{m})" + m = "OSut::insulatingLayer" + cl1 = OpenStudio::Model::Surface + cl2 = OpenStudio::Model::LayeredConstruction + n1 = "Entryway Wall 1" + m1 = "Invalid 'lc' arg #1 (#{m})" + m2 = "'#{n1}' #{cl1}? expecting #{cl2} (#{m})" model.getLayeredConstructions.each do |lc| lyr = mod1.insulatingLayer(lc) @@ -838,7 +858,6 @@ end lyr = mod1.insulatingLayer(nil) - expect(mod1.debug?).to be true expect(lyr[:index]).to be_nil expect(lyr[:type ]).to be_nil expect(lyr[:r ]).to be_zero @@ -848,7 +867,6 @@ expect(mod1.clean!).to eq(DBG) lyr = mod1.insulatingLayer("") - expect(mod1.debug?).to be true expect(lyr[:index]).to be_nil expect(lyr[:type ]).to be_nil expect(lyr[:r ]).to be_zero @@ -858,13 +876,25 @@ expect(mod1.clean!).to eq(DBG) lyr = mod1.insulatingLayer(model) - expect(mod1.debug?).to be true expect(lyr[:index]).to be_nil expect(lyr[:type ]).to be_nil expect(lyr[:r ]).to be_zero expect(mod1.debug?).to be true expect(mod1.logs.size).to eq(1) expect(mod1.logs.first[:message]).to eq(m1) + + eWall1 = model.getSurfaceByName(n1) + expect(eWall1).to_not be_empty + eWall1 = eWall1.get + + expect(mod1.clean!).to eq(DBG) + lyr = mod1.insulatingLayer(eWall1) + expect(lyr[:index]).to be_nil + expect(lyr[:type ]).to be_nil + expect(lyr[:r ]).to be_zero + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m2) end it "checks for spandrels" do From 0c41d0405fc0c9dcf80efa1b7bdf9055bbbb6a13 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 5 Aug 2025 11:23:20 -0400 Subject: [PATCH 02/15] Adds OS 3.10/Ubuntu-latest GH Action test --- .github/workflows/pull_request.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c4c828f..fa7967f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -134,3 +134,19 @@ jobs: docker exec -t test bundle update docker exec -t test bundle exec rake docker kill test + test_310x: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: Run Tests + run: | + echo $(pwd) + echo $(ls) + docker pull nrel/openstudio:3.10.0 + docker run --name test --rm -d -t -v $(pwd):/work -w /work nrel/openstudio:3.10.0 + docker exec -t test pwd + docker exec -t test ls + docker exec -t test bundle update + docker exec -t test bundle exec rake + docker kill test From 4394d0c5b44512fba3244f71509450594a991170 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 5 Aug 2025 11:28:50 -0400 Subject: [PATCH 03/15] Adds OS 3.10/Ubuntu-2204 GH AT (redux) --- .github/workflows/pull_request.yml | 36 ++---------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index fa7967f..b0339fb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,38 +22,6 @@ jobs: docker exec -t test bundle update docker exec -t test bundle exec rake docker kill test - test_330x: - runs-on: ubuntu-22.04 - steps: - - name: Check out repository - uses: actions/checkout@v2 - - name: Run Tests - run: | - echo $(pwd) - echo $(ls) - docker pull nrel/openstudio:3.3.0 - docker run --name test --rm -d -t -v $(pwd):/work -w /work nrel/openstudio:3.3.0 - docker exec -t test pwd - docker exec -t test ls - docker exec -t test bundle update - docker exec -t test bundle exec rake - docker kill test - test_340x: - runs-on: ubuntu-22.04 - steps: - - name: Check out repository - uses: actions/checkout@v2 - - name: Run Tests - run: | - echo $(pwd) - echo $(ls) - docker pull nrel/openstudio:3.4.0 - docker run --name test --rm -d -t -v $(pwd):/work -w /work nrel/openstudio:3.4.0 - docker exec -t test pwd - docker exec -t test ls - docker exec -t test bundle update - docker exec -t test bundle exec rake - docker kill test test_351x: runs-on: ubuntu-22.04 steps: @@ -134,8 +102,8 @@ jobs: docker exec -t test bundle update docker exec -t test bundle exec rake docker kill test - test_310x: - runs-on: ubuntu-latest + test_3100x: + runs-on: ubuntu-22.04 steps: - name: Check out repository uses: actions/checkout@v2 From 3f315a0e3ac69a17d0f18225b973f28bf7df033d Mon Sep 17 00:00:00 2001 From: brgix Date: Wed, 6 Aug 2025 09:56:16 -0400 Subject: [PATCH 04/15] Fixes ScheduleInterval methods/tests --- lib/osut/utils.rb | 60 ++++++++++++++++++++++++++++++++++++----- spec/osut_tests_spec.rb | 26 +++++++++++++++--- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 253de46..3c96f07 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -1113,7 +1113,7 @@ def spandrel?(s = nil) id = s.nameString m1 = "#{id}:spandrel" m2 = "#{id}:spandrel:boolean" - return mismatch(id, s, cl, mth) unless s.is_a?(cl) + return mismatch(id, s, cl, mth, false) unless s.is_a?(cl) if s.additionalProperties.hasFeature("spandrel") val = s.additionalProperties.getFeatureAsBoolean("spandrel") @@ -1140,7 +1140,7 @@ def fenestration?(s = nil) return invalid("subsurface", mth, 1, DBG, false) unless s.respond_to?(NS) id = s.nameString - return mismatch(id, s, cl, mth, false) unless s.is_a?(cl) + return mismatch(id, s, cl, mth, DBG, false) unless s.is_a?(cl) # OpenStudio::Model::SubSurface.validSubSurfaceTypeValues # "FixedWindow" : fenestration @@ -1433,7 +1433,15 @@ def scheduleIntervalMinMax(sched = nil) id = sched.nameString return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl) - vals = sched.timeSeries.values + values = sched.timeSeries.values + + values.each do |value| + if value.respond_to?(:to_f) + vals << value.to_f + else + invalid("numerical at #{i}", mth, 1, ERR) + end + end res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil res[:max] = vals.max.is_a?(Numeric) ? vals.min : nil @@ -1540,6 +1548,16 @@ def maxHeatScheduledSetpoint(zone = nil) res[:spt] = max if res[:spt] < max end end + + unless sched.to_ScheduleInterval.empty? + sched = sched.to_ScheduleInterval.get + max = scheduleIntervalMinMax(sched)[:max] + + if max + res[:spt] = max unless res[:spt] + res[:spt] = max if res[:spt] < max + end + end end return res if zone.thermostat.empty? @@ -1597,6 +1615,16 @@ def maxHeatScheduledSetpoint(zone = nil) end end + unless sched.to_ScheduleInterval.empty? + sched = sched.to_ScheduleInterval.get + max = scheduleIntervalMinMax(sched)[:max] + + if max + res[:spt] = max unless res[:spt] + res[:spt] = max if res[:spt] < max + end + end + unless sched.to_ScheduleYear.empty? sched = sched.to_ScheduleYear.get @@ -1718,6 +1746,16 @@ def minCoolScheduledSetpoint(zone = nil) res[:spt] = min if res[:spt] > min end end + + unless sched.to_ScheduleInterval.empty? + sched = sched.to_ScheduleInterval.get + min = scheduleIntervalMinMax(sched)[:min] + + if min + res[:spt] = min unless res[:spt] + res[:spt] = min if res[:spt] > min + end + end end return res if zone.thermostat.empty? @@ -1775,6 +1813,16 @@ def minCoolScheduledSetpoint(zone = nil) end end + unless sched.to_ScheduleInterval.empty? + sched = sched.to_ScheduleInerval.get + min = scheduleIntervalMinMax(sched)[:min] + + if min + res[:spt] = min unless res[:spt] + res[:spt] = min if res[:spt] > min + end + end + unless sched.to_ScheduleYear.empty? sched = sched.to_ScheduleYear.get @@ -2362,7 +2410,7 @@ def transforms(group = nil) # @return [OpenStudio::Vector3d] true normal vector # @return [nil] if invalid input (see logs) def trueNormal(s = nil, r = 0) - mth = "TBD::#{__callee__}" + mth = "OSut::#{__callee__}" cl = OpenStudio::Model::PlanarSurface return mismatch("surface", s, cl, mth) unless s.is_a?(cl) return invalid("rotation angle", mth, 2) unless r.respond_to?(:to_f) @@ -2891,7 +2939,7 @@ def getTriads(pts = nil, co = false) # @param pts [Set] 3D points # # @return [Bool] whether set is a valid triad (i.e. a trio of 3D points) - # @return [false] if invalid input (see logs) + # @return [false] if invalid input (see 'to_p3Dv' logs) def triad?(pts = nil) pts = to_p3Dv(pts) return false if pts.empty? @@ -5153,7 +5201,7 @@ def genSlab(pltz = [], z = 0) end # Once joined, re-adjust Z-axis coordinates. - unless z.zero? + unless z.round(2) == 0.00 vtx = OpenStudio::Point3dVector.new slb.each { |pt| vtx << OpenStudio::Point3d.new(pt.x, pt.y, z) } slb = vtx diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 52b6dc1..0cf2239 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -164,7 +164,7 @@ expect(surface.layers[1].nameString).to eq("OSut|polyiso|108") expect(surface.layers[2].nameString).to eq("OSut|concrete|100") - # Insulated, parking garage roof (polyiso above 8" slab). + # Roof above conditioned parking garage (polyiso under 8" slab). specs = {type: :roof, uo: uo2, clad: :heavy, frame: :medium, finish: :none} surface = cls1.genConstruction(model, specs) expect(surface).to_not be_nil @@ -982,7 +982,7 @@ expect(minmax).to have_key(:min) expect(minmax).to have_key(:max) expect(minmax[:min]).to be_within(TOL).of(23.89) - expect(minmax[:min]).to be_within(TOL).of(23.89) + expect(minmax[:max]).to be_within(TOL).of(23.89) expect(cls1.status).to be_zero expect(cls1.logs).to be_empty @@ -1289,9 +1289,27 @@ module M expect(cc.setTemperatureCalculationRequestedAfterLayerNumber(1)).to be true expect(floor.setConstruction(cc)).to be true + # Test 'fixed interval' schedule. Annual time series - no variation. + start = model.getYearDescription.makeDate(1, 1) + inter = OpenStudio::Time.new(0, 1, 0, 0) + values = OpenStudio.createVector(Array.new(8760, 22.78)) + series = OpenStudio::TimeSeries.new(start, inter, values, "") + limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + limits.setName("Radiant Electric Heating Setpoint Schedule Type Limits") + expect(limits.setNumericType("Continuous")).to be true + expect(limits.setUnitType("Temperature")).to be true + + schedule = OpenStudio::Model::ScheduleFixedInterval.new(model) + schedule.setName("Radiant Electric Heating Setpoint Schedule") + expect(schedule.setTimeSeries(series)).to be true + expect(schedule.setTranslatetoScheduleFile(false)).to be true + expect(schedule.setScheduleTypeLimits(limits)).to be true + + tvals = schedule.timeSeries.values + expect(tvals).is_a?(OpenStudio::Vector) + tvals.each { |tval| expect(tval.is_a?(Numeric)) } + availability = M.availabilitySchedule(model) - schedule = OpenStudio::Model::ScheduleConstant.new(model) - expect(schedule.setValue(22.78)).to be true # reuse cooling setpoint # Create radiant electric heating. ht = OpenStudio::Model::ZoneHVACLowTemperatureRadiantElectric.new( From fa96a3921b13fa835aa4336fd0add7995d0c88a8 Mon Sep 17 00:00:00 2001 From: brgix Date: Sat, 9 Aug 2025 08:07:10 -0400 Subject: [PATCH 05/15] Tweaks line intersection + tests --- lib/osut/utils.rb | 62 ++++++++++++++++++++--------------------- spec/osut_tests_spec.rb | 35 +++++++++++++++-------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 3c96f07..e0b6e16 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -2975,7 +2975,7 @@ def pointAlongSegment?(p0 = nil, sg = []) abn = b - a abn.normalize ap = p0 - a - sp = ap.dot(abn) + sp = ap.dot(abn) return false if sp < 0 apd = scalar(abn, sp) @@ -3018,8 +3018,8 @@ def pointAlongSegments?(p0 = nil, sgs = []) # @return [OpenStudio::Point3d] point of intersection of both lines # @return [nil] if no intersection, equal, or invalid input (see logs) def getLineIntersection(s1 = [], s2 = []) - s1 = getSegments(s1) - s2 = getSegments(s2) + s1 = getSegments(s1) + s2 = getSegments(s2) return nil if s1.empty? return nil if s2.empty? @@ -3030,10 +3030,10 @@ def getLineIntersection(s1 = [], s2 = []) return nil if same?(s1, s2) return nil if same?(s1, s2.to_a.reverse) - a1 = s1[0] - a2 = s1[1] - b1 = s2[0] - b2 = s2[1] + a1 = s1.first + b1 = s2.first + a2 = s1.last + b2 = s2.last # Matching segment endpoints? return a1 if same?(a1, b1) @@ -3042,18 +3042,18 @@ def getLineIntersection(s1 = [], s2 = []) return a2 if same?(a2, b2) # Segment endpoint along opposite segment? - return a1 if pointAlongSegments?(a1, s2) - return a2 if pointAlongSegments?(a2, s2) - return b1 if pointAlongSegments?(b1, s1) - return b2 if pointAlongSegments?(b2, s1) + return a1 if pointAlongSegment?(a1, s2) + return a2 if pointAlongSegment?(a2, s2) + return b1 if pointAlongSegment?(b1, s1) + return b2 if pointAlongSegment?(b2, s1) - # Line segments as vectors. Skip if colinear. + # Line segments as vectors. Skip if collinear or parallel. a = a2 - a1 b = b2 - b1 xab = a.cross(b) return nil if xab.length.round(4) < TOL2 - # Link 1st point to other segment endpoints as vectors. Must be coplanar. + # Link 1st point to other segment endpoints, as vectors. Must be coplanar. a1b1 = b1 - a1 a1b2 = b2 - a1 xa1b1 = a.cross(a1b1) @@ -3094,7 +3094,7 @@ def getLineIntersection(s1 = [], s2 = []) return nil if a.dot(p0 - a1) < 0 # Ensure intersection is sandwiched between endpoints. - return nil unless pointAlongSegments?(p0, s2) && pointAlongSegments?(p0, s1) + return nil unless pointAlongSegment?(p0, s2) && pointAlongSegment?(p0, s1) p0 end @@ -3253,8 +3253,11 @@ def getCollinears(pts = nil, n = 0) pts = getUniques(pts) ok = n.respond_to?(:to_i) v = OpenStudio::Point3dVector.new + a = [] return pts if pts.size < 3 return mismatch("n collinears", n, Integer, mth, DBG, v) unless ok + return invalid("+n collinears", mth, 0, ERR, v) if n > pts.size + return invalid("-n collinears", mth, 0, ERR, v) if n < 0 && n.abs > pts.size ncolls = getNonCollinears(pts) return pts if ncolls.empty? @@ -3654,22 +3657,17 @@ def overlap(p1 = nil, p2 = nil, flat = false) cw1 = clockwise?(p01) a1 = cw1 ? p01.to_a.reverse : p01.to_a a2 = p02.to_a - a2 = flatten(a2).to_a if flat - return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z) - - cw2 = clockwise?(a2) - a2 = a2.reverse if cw2 else t = OpenStudio::Transformation.alignFace(p01) a1 = t.inverse * p01 a2 = t.inverse * p02 - a2 = flatten(a2).to_a if flat - return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z) - - cw2 = clockwise?(a2) - a2 = a2.reverse if cw2 end + a2 = flatten(a2).to_a if flat + cw2 = clockwise?(a2) + a2 = a2.reverse if cw2 + return invalid("points 2", mth, 2, DBG, face) unless xyz?(a2, :z) + # Return either (transformed) polygon if one fits into the other. p1t = p01 @@ -3749,7 +3747,7 @@ def cast(p1 = nil, p2 = nil, ray = nil) p2 = poly(p2) return face if p1.empty? return face if p2.empty? - return mismatch("ray", ray, cl, mth) unless ray.is_a?(cl) + return mismatch("ray", ray, cl, mth, face) unless ray.is_a?(cl) # From OpenStudio SDK v3.7.0 onwards, one could/should rely on: # @@ -4082,7 +4080,7 @@ def outline(a = [], bfr = 0, flat = true) # # @param [Set] a triad (3D points) # - # @return [Set] a rectangular ULC box (see logs if empty) + # @return [Set] a rectangular BLC box (see logs if empty) def triadBox(pts = nil) mth = "OSut::#{__callee__}" bkp = OpenStudio::Point3dVector.new @@ -4233,7 +4231,7 @@ def boundedBox(pts = nil) next if same?(p1, sg.first) next if same?(p1, sg.last) next if same?(p2, sg.first) - next if same?(p2, sg.first) + next if same?(p2, sg.last) out = triadBox(OpenStudio::Point3dVector.new([m0, p1, p2])) next if out.empty? @@ -4500,7 +4498,7 @@ def getRealignedFace(pts = nil, force = false) # @param pts [Set] 3D points, once re/aligned # @param force [Bool] whether to force rotation of (narrow) bounded box # - # @return [Float] width al©ong X-axis, once re/aligned + # @return [Float] width along X-axis, once re/aligned # @return [0.0] if invalid inputs def alignedWidth(pts = nil, force = false) mth = "OSut::#{__callee__}" @@ -5150,7 +5148,7 @@ def genSlab(pltz = [], z = 0) pltz.each_with_index do |plt, i| id = "plate # #{i+1} (index #{i})" - return mismatch(id, plt, cl1, mth, DBG, slb) unless plt.is_a?(cl2) + return mismatch(id, plt, cl2, mth, DBG, slb) unless plt.is_a?(cl2) return hashkey( id, plt, :x, mth, DBG, slb) unless plt.key?(:x ) return hashkey( id, plt, :y, mth, DBG, slb) unless plt.key?(:y ) return hashkey( id, plt, :dx, mth, DBG, slb) unless plt.key?(:dx) @@ -7447,7 +7445,7 @@ def addSkyLights(spaces = [], opts = {}) pattern = "array" elsif fpm2.keys.include?("strips") pattern = "strips" - else fpm2.keys.include?("strip") + else # fpm2.keys.include?("strip") pattern = "strip" end else @@ -7458,7 +7456,7 @@ def addSkyLights(spaces = [], opts = {}) pattern = "strip" elsif fpm2.keys.include?("strips") pattern = "strips" - else fpm2.keys.include?("array") + else # fpm2.keys.include?("array") pattern = "array" end end @@ -7561,7 +7559,7 @@ def addSkyLights(spaces = [], opts = {}) # Size contraction: round 2: prioritize larger sets. adm2 = 0 - sets.each_with_index do |set, i| + sets.each_with_index do |set| next if set[:w].round(2) <= w0 next if set[:d].round(2) <= w0 diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 0cf2239..04d44f6 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -1306,8 +1306,8 @@ module M expect(schedule.setScheduleTypeLimits(limits)).to be true tvals = schedule.timeSeries.values - expect(tvals).is_a?(OpenStudio::Vector) - tvals.each { |tval| expect(tval.is_a?(Numeric)) } + expect(tvals).to be_a(OpenStudio::Vector) + tvals.each { |tval| expect(tval).to be_a(Numeric) } availability = M.availabilitySchedule(model) @@ -2354,10 +2354,10 @@ module M glazing = OpenStudio::Model::SubSurface.new(vec, model) # Glazing fits?, overlaps? parallel? - expect(mod1.parallel?(glazing, wall)).to be(true) + expect(mod1.parallel?(glazing, wall)).to be true expect(mod1.fits?(glazing, wall)).to be true expect(mod1.overlaps?(glazing, wall)).to be true - expect(mod1.parallel?(wall, glazing)).to be(true) + expect(mod1.parallel?(wall, glazing)).to be true expect(mod1.fits?(wall, glazing)).to be true expect(mod1.overlaps?(wall, glazing)).to be true @@ -2510,7 +2510,7 @@ module M # Overlap between roof and vertically-cast ceiling onto roof plane. olap02 = mod1.overlap(roof, cast02) expect(olap02.size).to eq(3) # not 5 - expect(mod1.fits?(olap02, roof)).to be(true) + expect(mod1.fits?(olap02, roof)).to be true olap02.each { |pt| expect(pl2.pointOnPlane(pt)) } @@ -2782,7 +2782,7 @@ module M collinears = mod1.getCollinears( [p0, p1, p2, p3] ) expect(collinears.size).to eq(1) - expect(mod1.same?(collinears[0], p1)).to be true + expect(mod1.same?(collinears.first, p1)).to be true # CASE a1: 2x end-to-end line segments (returns matching endpoints). expect(mod1.lineIntersects?( [p0, p1], [p1, p2] )).to be true @@ -2832,6 +2832,7 @@ module M expect(mod1.pointWithinPolygon?(p5, [p0, p1, p2, p3])).to be true expect(mod1.pointWithinPolygon?(p6, [p0, p1, p2, p3])).to be false expect(mod1.pointWithinPolygon?(p7, [p0, p1, p2, p3])).to be true + expect(mod1.status).to be_zero # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # @@ -2848,6 +2849,7 @@ module M expect(mod1.logs.first[:message]).to include("Empty 'plane'") expect(mod1.clean!).to eq(DBG) + # Test self-intersecting polygon. If reactivated, OpenStudio logs to stdout: # [utilities.Transformation] <1> Cannot compute outward normal for vertices # vtx = OpenStudio::Point3dVector.new @@ -2855,11 +2857,11 @@ module M # vtx << OpenStudio::Point3d.new( 0, 0, 10) # vtx << OpenStudio::Point3d.new(20, 0, 0) # vtx << OpenStudio::Point3d.new( 0, 0, 0) - # - # expect(mod1.poly(vtx)).to be_empty - # expect(mod1.status).to eq(ERR) - # expect(mod1.logs.size).to eq(1) - # expect(mod1.logs.first[:message]).to include("'(unaligned) points'") + + # Original polygon remains unaltered. + # vtx2 = mod1.poly(vtx) + # expect(mod1.same?(vtx, vtx2)).to be true + # expect(mod1.status).to eq(0) # expect(mod1.clean!).to eq(DBG) # Regular polygon, counterclockwise yet not UpperLeftCorner (ULC). @@ -5441,6 +5443,17 @@ module M expect(surface).to be_a(OpenStudio::Model::Surface) expect(surface.vertices.size).to eq(12) expect(surface.grossArea).to be_within(TOL).of(5 * 20 - 1) + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Invalid input case. + plates = ["osut"] + slab = mod1.genSlab(plates, z0) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to include("String? expecting Hash") + expect(slab).to be_a(OpenStudio::Point3dVector) + expect(slab).to be_empty + end it "checks roller shades" do From 5b9a05236b75a9f0523979ebb50acf2c710bc67b Mon Sep 17 00:00:00 2001 From: brgix Date: Sun, 10 Aug 2025 14:47:45 -0400 Subject: [PATCH 06/15] Tweaks methods relying on OpenStudio::Point3dVector (vs Array) --- lib/osut/utils.rb | 116 +++++++++++++++------------ spec/osut_tests_spec.rb | 172 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 233 insertions(+), 55 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index e0b6e16..4fc6b82 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -2443,31 +2443,31 @@ def scalar(v = OpenStudio::Vector3d.new, m = 0) ## # Returns OpenStudio 3D points as an OpenStudio point vector, validating - # points in the process (if Array). + # points in the process (e.g. if Array). # # @param pts [Set] OpenStudio 3D points # # @return [OpenStudio::Point3dVector] 3D vector (see logs if empty) def to_p3Dv(pts = nil) mth = "OSut::#{__callee__}" - cl1 = OpenStudio::Point3d - cl2 = OpenStudio::Point3dVector - cl3 = OpenStudio::Model::PlanarSurface - cl4 = Array v = OpenStudio::Point3dVector.new - if pts.is_a?(cl1) + if pts.is_a?(OpenStudio::Point3d) v << pts return v + elsif pts.is_a?(OpenStudio::Point3dVector) + return pts + elsif pts.is_a?(OpenStudio::Model::PlanarSurface) + pts.vertices.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) } + return v end - return pts if pts.is_a?(cl2) - return pts.vertices if pts.is_a?(cl3) - - return mismatch("points", pts, cl1, mth, DBG, v) unless pts.is_a?(cl4) + return mismatch("points", pts, Array, mth, DBG, v) unless pts.is_a?(Array) pts.each do |pt| - return mismatch("point", pt, cl4, mth, DBG, v) unless pt.is_a?(cl1) + unless pt.is_a?(OpenStudio::Point3d) + return mismatch("point", pt, OpenStudio::Point3d, mth, DBG, v) + end end pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) } @@ -2743,7 +2743,7 @@ def nextUp(pts = nil, pt = nil) pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) } - pair.nil? ? pts.first : pair.last + pair.nil? ? pts[0] : pair[-1] end ## @@ -2834,21 +2834,25 @@ def verticalPlane(p1 = nil, p2 = nil) # @param pts [Set 0 - v = v[n..-1] if n < 0 + n = 0 if n.abs > v.size + v = v[0..n-1] if n > 0 + v = v[n..-1] if n < 0 v end @@ -2965,12 +2969,11 @@ def pointAlongSegment?(p0 = nil, sg = []) cl1 = OpenStudio::Point3d cl2 = OpenStudio::Point3dVector return mismatch( "point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1) - return mismatch("segment", sg, cl2, mth, DBG, false) unless segment?(sg) - + return false unless segment?(sg) return true if holds?(sg, p0) - a = sg.first - b = sg.last + a = sg[ 0] + b = sg[-1] ab = b - a abn = b - a abn.normalize @@ -3201,28 +3204,33 @@ def blc(pts = nil) end ## - # Returns sequential non-collinear points in an OpenStudio 3D point vector. + # Returns non-collinear points in an OpenStudio 3D point vector. # # @param pts [Set 0 - a = a[n-1..-1] if n < 0 + n = 0 if n.abs > a.size + a = a[0..n-1] if n > 0 + a = a[n..-1] if n < 0 to_p3Dv(a) end ## - # Returns sequential collinear points in an OpenStudio 3D point vector. + # Returns collinear points in an OpenStudio 3D point vector. # # @param pts [Set pts.size - return invalid("-n collinears", mth, 0, ERR, v) if n < 0 && n.abs > pts.size + + if n.is_a?(Numeric) + n = n.to_i + else + mismatch("n points", n, Integer, mth, DBG, pts) + n = 0 + end ncolls = getNonCollinears(pts) - return pts if ncolls.empty? + return a if ncolls.empty? - to_p3Dv( pts.delete_if { |pt| holds?(ncolls, pt) } ) + a = pts.delete_if { |pt| holds?(ncolls, pt) } + + n = 0 if n.abs > a.size + a = a[0..n-1] if n > 0 + a = a[n..-1] if n < 0 + + a end ## @@ -7735,8 +7751,8 @@ def addSkyLights(spaces = [], opts = {}) sX = getSegments(t0 * vX) s0.each_with_index do |sg, j| - sg0 = sg.to_a - sgX = sX[j].to_a + sg0 = sg + sgX = sX[j] vec = OpenStudio::Point3dVector.new vec << sg0.first vec << sg0.last diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 04d44f6..76f3b64 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -2779,10 +2779,172 @@ module M # Independent point. p7 = OpenStudio::Point3d.new(14, 20, -5) + p8 = OpenStudio::Point3d.new(-9, -9, -5) + + # Stress test 'to_p3Dv'. 4 valid input cases. + # Valid case #1: a single Point3d. + vtx = mod1.to_p3Dv(p0) + expect(vtx).to be_a(OpenStudio::Point3dVector) + expect(vtx[0]).to eq(p0) # same object ID + + # Valid case #2: a Point3dVector. + vtxx = OpenStudio::Point3dVector.new + vtxx << p0 + vtxx << p1 + vtxx << p2 + vtxx << p3 + vtx = mod1.to_p3Dv(vtxx) + expect(vtx).to be_a(OpenStudio::Point3dVector) + expect(vtx[ 0]).to eq(p0) # same object ID + expect(vtx[ 1]).to eq(p1) # same object ID + expect(vtx[ 2]).to eq(p2) # same object ID + expect(vtx[-1]).to eq(p3) # same object ID + + # Valid case #3: Surface vertices. + model = OpenStudio::Model::Model.new + surface = OpenStudio::Model::Surface.new(vtxx, model) + expect(surface.vertices).to be_an(Array) # not an OpenStudio::Point3dVector + expect(surface.vertices.size).to eq(4) + vtx = mod1.to_p3Dv(vtxx) + expect(vtx).to be_a(OpenStudio::Point3dVector) + expect(vtx.size).to eq(4) + expect(vtx[0]).to eq(p0) + expect(vtx[1]).to eq(p1) + expect(vtx[2]).to eq(p2) + expect(vtx[3]).to eq(p3) + + # Valid case #4: Array. + vtx = mod1.to_p3Dv([p0, p1, p2, p3]) + expect(vtx).to be_a(OpenStudio::Point3dVector) + expect(vtx.size).to eq(4) + expect(vtx[0]).to eq(p0) + expect(vtx[1]).to eq(p1) + expect(vtx[2]).to eq(p2) + expect(vtx[3]).to eq(p3) + expect(mod1.status).to eq(0) + + # Stress test 'nextUp'. Invalid case. + m0 = "Invalid 'points (2+)' arg #1 (OSut::nextUp)" + pt = mod1.nextUp([], p0) + expect(pt).to be nil + expect(mod1.warn?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m0) + expect(mod1.clean!).to eq(DBG) + + # Valid case. + pt = mod1.nextUp([p0, p1, p2, p3], p0) + expect(pt).to be_a(OpenStudio::Point3d) + expect(pt).to eq(p1) + expect(mod1.status).to eq(0) + + pt = mod1.nextUp([p0, p0, p0], p0) + expect(pt).to be_a(OpenStudio::Point3d) + expect(pt).to eq(p0) + expect(mod1.status).to eq(0) + + # Stress test 'getSegments'. Invalid case. + sgs = mod1.getSegments(p3) + expect(sgs).to be_a(OpenStudio::Point3dVectorVector) + expect(sgs).to be_empty + expect(mod1.status).to eq(0) # nothing logged + + sgs = mod1.getSegments([p3, p3]) + expect(sgs).to be_a(OpenStudio::Point3dVectorVector) + expect(sgs).to be_empty + expect(mod1.status).to eq(0) # nothing logged - collinears = mod1.getCollinears( [p0, p1, p2, p3] ) + # Valid case. + sgs = mod1.getSegments([p0, p1, p2, p3]) + expect(sgs).to be_a(OpenStudio::Point3dVectorVector) + expect(sgs.size).to eq(4) + expect(sgs).to respond_to(:first) + expect(sgs).to_not respond_to(:last) + expect(sgs[-1]).to be_an(Array) # not an OpenStudio::Point3dVector + + # Stress test 'uniques'. + m0 = "'n points' String? expecting Integer (OSut::getUniques)" + + # Invalid number case - simply returns entire list of unique points. + uniks = mod1.getUniques([p0, p1, p2, p3], "osut") + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(4) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m0) + expect(mod1.clean!).to eq(DBG) + + # Valid, basic case. + uniks = mod1.getUniques([p0, p1, p2, p3]) + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(4) + + uniks = mod1.getUniques([p0, p1, p2, p3], 0) + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(4) + + # Valid, first 3 points. + uniks = mod1.getUniques([p0, p1, p2, p3], 3) + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(3) + + # Valid, last 3 points. + uniks = mod1.getUniques([p0, p1, p2, p3], -3) + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(3) + + # Valid, n = 5: returns original 4 uniques points. + uniks = mod1.getUniques([p0, p1, p2, p3], 5) + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(4) + + # Valid, n = -5: returns original 4 uniques points. + uniks = mod1.getUniques([p0, p1, p2, p3], -5) + expect(uniks).to be_a(OpenStudio::Point3dVector) + expect(uniks.size).to eq(4) + + # Stress tests collinears. + m0 = "'n points' String? expecting Integer (OSut::getCollinears)" + + # Invalid case - raise DEBUG message, yet returns valid collinears. + collinears = mod1.getCollinears([p0, p1, p3, p8], "osut") + expect(collinears).to be_a(OpenStudio::Point3dVector) expect(collinears.size).to eq(1) - expect(mod1.same?(collinears.first, p1)).to be true + expect(collinears[0]).to eq(p0) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m0) + expect(mod1.clean!).to eq(DBG) + + # Valid, basic case + collinears = mod1.getCollinears([p0, p1, p3, p8]) + expect(collinears.size).to eq(1) + expect(collinears[0]).to eq(p0) # same object ID + expect(mod1.same?(collinears.first, p0)).to be true # more expensive way + + collinears = mod1.getCollinears([p0, p1, p3, p8], 0) + expect(collinears.size).to eq(1) + expect(collinears[0]).to eq(p0) + + collinears = mod1.getCollinears([p0, p1, p2, p3, p8]) + expect(collinears.size).to eq(2) + expect(collinears[0]).to eq(p0) + expect(collinears[-1]).to eq(p1) + expect(mod1.pointAlongSegment?(p0, sgs.first)) # sg is an Array (size = 2) + + # Stress test pointAlongSegment? Invalid case. + m0 = "'points' String? expecting Array (OSut::to_p3Dv)" + expect(mod1.pointAlongSegment?(p3, "osut")).to be false + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m0) + expect(mod1.clean!).to eq(DBG) + + # Valid case. + pts = OpenStudio::Point3dVector.new + pts << p0 + pts << p1 + expect(mod1.pointAlongSegment?(p3, pts)).to be false # CASE a1: 2x end-to-end line segments (returns matching endpoints). expect(mod1.lineIntersects?( [p0, p1], [p1, p2] )).to be true @@ -2875,8 +3037,8 @@ module M expect(segments.size).to eq(3) segments.each_with_index do |segment, i| - unless mod1.xyz?(segment, :x, segment.first.x) - vplane = mod1.verticalPlane(segment.first, segment.last) + unless mod1.xyz?(segment, :x, segment[0].x) + vplane = mod1.verticalPlane(segment[0], segment[-1]) expect(vplane).to be_a(OpenStudio::Plane) end end @@ -5182,7 +5344,7 @@ module M file = File.join(__dir__, "files/osms/out/warehouse_sky.osm") model.save(file, true) end - + it "checks facet retrieval" do translator = OpenStudio::OSVersion::VersionTranslator.new expect(mod1.reset(DBG)).to eq(DBG) From 7c289b375e951f368f5378768bb09c3c27c5a15e Mon Sep 17 00:00:00 2001 From: brgix Date: Mon, 11 Aug 2025 08:44:47 -0400 Subject: [PATCH 07/15] Fixes typos, invalid variables/access, etc. --- lib/osut/utils.rb | 44 ++++++++++++++++++++--------------------- spec/osut_tests_spec.rb | 43 +++++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 4fc6b82..a829fc8 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -2896,7 +2896,6 @@ def getSegments(pts = nil) # @return [false] if invalid input (see logs) def segment?(pts = nil) pts = to_p3Dv(pts) - return false if pts.empty? return false unless pts.size == 2 return false if same?(pts[0], pts[1]) @@ -2968,7 +2967,7 @@ def pointAlongSegment?(p0 = nil, sg = []) mth = "OSut::#{__callee__}" cl1 = OpenStudio::Point3d cl2 = OpenStudio::Point3dVector - return mismatch( "point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1) + return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1) return false unless segment?(sg) return true if holds?(sg, p0) @@ -3004,8 +3003,8 @@ def pointAlongSegments?(p0 = nil, sgs = []) cl1 = OpenStudio::Point3d cl2 = OpenStudio::Point3dVectorVector sgs = sgs.is_a?(cl2) ? sgs : getSegments(sgs) - return empty("segments", mth, DBG, false) if sgs.empty? - return mismatch("point", p0, cl, mth, DBG, false) unless p0.is_a?(cl1) + return empty("segments", mth, DBG, false) if sgs.empty? + return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1) sgs.each { |sg| return true if pointAlongSegment?(p0, sg) } @@ -3111,8 +3110,8 @@ def getLineIntersection(s1 = [], s2 = []) # @return [Bool] whether 3D line intersects 3D segments # @return [false] if invalid input (see logs) def lineIntersects?(l = [], s = []) - l = getSegments(l) - s = getSegments(s) + l = getSegments(l) + s = getSegments(s) return nil if l.empty? return nil if s.empty? @@ -4759,8 +4758,9 @@ def genExtendedVertices(s = nil, set = [], tag = :vtx) set.each_with_index do |st, i| str1 = id + "subset ##{i+1}" str2 = str1 + " #{tag.to_s}" - next if st.key?(:void) && st[:void] return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?) + next if st.key?(:void) && st[:void] + return hashkey( str1, st, tag, mth, DBG, a) unless st.key?(tag) return empty("#{str2} vertices", mth, DBG, a) if st[tag].empty? return hashkey( str1, st, :ld, mth, DBG, a) unless st.key?(:ld) @@ -4769,9 +4769,9 @@ def genExtendedVertices(s = nil, set = [], tag = :vtx) return invalid("#{str2} polygon", mth, 0, DBG, a) if stt.empty? ld = st[:ld] - return mismatch(str, ld, Hash, mth, DBG, a) unless ld.is_a?(Hash) - return hashkey( str, ld, s, mth, DBG, a) unless ld.key?(s) - return mismatch(str, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl) + return mismatch(str2, ld, Hash, mth, DBG, a) unless ld.is_a?(Hash) + return hashkey( str2, ld, s, mth, DBG, a) unless ld.key?(s) + return mismatch(str2, ld[s], cl, mth, DBG, a) unless ld[s].is_a?(cl) end # Re-sequence polygon vertices. @@ -6056,7 +6056,6 @@ def grossRoofArea(spaces = []) # previously-added leader lines. # # @todo: revise approach for attics ONCE skylight wells have been added. - olap = nil olap = overlap(cst, rvi, false) next if olap.empty? @@ -6173,7 +6172,7 @@ def toToplit(spaces = [], opts = {}) if opts[:size].respond_to?(:to_f) w = opts[:size].to_f w2 = w * w - return invalid(size, mth, 0, ERR, []) if w.round(2) < gap4 + return invalid("size", mth, 0, ERR, []) if w.round(2) < gap4 else return mismatch("size", opts[:size], Numeric, mth, DBG, []) end @@ -6199,9 +6198,9 @@ def toToplit(spaces = [], opts = {}) spaces = spaces.reject { |sp| getRoofs(sp).empty? } spaces = spaces.reject { |sp| sp.floorArea < 4 * w2 } spaces = spaces.sort_by(&:floorArea).reverse - return empty("spaces", mth, WRN, 0) if spaces.empty? + return empty("spaces", mth, WRN, []) if spaces.empty? else - return mismatch("spaces", spaces, Array, mth, DBG, 0) + return mismatch("spaces", spaces, Array, mth, DBG, []) end # Unfenestrated spaces have no windows, glazed doors or skylights. By @@ -6317,6 +6316,7 @@ def addSkyLights(spaces = [], opts = {}) bfr = 0.005 # minimum array perimeter buffer (no wells) w = 1.22 # default 48" x 48" skylight base w2 = w * w # m2 + v = OpenStudio.openStudioVersion.split(".").join.to_i # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # # Excerpts of ASHRAE 90.1 2022 definitions: @@ -6433,10 +6433,10 @@ def addSkyLights(spaces = [], opts = {}) if frame.respond_to?(:frameWidth) frame = nil if v < 321 - frame = nil if f.frameWidth.round(2) < 0 - frame = nil if f.frameWidth.round(2) > gap + frame = nil if frame.frameWidth.round(2) < 0 + frame = nil if frame.frameWidth.round(2) > gap - f = f.frameWidth if frame + f = frame.frameWidth if frame log(WRN, "Skip Frame&Divider (#{mth})") unless frame else frame = nil @@ -6661,14 +6661,14 @@ def addSkyLights(spaces = [], opts = {}) next unless opts[opt] == false case opt - when :sidelit then filters.map! { |f| f.include?("b") ? f.delete("b") : f } - when :sloped then filters.map! { |f| f.include?("c") ? f.delete("c") : f } - when :plenum then filters.map! { |f| f.include?("d") ? f.delete("d") : f } - when :attic then filters.map! { |f| f.include?("e") ? f.delete("e") : f } + when :sidelit then filters.map! { |fl| fl.include?("b") ? fl.delete("b") : fl } + when :sloped then filters.map! { |fl| fl.include?("c") ? fl.delete("c") : fl } + when :plenum then filters.map! { |fl| fl.include?("d") ? fl.delete("d") : fl } + when :attic then filters.map! { |fl| fl.include?("e") ? fl.delete("e") : fl } end end - filters.reject! { |f| f.empty? } + filters.reject! { |fl| fl.empty? } filters.uniq! # Remaining filters may be further pruned automatically after space/roof diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 76f3b64..cd08e64 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -2836,12 +2836,10 @@ module M pt = mod1.nextUp([p0, p1, p2, p3], p0) expect(pt).to be_a(OpenStudio::Point3d) expect(pt).to eq(p1) - expect(mod1.status).to eq(0) pt = mod1.nextUp([p0, p0, p0], p0) expect(pt).to be_a(OpenStudio::Point3d) expect(pt).to eq(p0) - expect(mod1.status).to eq(0) # Stress test 'getSegments'. Invalid case. sgs = mod1.getSegments(p3) @@ -2928,12 +2926,47 @@ module M collinears = mod1.getCollinears([p0, p1, p2, p3, p8]) expect(collinears.size).to eq(2) - expect(collinears[0]).to eq(p0) + expect(collinears[ 0]).to eq(p0) expect(collinears[-1]).to eq(p1) expect(mod1.pointAlongSegment?(p0, sgs.first)) # sg is an Array (size = 2) - # Stress test pointAlongSegment? Invalid case. + # Only 2 collinears, so request for first 3 is ignored. + collinears = mod1.getCollinears([p0, p1, p2, p3, p8], 3) + expect(collinears.size).to eq(2) + expect(mod1.same?(collinears[0], p0)).to be true + expect(mod1.same?(collinears[1], p1)).to be true + + # First collinear (out of 2). + collinears = mod1.getCollinears([p0, p1, p2, p3, p8], 1) + expect(collinears.size).to eq(1) + expect(mod1.same?(collinears[0], p0)).to be true + + # Last collinear (out of 2). + collinears = mod1.getCollinears([p0, p1, p2, p3, p8], -1) + expect(collinears.size).to eq(1) + expect(mod1.same?(collinears[0], p1)).to be true + + # First two vs last two: same result. + collinears = mod1.getCollinears([p0, p1, p2, p3, p8], -2) + expect(collinears.size).to eq(2) + expect(mod1.same?(collinears[0], p0)).to be true + expect(mod1.same?(collinears[1], p1)).to be true + + # Ignore n request when n.abs > number of actual collinears. + collinears = mod1.getCollinears([p0, p1, p2, p3, p8], 6) + expect(collinears.size).to eq(2) + expect(mod1.same?(collinears[0], p0)).to be true + expect(mod1.same?(collinears[1], p1)).to be true + + collinears = mod1.getCollinears([p0, p1, p2, p3, p8], -6) + expect(collinears.size).to eq(2) + expect(mod1.same?(collinears[0], p0)).to be true + expect(mod1.same?(collinears[1], p1)).to be true + + # Stress test pointAlongSegment? m0 = "'points' String? expecting Array (OSut::to_p3Dv)" + + # Invalid case. expect(mod1.pointAlongSegment?(p3, "osut")).to be false expect(mod1.debug?).to be true expect(mod1.logs.size).to eq(1) @@ -5344,7 +5377,7 @@ module M file = File.join(__dir__, "files/osms/out/warehouse_sky.osm") model.save(file, true) end - + it "checks facet retrieval" do translator = OpenStudio::OSVersion::VersionTranslator.new expect(mod1.reset(DBG)).to eq(DBG) From 6ec6ce881ade093f24d4ea55fe02932658f9a9c6 Mon Sep 17 00:00:00 2001 From: brgix Date: Mon, 11 Aug 2025 09:00:07 -0400 Subject: [PATCH 08/15] Updates version --- LICENSE | 2 +- lib/osut.rb | 2 +- lib/osut/utils.rb | 2 +- lib/osut/version.rb | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index f8d9959..781b8cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022-2024, Denis Bourgeois +Copyright (c) 2022-2025, Denis Bourgeois All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/lib/osut.rb b/lib/osut.rb index 4772084..7561fec 100644 --- a/lib/osut.rb +++ b/lib/osut.rb @@ -1,6 +1,6 @@ # BSD 3-Clause License # -# Copyright (c) 2022-2024, Denis Bourgeois +# Copyright (c) 2022-2025, Denis Bourgeois # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index a829fc8..3d91c67 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -1,6 +1,6 @@ # BSD 3-Clause License # -# Copyright (c) 2022-2024, Denis Bourgeois +# Copyright (c) 2022-2025, Denis Bourgeois # All rights reserved. # # Redistribution and use in source and binary forms, with or without diff --git a/lib/osut/version.rb b/lib/osut/version.rb index 68bb03c..116357a 100644 --- a/lib/osut/version.rb +++ b/lib/osut/version.rb @@ -1,6 +1,6 @@ # BSD 3-Clause License # -# Copyright (c) 2022-2024, Denis Bourgeois +# Copyright (c) 2022-2025, Denis Bourgeois # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -29,5 +29,5 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. module OSut - VERSION = "0.6.0".freeze # OSut version + VERSION = "0.6.1".freeze # OSut version end From aab9a571a74b8e73cb392c93f8204c36ff27f7ce Mon Sep 17 00:00:00 2001 From: brgix Date: Mon, 11 Aug 2025 11:54:38 -0400 Subject: [PATCH 09/15] Renames getters - as per pyOSut --- lib/osut/utils.rb | 170 +++++++++++++++++------------------ spec/osut_tests_spec.rb | 190 ++++++++++++++++++++-------------------- 2 files changed, 180 insertions(+), 180 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 3d91c67..de2a5b1 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -2835,7 +2835,7 @@ def verticalPlane(p1 = nil, p2 = nil) # @param n [#to_i] requested number of unique points (0 returns all) # # @return [OpenStudio::Point3dVector] unique points (see logs) - def getUniques(pts = nil, n = 0) + def uniques(pts = nil, n = 0) mth = "OSut::#{__callee__}" pts = to_p3Dv(pts) v = OpenStudio::Point3dVector.new @@ -2866,10 +2866,10 @@ def getUniques(pts = nil, n = 0) # @param pts [Set] 3D points # # @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty) - def getSegments(pts = nil) + def segments(pts = nil) mth = "OSut::#{__callee__}" vv = OpenStudio::Point3dVectorVector.new - pts = getUniques(pts) + pts = uniques(pts) return vv if pts.size < 2 pts.each_with_index do |p1, i1| @@ -2912,10 +2912,10 @@ def segment?(pts = nil) # @param pts [OpenStudio::Point3dVector] 3D points # # @return [OpenStudio::Point3dVectorVector] triads (see logs if empty) - def getTriads(pts = nil, co = false) + def triads(pts = nil, co = false) mth = "OSut::#{__callee__}" vv = OpenStudio::Point3dVectorVector.new - pts = getUniques(pts) + pts = uniques(pts) return vv if pts.size < 2 pts.each_with_index do |p1, i1| @@ -3002,7 +3002,7 @@ def pointAlongSegments?(p0 = nil, sgs = []) mth = "OSut::#{__callee__}" cl1 = OpenStudio::Point3d cl2 = OpenStudio::Point3dVectorVector - sgs = sgs.is_a?(cl2) ? sgs : getSegments(sgs) + sgs = sgs.is_a?(cl2) ? sgs : segments(sgs) return empty("segments", mth, DBG, false) if sgs.empty? return mismatch("point", p0, cl1, mth, DBG, false) unless p0.is_a?(cl1) @@ -3019,9 +3019,9 @@ def pointAlongSegments?(p0 = nil, sgs = []) # # @return [OpenStudio::Point3d] point of intersection of both lines # @return [nil] if no intersection, equal, or invalid input (see logs) - def getLineIntersection(s1 = [], s2 = []) - s1 = getSegments(s1) - s2 = getSegments(s2) + def lineIntersection(s1 = [], s2 = []) + s1 = segments(s1) + s2 = segments(s2) return nil if s1.empty? return nil if s2.empty? @@ -3110,14 +3110,14 @@ def getLineIntersection(s1 = [], s2 = []) # @return [Bool] whether 3D line intersects 3D segments # @return [false] if invalid input (see logs) def lineIntersects?(l = [], s = []) - l = getSegments(l) - s = getSegments(s) + l = segments(l) + s = segments(s) return nil if l.empty? return nil if s.empty? l = l.first - s.each { |segment| return true if getLineIntersection(l, segment) } + s.each { |segment| return true if lineIntersection(l, segment) } false end @@ -3209,10 +3209,10 @@ def blc(pts = nil) # @param n [#to_i] requested number of non-collinears (0 returns all) # # @return [OpenStudio::Point3dVector] non-collinears (see logs) - def getNonCollinears(pts = nil, n = 0) + def nonCollinears(pts = nil, n = 0) mth = "OSut::#{__callee__}" a = [] - pts = getUniques(pts) + pts = uniques(pts) return pts if pts.size < 3 if n.is_a?(Numeric) @@ -3255,10 +3255,10 @@ def getNonCollinears(pts = nil, n = 0) # @param n [#to_i] requested number of collinears (0 returns all) # # @return [OpenStudio::Point3dVector] collinears (see logs) - def getCollinears(pts = nil, n = 0) + def collinears(pts = nil, n = 0) mth = "OSut::#{__callee__}" a = OpenStudio::Point3dVector.new - pts = getUniques(pts) + pts = uniques(pts) return pts if pts.size < 3 if n.is_a?(Numeric) @@ -3268,7 +3268,7 @@ def getCollinears(pts = nil, n = 0) n = 0 end - ncolls = getNonCollinears(pts) + ncolls = nonCollinears(pts) return a if ncolls.empty? a = pts.delete_if { |pt| holds?(ncolls, pt) } @@ -3314,7 +3314,7 @@ def poly(pts = nil, vx = false, uq = false, co = false, tt = false, sq = :no) # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # # Minimum 3 points? - p3 = getNonCollinears(pts, 3) + p3 = nonCollinears(pts, 3) return empty("polygon (non-collinears < 3)", mth, ERR, v) if p3.size < 3 # Coplanar? @@ -3345,8 +3345,8 @@ def poly(pts = nil, vx = false, uq = false, co = false, tt = false, sq = :no) # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # # Ensure uniqueness and/or non-collinearity. Preserve original sequence. p0 = a.first - a = getUniques(a).to_a if uq - a = getNonCollinears(a).to_a if co + a = uniques(a).to_a if uq + a = nonCollinears(a).to_a if co i0 = a.index { |pt| same?(pt, p0) } a = a.rotate(i0) unless i0.nil? @@ -3355,7 +3355,7 @@ def poly(pts = nil, vx = false, uq = false, co = false, tt = false, sq = :no) if vx && a.size > 3 zen = OpenStudio::Point3d.new(0, 0, 1000) - getTriads(a).each do |trio| + triads(a).each do |trio| p1 = trio[0] p2 = trio[1] p3 = trio[2] @@ -3421,31 +3421,31 @@ def pointWithinPolygon?(p0 = nil, s = [], entirely = false) return false unless pl.pointOnPlane(p0) entirely = false unless [true, false].include?(entirely) - segments = getSegments(s) + sgments = segments(s) # Along polygon edges, or near vertices? - if pointAlongSegments?(p0, segments) + if pointAlongSegments?(p0, sgments) return false if entirely return true unless entirely end - segments.each do |segment| + sgments.each do |sgment| # - draw vector from segment midpoint to point # - scale 1000x (assuming no building surface would be 1km wide) # - convert vector to an independent line segment # - loop through polygon segments, tally the number of intersections # - avoid double-counting polygon vertices as intersections # - return false if number of intersections is even - mid = midpoint(segment.first, segment.last) + mid = midpoint(sgment.first, sgment.last) mpV = scalar(mid - p0, 1000) p1 = p0 + mpV ctr = 0 # Skip if ~collinear. - next if mpV.cross(segment.last - segment.first).length.round(4) < TOL2 + next if mpV.cross(sgment.last - sgment.first).length.round(4) < TOL2 - segments.each do |sg| - intersect = getLineIntersection([p0, p1], sg) + sgments.each do |sg| + intersect = lineIntersection([p0, p1], sg) next unless intersect # Skip test altogether if one of the polygon vertices. @@ -3595,7 +3595,7 @@ def square?(pts = nil) return false if pts.empty? return false unless rectangular?(pts) - getSegments(pts).each do |pt| + segments(pts).each do |pt| l = (pt[1] - pt[0]).length d = l unless d return false unless l.round(2) == d.round(2) @@ -3629,7 +3629,7 @@ def fits?(p1 = nil, p2 = nil, entirely = false) p2.each { |p0| return false if pointWithinPolygon?(p0, p1, true) } # p1 segment mid-points must not lie OUTSIDE of p2. - getSegments(p1).each do |sg| + segments(p1).each do |sg| mp = midpoint(sg.first, sg.last) return false unless pointWithinPolygon?(mp, p2) end @@ -4100,7 +4100,7 @@ def triadBox(pts = nil) mth = "OSut::#{__callee__}" bkp = OpenStudio::Point3dVector.new box = [] - pts = getNonCollinears(pts) + pts = nonCollinears(pts) return bkp if pts.empty? t = xyz?(pts, :z) ? nil : OpenStudio::Transformation.alignFace(pts) @@ -4137,7 +4137,7 @@ def triadBox(pts = nil) box << OpenStudio::Point3d.new(p1.x, p1.y, p1.z) box << OpenStudio::Point3d.new(p2.x, p2.y, p2.z) box << OpenStudio::Point3d.new(p3.x, p3.y, p3.z) - box = getNonCollinears(box, 4) + box = nonCollinears(box, 4) return bkp unless box.size == 4 box = blc(box) @@ -4170,7 +4170,7 @@ def medialBox(pts = nil) # Generate vertical plane along longest segment. mpoints = [] - sgs = getSegments(pts) + sgs = segments(pts) longest = sgs.max_by { |s| OpenStudio.getDistanceSquared(s.first, s.last) } plane = verticalPlane(longest.first, longest.last) @@ -4184,7 +4184,7 @@ def medialBox(pts = nil) box << mpoints.first box << mpoints.last box << plane.project(mpoints.last) - box = getNonCollinears(box).to_a + box = nonCollinears(box).to_a return bkp unless box.size == 4 box = clockwise?(box) ? blc(box.reverse) : blc(box) @@ -4237,10 +4237,10 @@ def boundedBox(pts = nil) aire = 0 # PATH C : Right-angle, midpoint triad approach. - getSegments(pts).each do |sg| + segments(pts).each do |sg| m0 = midpoint(sg.first, sg.last) - getSegments(pts).each do |seg| + segments(pts).each do |seg| p1 = seg.first p2 = seg.last next if same?(p1, sg.first) @@ -4266,7 +4266,7 @@ def boundedBox(pts = nil) end # PATH D : Right-angle triad approach, may override PATH C boxes. - getSegments(pts).each do |sg| + segments(pts).each do |sg| p0 = sg.first p1 = sg.last @@ -4299,7 +4299,7 @@ def boundedBox(pts = nil) # PATH E : Medial box, segment approach. aire = 0 - getSegments(pts).each do |sg| + segments(pts).each do |sg| p0 = sg.first p1 = sg.last @@ -4332,7 +4332,7 @@ def boundedBox(pts = nil) # PATH F : Medial box, triad approach. aire = 0 - getTriads(pts).each do |sg| + triads(pts).each do |sg| p0 = sg[0] p1 = sg[1] p2 = sg[2] @@ -4364,7 +4364,7 @@ def boundedBox(pts = nil) holes = OpenStudio::Point3dVectorVector.new OpenStudio.computeTriangulation(outer, holes).each do |triangle| - getSegments(triangle).each do |sg| + segments(triangle).each do |sg| p0 = sg.first p1 = sg.last @@ -4421,7 +4421,7 @@ def boundedBox(pts = nil) # # @return [Hash] :set, :box, :bbox, :t, :r & :o # @return [Hash] :set, :box, :bbox, :t, :r & :o (nil) if invalid (see logs) - def getRealignedFace(pts = nil, force = false) + def realignedFace(pts = nil, force = false) mth = "OSut::#{__callee__}" out = { set: nil, box: nil, bbox: nil, t: nil, r: nil, o: nil } pts = poly(pts, false, true) @@ -4444,11 +4444,11 @@ def getRealignedFace(pts = nil, force = false) box = boundedBox(pts) return invalid("bounded box", mth, 0, DBG, out) if box.empty? - segments = getSegments(box) - return invalid("bounded box segments", mth, 0, DBG, out) if segments.empty? + sgments = segments(box) + return invalid("bounded box segments", mth, 0, DBG, out) if sgments.empty? # Deterministic ID of box rotation/translation 'origin'. - segments.each_with_index do |sg, idx| + sgments.each_with_index do |sg, idx| sgs[sg] = {} sgs[sg][:idx] = idx sgs[sg][:mid] = midpoint(sg[0], sg[1]) @@ -4468,10 +4468,10 @@ def getRealignedFace(pts = nil, force = false) i = sg0[:idx] end - k = i + 2 < segments.size ? i + 2 : i - 2 + k = i + 2 < sgments.size ? i + 2 : i - 2 - origin = midpoint(segments[i][0], segments[i][1]) - terminal = midpoint(segments[k][0], segments[k][1]) + origin = midpoint(sgments[i][0], sgments[i][1]) + terminal = midpoint(sgments[k][0], sgments[k][1]) seg = terminal - origin right = OpenStudio::Point3d.new(origin.x + d, origin.y , 0) - origin north = OpenStudio::Point3d.new(origin.x, origin.y + d, 0) - origin @@ -4525,7 +4525,7 @@ def alignedWidth(pts = nil, force = false) force = false end - pts = getRealignedFace(pts, force)[:set] + pts = realignedFace(pts, force)[:set] return 0 if pts.size < 2 pts.max_by(&:x).x - pts.min_by(&:x).x @@ -4549,7 +4549,7 @@ def alignedHeight(pts = nil, force = false) force = false end - pts = getRealignedFace(pts, force)[:set] + pts = realignedFace(pts, force)[:set] return 0 if pts.size < 2 pts.max_by(&:y).y - pts.min_by(&:y).y @@ -4648,7 +4648,7 @@ def genAnchors(s = nil, set = [], tag = :box) else st[:t] = OpenStudio::Transformation.alignFace(pts) unless st.key?(:t) tpts = st[:t].inverse * st[tag] - o = getRealignedFace(tpts, true) + o = realignedFace(tpts, true) tpts = st[:t] * (o[:r] * (o[:t] * o[:set])) st[:out] = o @@ -4666,7 +4666,7 @@ def genAnchors(s = nil, set = [], tag = :box) nb = 0 # Check for intersections between leader line and larger polygon edges. - getSegments(pts).each do |sg| + segments(pts).each do |sg| break unless nb.zero? next if holds?(sg, pt) @@ -4680,7 +4680,7 @@ def genAnchors(s = nil, set = [], tag = :box) ost = other[tag] - getSegments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) } + segments(ost).each { |sg| nb += 1 if lineIntersects?(ld, sg) } end # ... and previous leader lines (first come, first serve basis). @@ -4698,7 +4698,7 @@ def genAnchors(s = nil, set = [], tag = :box) end # Finally, check for self-intersections. - getSegments(tpts).each do |sg| + segments(tpts).each do |sg| break unless nb.zero? next if holds?(sg, tpts.first) @@ -5235,18 +5235,18 @@ def genSlab(pltz = [], z = 0) # @param spaces [Set] target spaces # # @return [Array] roofs (may be empty) - def getRoofs(spaces = []) + def roofs(spaces = []) mth = "OSut::#{__callee__}" up = OpenStudio::Point3d.new(0,0,1) - OpenStudio::Point3d.new(0,0,0) - roofs = [] + rfs = [] spaces = spaces.is_a?(OpenStudio::Model::Space) ? [spaces] : spaces spaces = spaces.respond_to?(:to_a) ? spaces.to_a : [] spaces = spaces.select { |space| space.is_a?(OpenStudio::Model::Space) } # Space-specific outdoor-facing roof surfaces. - roofs = facets(spaces, "Outdoors", "RoofCeiling") - roofs = roofs.select { |roof| roof?(roof) } + rfs = facets(spaces, "Outdoors", "RoofCeiling") + rfs = rfs.select { |rf| roof?(rf) } spaces.each do |space| # When unoccupied spaces are involved (e.g. plenums, attics), the target @@ -5282,12 +5282,12 @@ def getRoofs(spaces = []) cst = cast(cv0, rvi, up) next unless overlaps?(cst, rvi, false) - roofs << ruf unless roofs.include?(ruf) + rfs << ruf unless rfs.include?(ruf) end end end - roofs + rfs end ## @@ -5313,10 +5313,10 @@ def daylit?(space = nil, sidelit = true, toplit = true, baselit = true) return invalid("baselit" , mth, 4, DBG, false) unless ck4 walls = sidelit ? facets(space, "Outdoors", "Wall") : [] - roofs = toplit ? facets(space, "Outdoors", "RoofCeiling") : [] + rufs = toplit ? facets(space, "Outdoors", "RoofCeiling") : [] floors = baselit ? facets(space, "Outdoors", "Floor") : [] - (walls + roofs + floors).each do |surface| + (walls + rufs + floors).each do |surface| surface.subSurfaces.each do |sub| # All fenestrated subsurface types are considered, as user can set these # explicitly (e.g. skylight in a wall) in OpenStudio. @@ -5443,11 +5443,11 @@ def addSubs(s = nil, subs = [], clear = false, bound = false, realign = false, b box = boundedBox(s0) if realign - s00 = getRealignedFace(box, true) + s00 = realignedFace(box, true) return invalid("bound realignment", mth, 0, DBG, false) unless s00[:set] end elsif realign - s00 = getRealignedFace(s0, false) + s00 = realignedFace(s0, false) return invalid("unbound realignment", mth, 0, DBG, false) unless s00[:set] end @@ -6085,24 +6085,24 @@ def grossRoofArea(spaces = []) # (Array of 2x linked surfaces). Each surface may be linked to more than one # horizontal ridge. # - # @param roofs [Array] target surfaces + # @param rfs [Array] target surfaces # # @return [Array] horizontal ridges (see logs if empty) - def getHorizontalRidges(roofs = []) + def horizontalRidges(rfs = []) mth = "OSut::#{__callee__}" ridges = [] - return ridges unless roofs.is_a?(Array) + return ridges unless rfs.is_a?(Array) - roofs = roofs.select { |s| s.is_a?(OpenStudio::Model::Surface) } - roofs = roofs.select { |s| sloped?(s) } + rfs = rfs.select { |s| s.is_a?(OpenStudio::Model::Surface) } + rfs = rfs.select { |s| sloped?(s) } - roofs.each do |roof| - maxZ = roof.vertices.max_by(&:z).z - next if roof.space.empty? + rfs.each do |rf| + maxZ = rf.vertices.max_by(&:z).z + next if rf.space.empty? - space = roof.space.get + space = rf.space.get - getSegments(roof).each do |edge| + segments(rf).each do |edge| next unless xyz?(edge, :z, maxZ) # Skip if already tracked. @@ -6117,18 +6117,18 @@ def getHorizontalRidges(roofs = []) next if match - ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [roof] } + ridge = { edge: edge, length: (edge[1] - edge[0]).length, roofs: [rf] } # Links another roof (same space)? match = false - roofs.each do |ruf| + rfs.each do |ruf| break if match - next if ruf == roof + next if ruf == rf next if ruf.space.empty? next unless ruf.space.get == space - getSegments(ruf).each do |edg| + segments(ruf).each do |edg| break if match next unless same?(edge, edg) || same?(edge, edg.reverse) @@ -6195,7 +6195,7 @@ def toToplit(spaces = [], opts = {}) spaces = spaces.select { |sp| sp.partofTotalFloorArea } spaces = spaces.reject { |sp| unconditioned?(sp) } spaces = spaces.reject { |sp| vestibule?(sp) } - spaces = spaces.reject { |sp| getRoofs(sp).empty? } + spaces = spaces.reject { |sp| roofs(sp).empty? } spaces = spaces.reject { |sp| sp.floorArea < 4 * w2 } spaces = spaces.sort_by(&:floorArea).reverse return empty("spaces", mth, WRN, []) if spaces.empty? @@ -6241,7 +6241,7 @@ def toToplit(spaces = [], opts = {}) # Gather roof surfaces - possibly those of attics or plenums above. spaces.each do |sp| - getRoofs(sp).each do |rf| + roofs(sp).each do |rf| espaces[sp] = {roofs: []} unless espaces.key?(sp) espaces[sp][:roofs] << rf unless espaces[sp][:roofs].include?(rf) end @@ -6493,7 +6493,7 @@ def addSkyLights(spaces = [], opts = {}) end # Purge if requested. - getRoofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear + roofs(spaces).each { |s| s.subSurfaces.map(&:remove) } if clear # Safely exit, e.g. if strictly called to purge existing roof subsurfaces. return 0 if area && area.round(2) == 0 @@ -6771,7 +6771,7 @@ def addSkyLights(spaces = [], opts = {}) # Process outdoor-facing roof surfaces of plenums and attics above. rooms.each do |space, room| t0 = room[:t0] - rufs = getRoofs(space) - room[:roofs] + rufs = roofs(space) - room[:roofs] rufs.each do |ruf| next unless roof?(ruf) @@ -6933,12 +6933,12 @@ def addSkyLights(spaces = [], opts = {}) # Ensure uniqueness of plenum roofs. attics.values.each do |attic| attic[:roofs ].uniq! - attic[:ridges] = getHorizontalRidges(attic[:roofs]) # @todo + attic[:ridges] = horizontalRidges(attic[:roofs]) # @todo end plenums.values.each do |plenum| plenum[:roofs ].uniq! - plenum[:ridges] = getHorizontalRidges(plenum[:roofs]) # @todo + plenum[:ridges] = horizontalRidges(plenum[:roofs]) # @todo end # Regardless of the selected skylight arrangement pattern, the solution only @@ -7747,8 +7747,8 @@ def addSkyLights(spaces = [], opts = {}) # Generate well walls. vX = cast(roof, tile, ray) - s0 = getSegments(t0 * roof.vertices) - sX = getSegments(t0 * vX) + s0 = segments(t0 * roof.vertices) + sX = segments(t0 * vX) s0.each_with_index do |sg, j| sg0 = sg diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index cd08e64..dbf7055 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -1914,7 +1914,7 @@ module M pts = r * a expect(mod1.same?(pts, vtx)).to be true - output1 = mod1.getRealignedFace(vtx) + output1 = mod1.realignedFace(vtx) expect(mod1.status).to be_zero expect(output1).to be_a Hash expect(output1).to have_key(:set) @@ -1928,7 +1928,7 @@ module M ubbox1 = output1[:bbox] # Realign a previously realigned surface? - output2 = mod1.getRealignedFace(output1[:box]) + output2 = mod1.realignedFace(output1[:box]) ubox2 = output1[ :box] ubbox2 = output1[:bbox] @@ -1957,12 +1957,12 @@ module M vtx << OpenStudio::Point3d.new( 5, 2, 0) vtx << OpenStudio::Point3d.new( 6, 4, 0) - output3 = mod1.getRealignedFace(vtx) + output3 = mod1.realignedFace(vtx) ubox3 = output3[ :box] ubbox3 = output3[:bbox] # Realign a previously realigned surface? - output4 = mod1.getRealignedFace(output3[:box]) + output4 = mod1.realignedFace(output3[:box]) ubox4 = output4[ :box] ubbox4 = output4[:bbox] @@ -1993,12 +1993,12 @@ module M vtx << OpenStudio::Point3d.new( 1, 4, 0) vtx << OpenStudio::Point3d.new( 2, 2, 0) - output5 = mod1.getRealignedFace(vtx) + output5 = mod1.realignedFace(vtx) ubox5 = output5[ :box] ubbox5 = output5[:bbox] # Realign a previously realigned surface? - output6 = mod1.getRealignedFace(output5[:box]) + output6 = mod1.realignedFace(output5[:box]) ubox6 = output6[ :box] ubbox6 = output6[:bbox] @@ -2033,12 +2033,12 @@ module M vtx << OpenStudio::Point3d.new( 2, 6, 0) vtx << OpenStudio::Point3d.new( 1, 4, 0) - output7 = mod1.getRealignedFace(vtx) + output7 = mod1.realignedFace(vtx) ubox7 = output7[ :box] ubbox7 = output7[:bbox] # Realign a previously realigned surface? - output8 = mod1.getRealignedFace(ubox7) + output8 = mod1.realignedFace(ubox7) ubox8 = output8[ :box] ubbox8 = output8[:bbox] @@ -2073,11 +2073,11 @@ module M vtx << OpenStudio::Point3d.new( 6, 2, 0) vtx << OpenStudio::Point3d.new( 6, 4, 0) - output9 = mod1.getRealignedFace(vtx) + output9 = mod1.realignedFace(vtx) ubox9 = output9[ :box] ubbox9 = output9[:bbox] - output10 = mod1.getRealignedFace(vtx, true) # no impact + output10 = mod1.realignedFace(vtx, true) # no impact ubox10 = output10[ :box] ubbox10 = output10[:bbox] expect(mod1.same?(ubox9, ubox10)).to be true @@ -2090,11 +2090,11 @@ module M vtx << OpenStudio::Point3d.new( 4, 2, 0) vtx << OpenStudio::Point3d.new( 4, 6, 0) - output11 = mod1.getRealignedFace(vtx) + output11 = mod1.realignedFace(vtx) ubox11 = output11[ :box] ubbox11 = output11[:bbox] - output12 = mod1.getRealignedFace(vtx, true) # narrow, now wide + output12 = mod1.realignedFace(vtx, true) # narrow, now wide ubox12 = output12[ :box] ubbox12 = output12[:bbox] expect(mod1.same?(ubox11, ubox12)).to be false @@ -2110,14 +2110,14 @@ module M vtx << OpenStudio::Point3d.new( 3, 8, 0) vtx << OpenStudio::Point3d.new( 1, 4, 0) - output13 = mod1.getRealignedFace(vtx) + output13 = mod1.realignedFace(vtx) uset13 = output13[ :set] ubox13 = output13[ :box] ubbox13 = output13[:bbox] # Pre-isolate bounded box (preferable with irregular surfaces). box = mod1.boundedBox(vtx) - output14 = mod1.getRealignedFace(box) + output14 = mod1.realignedFace(box) uset14 = output14[ :set] ubox14 = output14[ :box] ubbox14 = output14[:bbox] @@ -2226,20 +2226,20 @@ module M sg2 << OpenStudio::Point3d.new(12, 14, 0) sg2 << OpenStudio::Point3d.new(12, 6, 0) - expect(mod1.getLineIntersection(sg1, sg2)).to be_nil + expect(mod1.lineIntersection(sg1, sg2)).to be_nil sg1 = OpenStudio::Point3dVector.new sg1 << OpenStudio::Point3d.new(0.60,19.06, 0) sg1 << OpenStudio::Point3d.new(0.60, 0.60, 0) sg1 << OpenStudio::Point3d.new(0.00, 0.00, 0) sg1 << OpenStudio::Point3d.new(0.00,19.66, 0) - sgs1 = mod1.getSegments(sg1) + sgs1 = mod1.segments(sg1) sg2 = OpenStudio::Point3dVector.new sg2 << OpenStudio::Point3d.new(9.83, 9.83, 0) sg2 << OpenStudio::Point3d.new(0.00, 0.00, 0) sg2 << OpenStudio::Point3d.new(0.00,19.66, 0) - sgs2 = mod1.getSegments(sg2) + sgs2 = mod1.segments(sg2) expect(mod1.same?(sg1[2], sg2[1])).to be true expect(mod1.same?(sg1[3], sg2[2])).to be true @@ -2392,13 +2392,13 @@ module M south = south.get # Side test: triad, medial and bounded boxes. - pts = mod1.getNonCollinears(ceiling.vertices, 3) + pts = mod1.nonCollinears(ceiling.vertices, 3) box01 = mod1.triadBox(pts) box11 = mod1.boundedBox(ceiling) expect(mod1.same?(box01, box11)).to be true expect(mod1.fits?(box01, ceiling)).to be true - pts = mod1.getNonCollinears(roof.vertices, 3) + pts = mod1.nonCollinears(roof.vertices, 3) box02 = mod1.medialBox(pts) box12 = mod1.boundedBox(roof) expect(mod1.same?(box02, box12)).to be true @@ -2841,19 +2841,19 @@ module M expect(pt).to be_a(OpenStudio::Point3d) expect(pt).to eq(p0) - # Stress test 'getSegments'. Invalid case. - sgs = mod1.getSegments(p3) + # Stress test 'segments'. Invalid case. + sgs = mod1.segments(p3) expect(sgs).to be_a(OpenStudio::Point3dVectorVector) expect(sgs).to be_empty expect(mod1.status).to eq(0) # nothing logged - sgs = mod1.getSegments([p3, p3]) + sgs = mod1.segments([p3, p3]) expect(sgs).to be_a(OpenStudio::Point3dVectorVector) expect(sgs).to be_empty expect(mod1.status).to eq(0) # nothing logged # Valid case. - sgs = mod1.getSegments([p0, p1, p2, p3]) + sgs = mod1.segments([p0, p1, p2, p3]) expect(sgs).to be_a(OpenStudio::Point3dVectorVector) expect(sgs.size).to eq(4) expect(sgs).to respond_to(:first) @@ -2861,10 +2861,10 @@ module M expect(sgs[-1]).to be_an(Array) # not an OpenStudio::Point3dVector # Stress test 'uniques'. - m0 = "'n points' String? expecting Integer (OSut::getUniques)" + m0 = "'n points' String? expecting Integer (OSut::uniques)" # Invalid number case - simply returns entire list of unique points. - uniks = mod1.getUniques([p0, p1, p2, p3], "osut") + uniks = mod1.uniques([p0, p1, p2, p3], "osut") expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(4) expect(mod1.debug?).to be true @@ -2873,95 +2873,95 @@ module M expect(mod1.clean!).to eq(DBG) # Valid, basic case. - uniks = mod1.getUniques([p0, p1, p2, p3]) + uniks = mod1.uniques([p0, p1, p2, p3]) expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(4) - uniks = mod1.getUniques([p0, p1, p2, p3], 0) + uniks = mod1.uniques([p0, p1, p2, p3], 0) expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(4) # Valid, first 3 points. - uniks = mod1.getUniques([p0, p1, p2, p3], 3) + uniks = mod1.uniques([p0, p1, p2, p3], 3) expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(3) # Valid, last 3 points. - uniks = mod1.getUniques([p0, p1, p2, p3], -3) + uniks = mod1.uniques([p0, p1, p2, p3], -3) expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(3) # Valid, n = 5: returns original 4 uniques points. - uniks = mod1.getUniques([p0, p1, p2, p3], 5) + uniks = mod1.uniques([p0, p1, p2, p3], 5) expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(4) # Valid, n = -5: returns original 4 uniques points. - uniks = mod1.getUniques([p0, p1, p2, p3], -5) + uniks = mod1.uniques([p0, p1, p2, p3], -5) expect(uniks).to be_a(OpenStudio::Point3dVector) expect(uniks.size).to eq(4) # Stress tests collinears. - m0 = "'n points' String? expecting Integer (OSut::getCollinears)" + m0 = "'n points' String? expecting Integer (OSut::collinears)" # Invalid case - raise DEBUG message, yet returns valid collinears. - collinears = mod1.getCollinears([p0, p1, p3, p8], "osut") - expect(collinears).to be_a(OpenStudio::Point3dVector) - expect(collinears.size).to eq(1) - expect(collinears[0]).to eq(p0) + colls = mod1.collinears([p0, p1, p3, p8], "osut") + expect(colls).to be_a(OpenStudio::Point3dVector) + expect(colls.size).to eq(1) + expect(colls[0]).to eq(p0) expect(mod1.debug?).to be true expect(mod1.logs.size).to eq(1) expect(mod1.logs.first[:message]).to eq(m0) expect(mod1.clean!).to eq(DBG) # Valid, basic case - collinears = mod1.getCollinears([p0, p1, p3, p8]) - expect(collinears.size).to eq(1) - expect(collinears[0]).to eq(p0) # same object ID - expect(mod1.same?(collinears.first, p0)).to be true # more expensive way - - collinears = mod1.getCollinears([p0, p1, p3, p8], 0) - expect(collinears.size).to eq(1) - expect(collinears[0]).to eq(p0) - - collinears = mod1.getCollinears([p0, p1, p2, p3, p8]) - expect(collinears.size).to eq(2) - expect(collinears[ 0]).to eq(p0) - expect(collinears[-1]).to eq(p1) + colls = mod1.collinears([p0, p1, p3, p8]) + expect(colls.size).to eq(1) + expect(colls[0]).to eq(p0) # same object ID + expect(mod1.same?(colls.first, p0)).to be true # more expensive way + + colls = mod1.collinears([p0, p1, p3, p8], 0) + expect(colls.size).to eq(1) + expect(colls[0]).to eq(p0) + + colls = mod1.collinears([p0, p1, p2, p3, p8]) + expect(colls.size).to eq(2) + expect(colls[ 0]).to eq(p0) + expect(colls[-1]).to eq(p1) expect(mod1.pointAlongSegment?(p0, sgs.first)) # sg is an Array (size = 2) # Only 2 collinears, so request for first 3 is ignored. - collinears = mod1.getCollinears([p0, p1, p2, p3, p8], 3) - expect(collinears.size).to eq(2) - expect(mod1.same?(collinears[0], p0)).to be true - expect(mod1.same?(collinears[1], p1)).to be true + colls = mod1.collinears([p0, p1, p2, p3, p8], 3) + expect(colls.size).to eq(2) + expect(mod1.same?(colls[0], p0)).to be true + expect(mod1.same?(colls[1], p1)).to be true # First collinear (out of 2). - collinears = mod1.getCollinears([p0, p1, p2, p3, p8], 1) + collinears = mod1.collinears([p0, p1, p2, p3, p8], 1) expect(collinears.size).to eq(1) expect(mod1.same?(collinears[0], p0)).to be true # Last collinear (out of 2). - collinears = mod1.getCollinears([p0, p1, p2, p3, p8], -1) - expect(collinears.size).to eq(1) - expect(mod1.same?(collinears[0], p1)).to be true + colls = mod1.collinears([p0, p1, p2, p3, p8], -1) + expect(colls.size).to eq(1) + expect(mod1.same?(colls[0], p1)).to be true # First two vs last two: same result. - collinears = mod1.getCollinears([p0, p1, p2, p3, p8], -2) - expect(collinears.size).to eq(2) - expect(mod1.same?(collinears[0], p0)).to be true - expect(mod1.same?(collinears[1], p1)).to be true + colls = mod1.collinears([p0, p1, p2, p3, p8], -2) + expect(colls.size).to eq(2) + expect(mod1.same?(colls[0], p0)).to be true + expect(mod1.same?(colls[1], p1)).to be true # Ignore n request when n.abs > number of actual collinears. - collinears = mod1.getCollinears([p0, p1, p2, p3, p8], 6) - expect(collinears.size).to eq(2) - expect(mod1.same?(collinears[0], p0)).to be true - expect(mod1.same?(collinears[1], p1)).to be true + colls = mod1.collinears([p0, p1, p2, p3, p8], 6) + expect(colls.size).to eq(2) + expect(mod1.same?(colls[0], p0)).to be true + expect(mod1.same?(colls[1], p1)).to be true - collinears = mod1.getCollinears([p0, p1, p2, p3, p8], -6) - expect(collinears.size).to eq(2) - expect(mod1.same?(collinears[0], p0)).to be true - expect(mod1.same?(collinears[1], p1)).to be true + colls = mod1.collinears([p0, p1, p2, p3, p8], -6) + expect(colls.size).to eq(2) + expect(mod1.same?(colls[0], p0)).to be true + expect(mod1.same?(colls[1], p1)).to be true # Stress test pointAlongSegment? m0 = "'points' String? expecting Array (OSut::to_p3Dv)" @@ -2981,41 +2981,41 @@ module M # CASE a1: 2x end-to-end line segments (returns matching endpoints). expect(mod1.lineIntersects?( [p0, p1], [p1, p2] )).to be true - pt = mod1.getLineIntersection( [p0, p1], [p1, p2] ) + pt = mod1.lineIntersection( [p0, p1], [p1, p2] ) expect(mod1.same?(pt, p1)).to be true # CASE a2: as a1, sequence of line segment endpoints doesn't matter. expect(mod1.lineIntersects?( [p1, p0], [p1, p2] )).to be true - pt = mod1.getLineIntersection( [p1, p0], [p1, p2] ) + pt = mod1.lineIntersection( [p1, p0], [p1, p2] ) expect(mod1.same?(pt, p1)).to be true # CASE b1: 2x right-angle line segments, with 1x matching at corner. expect(mod1.lineIntersects?( [p1, p2], [p1, p3] )).to be true - pt = mod1.getLineIntersection( [p1, p2], [p2, p3] ) + pt = mod1.lineIntersection( [p1, p2], [p2, p3] ) expect(mod1.same?(pt, p2)).to be true # CASE b2: as b1, sequence of segments doesn't matter. expect(mod1.lineIntersects?( [p2, p3], [p1, p2] )).to be true - pt = mod1.getLineIntersection( [p2, p3], [p1, p2] ) + pt = mod1.lineIntersection( [p2, p3], [p1, p2] ) expect(mod1.same?(pt, p2)).to be true # CASE c: 2x right-angle line segments, yet disconnected. expect(mod1.lineIntersects?( [p0, p1], [p2, p3] )).to be false - expect(mod1.getLineIntersection( [p0, p1], [p2, p3] )).to be_nil + expect(mod1.lineIntersection( [p0, p1], [p2, p3] )).to be_nil # CASE d: 2x connected line segments, acute angle. expect(mod1.lineIntersects?( [p0, p2], [p3, p0] )).to be true - pt = mod1.getLineIntersection( [p0, p2], [p3, p0] ) + pt = mod1.lineIntersection( [p0, p2], [p3, p0] ) expect(mod1.same?(pt, p0)).to be true # CASE e1: 2x disconnected line segments, right angle. expect(mod1.lineIntersects?( [p0, p2], [p4, p6] )).to be true - pt = mod1.getLineIntersection( [p0, p2], [p4, p6] ) + pt = mod1.lineIntersection( [p0, p2], [p4, p6] ) expect(mod1.same?(pt, p5)).to be true # CASE e2: as e1, sequence of line segment endpoints doesn't matter. expect(mod1.lineIntersects?( [p0, p2], [p6, p4] )).to be true - pt = mod1.getLineIntersection( [p0, p2], [p6, p4] ) + pt = mod1.lineIntersection( [p0, p2], [p6, p4] ) expect(mod1.same?(pt, p5)).to be true # Point ENTIRELY within (vs outside) a polygon. @@ -3065,13 +3065,13 @@ module M vtx << OpenStudio::Point3d.new( 0, 0, 10) vtx << OpenStudio::Point3d.new( 0, 0, 0) - segments = mod1.getSegments(vtx) - expect(segments).to be_a(OpenStudio::Point3dVectorVector) - expect(segments.size).to eq(3) + sgments = mod1.segments(vtx) + expect(sgments).to be_a(OpenStudio::Point3dVectorVector) + expect(sgments.size).to eq(3) - segments.each_with_index do |segment, i| - unless mod1.xyz?(segment, :x, segment[0].x) - vplane = mod1.verticalPlane(segment[0], segment[-1]) + sgments.each_with_index do |sgment, i| + unless mod1.xyz?(sgment, :x, sgment[0].x) + vplane = mod1.verticalPlane(sgment[0], sgment[-1]) expect(vplane).to be_a(OpenStudio::Plane) end end @@ -4629,7 +4629,7 @@ module M # "Story 1 West Perimeter Space" : "Surface 6" model.getSpaces.each do |space| - rufs = mod1.getRoofs(space) + rufs = mod1.roofs(space) expect(rufs.size).to eq(1) ruf = rufs.first expect(ruf).to be_a(OpenStudio::Model::Surface) @@ -4676,7 +4676,7 @@ module M model.getSpaces.each do |space| next unless space.partofTotalFloorArea - rufs = mod1.getRoofs(space) + rufs = mod1.roofs(space) expect(rufs.size).to eq(1) ruf = rufs.first expect(ruf).to be_a(OpenStudio::Model::Surface) @@ -4800,7 +4800,7 @@ module M ld2 = OpenStudio::Point3d.new( 8, 3, 0) sg1 = OpenStudio::Point3d.new(12, 14, 0) sg2 = OpenStudio::Point3d.new(12, 6, 0) - expect(mod1.getLineIntersection([sg1, sg2], [ld1, ld2])).to be_nil + expect(mod1.lineIntersection([sg1, sg2], [ld1, ld2])).to be_nil # To support multiple polygon inserts within a larger polygon, subset boxes # must be first 'aligned' (along a temporary XY plane) in a systematic way @@ -4997,7 +4997,7 @@ module M # TOTAL attic roof area, including overhangs. roofs = mod1.facets(attic, "Outdoors", "RoofCeiling") - rufs = mod1.getRoofs(model.getSpaces) + rufs = mod1.roofs(model.getSpaces) total1 = roofs.sum(&:grossArea) total2 = rufs.sum(&:grossArea) expect(total1.round(2)).to eq(total2.round(2)) @@ -5064,11 +5064,11 @@ module M expect((tot1 - net).round(2)).to eq(sky_area1.round(2)) # In absence of skylight wells (more importantly, in absence of leader lines - # anchoring skylight base surfaces), OSut's 'getRoofs' & 'grossRoofArea' + # anchoring skylight base surfaces), OSut's 'roofs' & 'grossRoofArea' # report not only on newly-added base surfaces (or their areas), but also # overalpping areas of attic roofs above. Unfortunately, these become # unreliable with newly-added skylight wells. - rfs2 = mod1.getRoofs(core) + rfs2 = mod1.roofs(core) tot2 = rfs2.sum(&:grossArea) expect(tot2.round(2)).to eq(tot1.round(2)) expect(tot2.round(2)).to eq(mod1.grossRoofArea(core).round(2)) @@ -5086,11 +5086,11 @@ module M # if a higher-level application, relying on 'addSkylights' (e.g. an # OpenStudio measure), stores its output for subsequent reporting purposes. - # Deeper dive: Why are OSut's 'getRoofs' and 'grossRoofArea' unreliable + # Deeper dive: Why are OSut's 'roofs' and 'grossRoofArea' unreliable # with leader lines? Both rely on OSut's 'overlaps?', itself relying on # OpenStudio's 'join' and 'intersect': if neither are successful in joining # (or intersecting) 2x polygons (e.g. attic roof vs cast core ceiling), - # there can be no identifiable overlap. In such cases, both 'getRoofs' and + # there can be no identifiable overlap. In such cases, both 'roofs' and # 'grossRoofArea' ignore overlapping attic roofs. A demo: roof_north = model.getSurfaceByName("Attic_roof_north") core_ceiling = model.getSurfaceByName("Core_ZN_ceiling") @@ -5119,11 +5119,11 @@ module M expect(OpenStudio.join(a_roof_north, c_core_ceiling, TOL2)).to be_empty expect(OpenStudio.intersect(a_roof_north, c_core_ceiling, TOL)).to be_empty - # A future revision of OSut's 'getRoofs' and 'grossRoofArea' would require: + # A future revision of OSut's 'roofs' and 'grossRoofArea' would require: # - a new method identifying leader lines amongts surface vertices # - a new method identifying surface cutouts amongst surface vertices # - a method to purge both leader lines and cutouts from surface vertices - # - have 'getRoofs' & 'grossRoofArea' rely on the remaining outer vertices + # - have 'roofs' & 'grossRoofArea' rely on the remaining outer vertices # ... @todo? # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # @@ -5348,8 +5348,8 @@ module M gra_bulk = mod1.grossRoofArea(bulk) gra_fine = mod1.grossRoofArea(fine) - bulk_roof_m2 = mod1.getRoofs(bulk).sum(&:grossArea) - fine_roof_m2 = mod1.getRoofs(fine).sum(&:grossArea) + bulk_roof_m2 = mod1.roofs(bulk).sum(&:grossArea) + fine_roof_m2 = mod1.roofs(fine).sum(&:grossArea) expect(gra_bulk.round(2)).to eq(bulk_roof_m2.round(2)) expect(gra_fine.round(2)).to eq(fine_roof_m2.round(2)) From c53fe281cc261ee34243b0b81aaaf004fa79002a Mon Sep 17 00:00:00 2001 From: brgix Date: Mon, 11 Aug 2025 16:35:06 -0400 Subject: [PATCH 10/15] Adds space width & height --- lib/osut/utils.rb | 66 ++++++++++++++++++++++++++++++++ spec/osut_tests_spec.rb | 84 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index de2a5b1..312a8a3 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -4555,6 +4555,72 @@ def alignedHeight(pts = nil, force = false) pts.max_by(&:y).y - pts.min_by(&:y).y end + ## + # Fetch a space's full height (in space coordinates). The solution considers + # all surface types ("Floor" vs "Wall" vs "RoofCeiling"). + # + # @param space [OpenStudio::Model::Space] a space + # + # @return [Float] full height of space (0 if invalid input) + def spaceHeight(space = nil) + return 0 unless space.is_a?(OpenStudio::Model::Space) + + minZ = 1000 + maxZ = 0 + + space.surfaces.each do |surface| + minZ = [surface.vertices.min_by(&:z).z, minZ].min + maxZ = [surface.vertices.max_by(&:z).z, maxZ].max + end + + maxZ < minZ ? 0 : maxZ - minZ + end + + ## + # Fetch a space's width, based on the geometry of space floors. + # + # @param space [OpenStudio::Model::Space] a space + # + # @return [Float] width of a space (0 if invalid input) + def spaceWidth(space = nil) + return 0 unless space.is_a?(OpenStudio::Model::Space) + + floors = facets(space, "all", "Floor") + return 0 if floors.empty? + + # Automatically determining a space's "width" is not straightforward: + # - a space may hold multiple floor surfaces at various Z-axis levels + # - a space may hold multiple floor surfaces, with unique "widths" + # - a floor surface may expand/contract (in "width") along its length. + # + # First, attempt to merge all floor surfaces together as 1x polygon: + # - select largest floor surface (in area) + # - determine its 3D plane + # - retain only other floor surfaces sharing same 3D plane + # - recover potential union between floor surfaces + # - fall back to largest floor surface if invalid union + floors = floors.sort_by(&:grossArea).reverse + floor = floors.first + plane = floor.plane + t = OpenStudio::Transformation.alignFace(floor.vertices) + polyg = poly(floor, false, true, true, t, :ulc).to_a.reverse + return 0 if polyg.empty? + + if floors.size > 1 + floors = floors.select { |flr| plane.equal(flr.plane, 0.001) } + + if floors.size > 1 + polygs = floors.map { |flr| poly(flr, false, true, true, t, :ulc) } + polygs = polygs.reject { |plg| plg.empty? } + polygs = polygs.map { |plg| plg.to_a.reverse } + union = OpenStudio.joinAll(polygs, 0.01).first + polyg = poly(union, false, true, true) + end + end + + height(boundedBox(polyg)) + end + ## # Identifies 'leader line anchors', i.e. specific 3D points of a (larger) set # (e.g. delineating a larger, parent polygon), each anchor linking the BLC diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index dbf7055..5d1b14f 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -3862,7 +3862,7 @@ module M expect(mod1.level).to eq(DBG) expect(mod1.clean!).to eq(DBG) - # Modified NREL SEB model' + # Modified NREL SEB model. file = File.join(__dir__, "files/osms/out/seb_ext2.osm") path = OpenStudio::Path.new(file) model = translator.loadModel(path) @@ -5724,4 +5724,86 @@ module M file = File.join(__dir__, "files/osms/out/seb_ext5.osm") model.save(file, true) end + + it "checks space height & width" do + translator = OpenStudio::OSVersion::VersionTranslator.new + expect(mod1.reset(DBG)).to eq(DBG) + expect(mod1.level).to eq(DBG) + expect(mod1.clean!).to eq(DBG) + + file = File.join(__dir__, "files/osms/in/warehouse.osm") + path = OpenStudio::Path.new(file) + model = translator.loadModel(path) + expect(model).to_not be_empty + model = model.get + + fine = model.getSpaceByName("Zone2 Fine Storage") + expect(fine).to_not be_empty + fine = fine.get + + # The Fine Storage space has 2 floors, at different Z-axis levels: + # - main ground floor (slab on grade), Z=0.00m + # - a mezzanine floor, adjacent to the office space ceiling below, Z=4.27m + expect(mod1.facets(fine, "all", "floor").size).to eq(2) + groundfloor = model.getSurfaceByName("Fine Storage Floor") + mezzanine = model.getSurfaceByName("Office Roof Reversed") + expect(groundfloor).to_not be_empty + expect(mezzanine).to_not be_empty + groundfloor = groundfloor.get + mezzanine = mezzanine.get + + # The ground floor is L-shaped, floor surfaces have differenet Z=axis + # levels, etc. In the context of codes/standards like ASHRAE 90.1 or the + # Canadian NECB, determining what constitutes a space's 'height' and/or + # 'width' can matter, namely with regards to geometry-based LPD rules (e.g. + # adjustments per corridor 'width'). Not stating here what the definitive + # answers should be in all cases. There are however a few OSut functions + # that may be useful. + # + # OSut's 'aligned' height and width functions were initially developed for + # non-flat surfaces, like walls and sloped roofs - particularly useful when + # such surfaces are rotated in 3D space. It's somewhat less intuitive when + # applied to horizontal surfaces like floors. In a nutshell, the functions + # lay out the surface in a 2D grid, aligning it along its 'bounded box'. It + # then determines a bounding box around the surface, once aligned: + # - 'aligned height' designates the narrowest edge of the bounding box + # - 'aligned width' designates the widest edge of the bounding box + # + # Useful? In some circumstances, maybe. One can argue that these may be of + # limited use for width-based LPD adjustment calculations. + expect(mod1.alignedHeight(groundfloor)).to be_within(TOL).of(30.48) + expect(mod1.alignedWidth(groundfloor)).to be_within(TOL).of(45.72) + expect(mod1.alignedHeight(mezzanine)).to be_within(TOL).of(9.14) + expect(mod1.alignedWidth(mezzanine)).to be_within(TOL).of(25.91) + + # OSut's 'spaceHeight' and 'spaceWidth' are more suitable for height- or + # width-based LPD adjustement calculations. OSut sets a space's width as + # the widest edge of the largest bounded box it can fit within a collection + # of neighbouring floor surfaces. This is considered reasonable for a long + # corridor, with a varying width along its full length. Achtung! The + # function can be time consuming for very convoluted spaces (e.g. long + # corridors with multiple concavities). + expect(mod1.spaceHeight(fine)).to be_within(TOL).of(8.53) + expect(mod1.spaceWidth(fine)).to be_within(TOL).of(21.33) + expect(mod1.status).to eq(0) + + file = File.join(__dir__, "files/osms/out/seb_sky.osm") + path = OpenStudio::Path.new(file) + model = translator.loadModel(path) + expect(model).to_not be_empty + model = model.get + + openarea = model.getSpaceByName("Open area 1") + expect(openarea).to_not be_empty + openarea = openarea.get + + floor = mod1.facets(openarea, "all", "floor") + expect(floor.size).to eq(1) + floor = floor.first + + expect(mod1.alignedHeight(floor)).to be_within(TOL).of(6.88) + expect(mod1.alignedWidth(floor)).to be_within(TOL).of(8.22) + expect(mod1.spaceHeight(openarea)).to be_within(TOL).of(3.96) + expect(mod1.spaceWidth(openarea)).to be_within(TOL).of(7.90) + end end From a1a86120f9c953f1d6575c11be19f72eca1a880d Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 12 Aug 2025 06:39:02 -0400 Subject: [PATCH 11/15] Adds 'realignFace' to 'spaceWidth' --- lib/osut/utils.rb | 6 +++++- spec/osut_tests_spec.rb | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 312a8a3..bc9da8c 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -4615,10 +4615,14 @@ def spaceWidth(space = nil) polygs = polygs.map { |plg| plg.to_a.reverse } union = OpenStudio.joinAll(polygs, 0.01).first polyg = poly(union, false, true, true) + return 0 if polyg.empty? end end - height(boundedBox(polyg)) + res = realignedFace(polyg.to_a.reverse) + return 0 if res[:box].nil? + + height(res[:box]) end ## diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 5d1b14f..12d7687 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -5785,8 +5785,8 @@ module M # corridors with multiple concavities). expect(mod1.spaceHeight(fine)).to be_within(TOL).of(8.53) expect(mod1.spaceWidth(fine)).to be_within(TOL).of(21.33) - expect(mod1.status).to eq(0) + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # file = File.join(__dir__, "files/osms/out/seb_sky.osm") path = OpenStudio::Path.new(file) model = translator.loadModel(path) @@ -5804,6 +5804,8 @@ module M expect(mod1.alignedHeight(floor)).to be_within(TOL).of(6.88) expect(mod1.alignedWidth(floor)).to be_within(TOL).of(8.22) expect(mod1.spaceHeight(openarea)).to be_within(TOL).of(3.96) - expect(mod1.spaceWidth(openarea)).to be_within(TOL).of(7.90) + expect(mod1.spaceWidth(openarea)).to be_within(TOL).of(3.77) + + expect(mod1.status).to eq(0) end end From 699d8aa3c6e95a17153fc17f688fe3c267c8cdbc Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 12 Aug 2025 08:18:09 -0400 Subject: [PATCH 12/15] Tweaks space width/height methods/tests --- lib/osut/utils.rb | 6 ++++-- spec/osut_tests_spec.rb | 22 ++++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index bc9da8c..35cb90c 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -4565,8 +4565,8 @@ def alignedHeight(pts = nil, force = false) def spaceHeight(space = nil) return 0 unless space.is_a?(OpenStudio::Model::Space) - minZ = 1000 - maxZ = 0 + minZ = 10000 + maxZ = -10000 space.surfaces.each do |surface| minZ = [surface.vertices.min_by(&:z).z, minZ].min @@ -4599,6 +4599,7 @@ def spaceWidth(space = nil) # - retain only other floor surfaces sharing same 3D plane # - recover potential union between floor surfaces # - fall back to largest floor surface if invalid union + # - return width of largest bounded box floors = floors.sort_by(&:grossArea).reverse floor = floors.first plane = floor.plane @@ -4622,6 +4623,7 @@ def spaceWidth(space = nil) res = realignedFace(polyg.to_a.reverse) return 0 if res[:box].nil? + # A bounded box's 'height', at its narrowest, is its 'width'. height(res[:box]) end diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 12d7687..b8cf6e9 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -5743,7 +5743,7 @@ module M # The Fine Storage space has 2 floors, at different Z-axis levels: # - main ground floor (slab on grade), Z=0.00m - # - a mezzanine floor, adjacent to the office space ceiling below, Z=4.27m + # - mezzanine floor, adjacent to the office space ceiling below, Z=4.27m expect(mod1.facets(fine, "all", "floor").size).to eq(2) groundfloor = model.getSurfaceByName("Fine Storage Floor") mezzanine = model.getSurfaceByName("Office Roof Reversed") @@ -5755,10 +5755,10 @@ module M # The ground floor is L-shaped, floor surfaces have differenet Z=axis # levels, etc. In the context of codes/standards like ASHRAE 90.1 or the # Canadian NECB, determining what constitutes a space's 'height' and/or - # 'width' can matter, namely with regards to geometry-based LPD rules (e.g. - # adjustments per corridor 'width'). Not stating here what the definitive - # answers should be in all cases. There are however a few OSut functions - # that may be useful. + # 'width' matters, namely with regards to geometry-based LPD rules (e.g. + # adjustments based on corridor 'width'). Not stating here what the + # definitive answers should be in all cases. There are however a few OSut + # functions that may be helpful. # # OSut's 'aligned' height and width functions were initially developed for # non-flat surfaces, like walls and sloped roofs - particularly useful when @@ -5778,11 +5778,13 @@ module M # OSut's 'spaceHeight' and 'spaceWidth' are more suitable for height- or # width-based LPD adjustement calculations. OSut sets a space's width as - # the widest edge of the largest bounded box it can fit within a collection - # of neighbouring floor surfaces. This is considered reasonable for a long - # corridor, with a varying width along its full length. Achtung! The - # function can be time consuming for very convoluted spaces (e.g. long - # corridors with multiple concavities). + # the length of the narrowest edge of the largest bounded box that fits + # within a collection of neighbouring floor surfaces. This is considered + # reasonable for a long corridor, with varying widths along its full + # length (e.g. occasional alcoves). + # + # Achtung! The function can be time consuming (multiple iterations) for + # very convoluted spaces (e.g. long corridors with multiple concavities). expect(mod1.spaceHeight(fine)).to be_within(TOL).of(8.53) expect(mod1.spaceWidth(fine)).to be_within(TOL).of(21.33) From b462885a609003106869258d2b70671118a93501 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 12 Aug 2025 11:46:27 -0400 Subject: [PATCH 13/15] Tests candidate OSlg v040 --- Gemfile | 2 ++ osut.gemspec | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index b4e2a20..65a417d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ source "https://rubygems.org" +gem "oslg", git: "https://github.com/rd2/oslg", branch: "v033" + gemspec diff --git a/osut.gemspec b/osut.gemspec index d4799aa..1aac70e 100644 --- a/osut.gemspec +++ b/osut.gemspec @@ -26,7 +26,7 @@ Gem::Specification.new do |s| s.required_ruby_version = [">= 2.5.0", "< 4"] s.metadata = {} - s.add_dependency "oslg", ">= 0.3.0" + s.add_development_dependency "oslg", ">= 0.4.0" s.add_development_dependency "bundler", "~> 2.1" s.add_development_dependency "rake", "~> 13.0" s.add_development_dependency "rspec", "~> 3.11" From ba28585969375c89eda0cdc1aec5066ff7b350df Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 12 Aug 2025 15:47:10 -0400 Subject: [PATCH 14/15] Bumps version to v070 --- lib/osut/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/osut/version.rb b/lib/osut/version.rb index 116357a..7b75bd4 100644 --- a/lib/osut/version.rb +++ b/lib/osut/version.rb @@ -29,5 +29,5 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. module OSut - VERSION = "0.6.1".freeze # OSut version + VERSION = "0.7.0".freeze end From f2d5b55b20c37625c6ab8a1132bed181939d6ef4 Mon Sep 17 00:00:00 2001 From: brgix Date: Wed, 13 Aug 2025 06:52:12 -0400 Subject: [PATCH 15/15] Pulls OSlg release v040 --- Gemfile | 2 -- osut.gemspec | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 65a417d..b4e2a20 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,3 @@ source "https://rubygems.org" -gem "oslg", git: "https://github.com/rd2/oslg", branch: "v033" - gemspec diff --git a/osut.gemspec b/osut.gemspec index 1aac70e..2db34fa 100644 --- a/osut.gemspec +++ b/osut.gemspec @@ -26,7 +26,7 @@ Gem::Specification.new do |s| s.required_ruby_version = [">= 2.5.0", "< 4"] s.metadata = {} - s.add_development_dependency "oslg", ">= 0.4.0" + s.add_dependency "oslg", ">= 0.4.0" s.add_development_dependency "bundler", "~> 2.1" s.add_development_dependency "rake", "~> 13.0" s.add_development_dependency "rspec", "~> 3.11"