I’ve been trying to build a chair distance algorithm that finds the optimal number/location of chairs from a list of clustered points.
The clustering portion of the script is fairly straightforward, but I’m struggling to finish off the final portion of the logic. I’m trying to do it in a single block of Python and I think I’ve almost got it.
For now I’m prototyping my Python based on one cluster of chairs, and then will expand it one level after most likely to handle multiple clusters.
The logic is as such;
Check if the cluster is 1 chair
if not
For each chair…
Collect all chairs in 1500 from that chair, remove from testing
Find nearest chair outside 1500, that is now the test chair
Repeat step 3-5 until there are no chairs to test
Collect each test set
Return the test with the most possible chairs for that cluster
I am aware there is a more complex workflow here but I was hoping to package this portion of the workflow to a custom node for others to access;
My code is below;
# Made by Gavin Crump
# Free for use
# BIM Guru, www.bimguru.com.au
# Boilerplate text
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
# Preparing input from dynamo to revit
cluster = IN[0]
radius = IN[1]
results = []
# Identify the seat count in the group
seat_count = len(cluster)
# Catch if there's only 1 chair
if seat_count == 1:
test_count = 1
results_c = cluster
else:
test_count = 0
results_c = []
# Exhaust all possibile starting chairs
while test_count < seat_count:
# First seat is in the result
test_seat = cluster[test_count]
# These are the chairs that are compliant
seats_in = []
seats_in.append(cluster[test_count])
# Collect remaining chairs, make a new list
other_ind = range(seat_count)
del other_ind[test_count]
# These are all chairs yet to be tested
seats_rest = []
for i_test in other_ind:
seats_rest.append(cluster[i_test])
# Uptick the test counter
test_count += 1
# Keep finding more chairs until they're tested
while len(seats_rest) > 0:
distances = []
# Check each remaining chair
for s in seats_rest:
# Get the distance and index of the chair
d_test = Geometry.DistanceTo(test_seat,s)
i_test = seats_rest.index(s)
# If it's in the test radius, remove it
if d_test < radius:
del seats_rest[i_test]
# If it's not, let its distance through
else:
distances.append(d_test)
# Catch all if there were no valid chairs
if len(distances) > 0:
# Find next closest chair
d_next = min(distances)
i_next = distances.index(d_next)
# Make it the new chair, move to the result
test_seat = seats_rest[i_next]
seats_in.append(seats_rest[i_next])
del seats_rest[i_next]
# Add the permutation to the cluster's results
results_c.append(seats_in)
# Check the number of chairs per permutation
lens = []
# Collect the largest permutation
for r in results_c:
lens.append(len(r))
final = results_c[lens.index(max(lens))]
# Preparing output to Dynamo
OUT = final
I think the problem is how I’m handling the iterable seat list for the testing permutations - I know there are some limits in whether iteration lists are locally mutable, wondering if there’s an additional step to implement this?
Any assistance in closing off the logic here would be great, hoping to make a video to share this if it works - too many proprietary tools doing this - hoping to break the silo before it forms.
Yes in this case it is testing an existing layout, similarly to the approach by the occupancy checker script. In this case I’ve collected the points clustered by whether they fall within the radius of one another (using a surface union/intersect and group by intersecting union).
I’m also looking into the logic used by the full script to see if I can unravel the failure point in my own approach, but they’ve taken a bit of an abstract (albeit impressive) one using custom classes.
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
def nextSpaceChair(group, elemA = None):
global margin
group = list(group)
if elemA is None:
elemA = group.pop(0)
return [elemA] + nextSpaceChair(group, elemA )
else:
locPt = elemA.Location.Point
lstPair = [[x, x.Location.Point.DistanceTo(locPt)] for x in group if x.Location.Point.DistanceTo(locPt) > margin]
lstPair.sort(key = lambda x : x[1])
if len(lstPair) > 0:
#get the nearest chair >= margin
nearElem = lstPair[0][0]
#get the rest
group = [x[0] for x in lstPair[1:] ]
return [nearElem] + nextSpaceChair(group, nearElem )
else:
return []
allGroup = list(UnwrapElement(IN[0]))
margin = IN[1] * 0.00328084 #simple convert to feet
OUT = [nextSpaceChair(goup) for goup in allGroup ]
A variant with a better result (permutation of the first element to be tested)
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
def nextSpaceChair(group = [], elemA = None, startIdx = 0):
global margin
group = list(group)
if elemA is None:
elemA = group.pop(startIdx)
return [elemA] + nextSpaceChair(group, elemA )
else:
locPt = elemA.Location.Point
lstPair = [[x, x.Location.Point.DistanceTo(locPt)] for x in group if x.Location.Point.DistanceTo(locPt) > margin]
lstPair.sort(key = lambda x : x[1])
if len(lstPair) > 0:
#get the nearest chair >= 1.50m
nearElem = lstPair[0][0]
#get the rest
group = [x[0] for x in lstPair[1:] ]
return [nearElem] + nextSpaceChair(group = group, elemA = nearElem )
else:
return []
allGroup = list(UnwrapElement(IN[0]))
margin = IN[1] * 0.00328084 #simple convert in feet
out = []
for g in allGroup:
# test with change first element, for each chair in group
testLst = [nextSpaceChair(group = g, elemA = None, startIdx = i) for i in range(len(g))]
maxlst = max(testLst, key = len)
out.append(maxlst)
OUT = out
@Vikram_Subbaiah I’m still learning how to structure imperative functions in Dynamo, so this is a very helpful approach for me to review, thanks for sharing it! I didn’t expect it would be possible in design script, so this is pretty awesome.
@c.poupin Thanks so much for sharing your approach - this is quite sophisticated vs my Python capabilities (I can probably unravel all but the lambda function based on my knowledge currently) but I’ll be able to learn a lot from what you’ve shared. I’ve compared it to my previous workflow and the output works perfectly.
The calling of a globally defined variable from within a function is a new one to me - quite a valuable little line of code though. The .pop() method is new to me also, but again very useful vs my rather clumsy list rebuilding from parallel indices.
Assuming you have no issues with it, I’d love to package this into a custom node for others to use in my package (Crumple) with credit to you for the code, and a link to the thread in the Python block.
Here the definition of the global variable inside the function can be removed (because we do not modify the latter), I have put it for ease of reading the code and to test a modification of the margin inside the function