Covid chair spacing iteration challenge

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;

  1. Check if the cluster is 1 chair

if not

  1. For each chair…

  2. Collect all chairs in 1500 from that chair, remove from testing

  3. Find nearest chair outside 1500, that is now the test chair

  4. Repeat step 3-5 until there are no chairs to test

  5. Collect each test set

  6. 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.

Chair testing.rvt (1.5 MB)
Cluster chairs by overlap.dyn (52.3 KB)

2 Likes

To confirm: the points which represent chairs are pre determined and locked in place, correct?

1 Like

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.

1 Like

@GavinCrump Here is an attempt with Design Script. Hope it helps.


distancing.dyn (10.9 KB)

def TooClose (elm:var[]..[],min)
{
	pnt1 = List.FirstItem(elm.GetLocation());
	elm2 = List.RestOfItems(elm);
	pnt2 = elm2.GetLocation();
	bln1 = pnt1.DistanceTo(pnt2) < min;
	elm3 = List.FilterByBoolMask(elm2,bln1)["out"];
	return elm3;
};

def Distancing (elm:var[]..[],min)
{
	return = [Imperative]
	{
		if (List.Count(elm)>1)
		{
			elm1 = [List.FirstItem(elm)];
			c = 1;
			while (List.Count(elm) > 1)
			{
				elm2 = TooClose (elm,min);
				elm1[c] = List.FirstItem(elm2);
				elm = elm2;
				c = c + 1;
			}
			return elm1;
		}
		else
		{
			return elm;
		}
	}
};
5 Likes

Hello,
a test with a Python recursive function and Revit API

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 ]
5 Likes

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
4 Likes

@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.

2 Likes

Hello Gavin

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

no problem :wink:

3 Likes

Awesome, new nodes in Crumple as of just now :slight_smile:

Now time to record the YouTube tutorial!

4 Likes