如何根据单位所在社区的理想程度重新排序单位?(在Processing中)

31
我需要帮助实现一个算法,该算法可以生成建筑平面图。我最近在阅读Kostas Terzidis教授的最新著作Permutation Design: Buildings, Texts and Contexts(2014)时偶然发现了这个算法。 背景 考虑一个被分成网格系统的场地(a)。另外,考虑一个要放置在场地限制范围内的空间列表(c),以及一个邻接矩阵来确定这些空间的放置条件和相邻关系(d)。

enter image description here

引用Terzidis教授的话:

“解决这个问题的一种方法是在网格中随机放置空格,直到所有空格都适合并且满足约束条件”

上图展示了这样一个问题和一个样本解决方案 (f)。

算法(在书中简要描述)

1/ “每个空格都与一个列表相关联,该列表包含根据期望邻域程度排序的所有其他空格。”

2/ “然后从列表中选择每个空格的每个单元,并将它们逐个随机放置在站点上,直到它们适合站点并且满足相邻条件。(如果失败,则重复此过程)”

九个随机生成的计划示例:

enter image description here

我应该补充一下,作者后来解释说这个算法不依赖于蛮力技术

问题

正如您所看到的,解释相对模糊,并且步骤2在编码方面相当不清晰。到目前为止,我只有“拼图的碎片”:

  • 一个“站点”(选定整数列表)
  • 邻接矩阵(嵌套列表)
  • “空间”(字典列表)

对于每个单元:

  • a function that returns its direct neighbors
  • a list of its desirable neighbors with their indices in sorted order
  • a fitness score based on its actual neighbors

    from random import shuffle
    
    n_col, n_row = 7, 5
    to_skip = [0, 1, 21, 22, 23, 24, 28, 29, 30, 31]
    site = [i for i in range(n_col * n_row) if i not in to_skip]
    fitness, grid = [[None if i in to_skip else [] for i in range(n_col * n_row)] for e in range(2)]
    
    n = 2
    k = (n_col * n_row) - len(to_skip)
    rsize = 50
    
    #Adjacency matrix
    adm = [[0, 6, 1, 5, 2],
           [6, 0, 1, 4, 0],
           [1, 1, 0, 8, 0],
           [5, 4, 8, 0, 3],
           [2, 0, 0, 3, 0]]
    
    
    spaces = {"office1": [0 for i in range(4)], 
              "office2": [1 for i in range(6)], 
              "office3": [2 for i in range(6)],
              "passage": [3 for i in range(7)],
              "entry": [4 for i in range(2)]}
    
    
    def setup():
        global grid
        size(600, 400, P2D)
        rectMode(CENTER)
        strokeWeight(1.4)
    
        #Shuffle the order for the random placing to come
        shuffle(site)
    
        #Place units randomly within the limits of the site
        i = -1   
        for space in spaces.items():
            for unit in space[1]:    
                i+=1
                grid[site[i]] = unit
    
    
        #For each unit of each space... 
        i = -1   
        for space in spaces.items():
            for unit in space[1]:    
                i+=1
    
                #Get the indices of the its DESIRABLE neighbors in sorted order
                ada = adm[unit]
                sorted_indices = sorted(range(len(ada)), key = ada.__getitem__)[::-1]
    
                #Select indices with positive weight (exluding 0-weight indices)
                pindices = [e for e in sorted_indices if ada[e] > 0] 
    
                #Stores its fitness score (sum of the weight of its REAL neighbors)
                fitness[site[i]] = sum([ada[n] for n in getNeighbors(i) if n in pindices])
    
        print 'Fitness Score:', fitness
    
    def draw():
        background(255)
    
        #Grid's background
        fill(170)
        noStroke()
        rect(width/2 - (rsize/2) , height/2 + rsize/2 + n_row , rsize*n_col, rsize*n_row)
    
    
        #Displaying site (grid cells of all selected units) + units placed randomly
        for i, e in enumerate(grid):
            if isinstance(e, list): pass
            elif e == None: pass
            else:
                fill(50 + (e * 50), 255 - (e * 80), 255 - (e * 50), 180)
                rect(width/2 - (rsize*n_col/2) + (i%n_col * rsize), height/2 + (rsize*n_row/2) + (n_row - ((k+len(to_skip))-(i+1))/n_col * rsize), rsize, rsize)
                fill(0)
                text(e+1, width/2 - (rsize*n_col/2) + (i%n_col * rsize), height/2 + (rsize*n_row/2) + (n_row - ((k+len(to_skip))-(i+1))/n_col * rsize))
    
    
    
    
    def getNeighbors(i):
        neighbors = []
    
        if site[i] > n_col and site[i] < len(grid) - n_col:
            if site[i]%n_col > 0 and site[i]%n_col < n_col - 1:
                if grid[site[i]-1] != None: neighbors.append(grid[site[i]-1])
                if grid[site[i]+1] != None: neighbors.append(grid[site[i]+1])
                if grid[site[i]-n_col] != None: neighbors.append(grid[site[i]-n_col]) 
                if grid[site[i]+n_col] != None: neighbors.append(grid[site[i]+n_col])
    
        if site[i] <= n_col:
            if site[i]%n_col > 0 and site[i]%n_col < n_col - 1:
                if grid[site[i]-1] != None: neighbors.append(grid[site[i]-1])
                if grid[site[i]+1] != None: neighbors.append(grid[site[i]+1])
                if grid[site[i]+n_col] != None: neighbors.append(grid[site[i]+n_col])
    
            if site[i]%n_col == 0:
                if grid[site[i]+1] != None: neighbors.append(grid[site[i]+1])
                if grid[site[i]+n_col] != None: neighbors.append(grid[site[i]+n_col])
    
            if site[i] == n_col-1:
                if grid[site[i]-1] != None: neighbors.append(grid[site[i]-1])
                if grid[site[i]+n_col] != None: neighbors.append(grid[site[i]+n_col])
    
        if site[i] >= len(grid) - n_col:
            if site[i]%n_col > 0 and site[i]%n_col < n_col - 1:
                if grid[site[i]-1] != None: neighbors.append(grid[site[i]-1])
                if grid[site[i]+1] != None: neighbors.append(grid[site[i]+1])
                if grid[site[i]-n_col] != None: neighbors.append(grid[site[i]-n_col])
    
            if site[i]%n_col == 0:
                if grid[site[i]+1] != None: neighbors.append(grid[site[i]+1])
                if grid[site[i]-n_col] != None: neighbors.append(grid[site[i]-n_col])
    
            if site[i]%n_col == n_col-1:
                if grid[site[i]-1] != None: neighbors.append(grid[site[i]-1])
                if grid[site[i]-n_col] != None: neighbors.append(grid[site[i]-n_col])
    
        if site[i]%n_col == 0:
            if site[i] > n_col and site[i] < len(grid) - n_col:
                if grid[site[i]+1] != None: neighbors.append(grid[site[i]+1])
                if grid[site[i]+n_col] != None: neighbors.append(grid[site[i]+n_col])
                if grid[site[i]-n_col] != None: neighbors.append(grid[site[i]-n_col])
    
        if site[i]%n_col == n_col - 1:
            if site[i] > n_col and site[i] < len(grid) - n_col:
                if grid[site[i]-1] != None: neighbors.append(grid[site[i]-1])
                if grid[site[i]+n_col] != None: neighbors.append(grid[site[i]+n_col])
                if grid[site[i]-n_col] != None: neighbors.append(grid[site[i]-n_col])
    
        return neighbors
    

enter image description here

我真的很希望有人能帮助连接各个点并解释给我听:
如何根据单位所在邻居的理想程度重新排序?
编辑
正如一些人注意到的那样,该算法基于某些空间(由单位组成)相邻的可能性。因此,对于每个随机放置在站点限制内的单位:
- 我们先检查它的直接邻居(上下左右) - 如果至少有2个邻居,则计算适应度分数。(=这2个或更多邻居的权重之和) - 最后,如果相邻概率高,就放置该单位。
大致而言,可以这样翻译:
    i = -1   
    for space in spaces.items():
        for unit in space[1]:    
            i+=1

            #Get the indices of the its DESIRABLE neighbors (from the adjacency matrix 'adm') in sorted order
            weights = adm[unit]
            sorted_indices = sorted(range(len(weights)), key = weights.__getitem__)[::-1]

            #Select indices with positive weight (exluding 0-weight indices)
            pindices = [e for e in sorted_indices if weights[e] > 0] 


            #If random grid cell is empty
            if not grid[site[i]]:

                #List of neighbors
                neighbors = [n for n in getNeighbors(i) if isinstance(n, int)]

                #If no neighbors -> place unit
                if len(neighbors) == 0:
                    grid[site[i]] = unit 

                #If at least 1 of the neighbors == unit: -> place unit (facilitate grouping)
                if len(neighbors) > 0 and unit in neighbors:
                    grid[site[i]] = unit  

                #If 2 or 3 neighbors, compute fitness score and place unit if probability is high
                if len(neighbors) >= 2 and len(neighbors) < 4:

                    fscore = sum([weights[n] for n in neighbors if n in pindices]) #cumulative weight of its ACTUAL neighbors
                    count = [1 for t in range(10) if random(sum(weights)) < fscore] #add 1 if fscore higher than a number taken at random between 0 and the cumulative weight of its DESIRABLE neighbors

                    if len(count) > 5: 
                        grid[site[i]] = unit

                #If 4 neighbors and high probability, 1 of them must belong to the same space
                if len(neighbors) > 3:

                    fscore = sum([weights[n] for n in neighbors if n in pindices]) #cumulative weight of its ACTUAL neighbors                    
                    count = [1 for t in range(10) if random(sum(weights)) < fscore] #add 1 if fscore higher than a number taken at random between 0 and the cumulative weight of its DESIRABLE neighbors

                    if len(count) > 5 and unit in neighbors:                       
                        grid[site[i]] = unit


            #if random grid cell not empty -> pass
            else: pass

考虑到很大一部分单位不会在第一次运行时放置(因为邻接概率低),我们需要一遍又一遍地迭代,直到找到一个随机分布,其中所有单位都可以适应。

enter image description here

经过数千次迭代,找到了一个适合的拟合,并满足所有相邻要求。

然而,请注意此算法产生了分离的组,而不是像提供的示例中那样非分割和均匀的堆栈。我还应该补充一点,近5000次迭代比Terzidis先生在他的书中提到的274次迭代多得多。

问题:

  • 我使用这种算法的方式有问题吗?
  • 如果没有,那么我缺少什么隐含条件?

矩阵(d)的确切含义是什么?例如,m [2] [4] = 4m [3] [4] = 8的含义是什么?值4和8代表什么意思? - Rabbid76
这是一个权重,值越高,两个空间相邻的概率就越大。在这里,“Office3”(3)和“Passage”(4)相邻的概率很高(8)。 - solub
我对解决这个问题很感兴趣,但我不知道你使用的 gui 库是什么。你能否把它包含在内,这样我才能运行代码? - David Culbreth
1
然后从列表中选择每个空间的每个单元,然后逐个随机放置在场地中,直到它们适合场地并满足相邻条件。满足什么条件?邻接矩阵看起来不像是相邻建筑物的计数,而更像是一种启发式方法(较高的值意味着更可能相邻)。那么,如何验证“满足相邻条件”并且给定的输出是正确的?我觉得这个问题陈述很模糊,一些更精确的定义会很有用。 - Matt Messersmith
@MattMessersmith 我完全同意,但这就是我所拥有的所有信息。您可以在第64页和65页上阅读原始描述:http://www.academia.edu/7776434/Digit_Mat_t_ers._Processes_of_Normalization_in_Architectural_Design - solub
显示剩余5条评论
2个回答

3
我提出的解决方案是在记录有效解决方案的同时多次重复算法。由于解决方案不唯一,我希望算法能够抛出多个解决方案。每个解决方案都将基于相邻关系亲和力得分。
我称一个完整运行尝试寻找有效植物分布为“尝试”。完整脚本运行将包括N个尝试。
每次尝试都以两个随机(均匀)选择开始:
- 网格中的起始点 - 起始办公室
确定点和办公室后,就会出现“扩展过程”,试图将所有办公区块放入网格中。
每个新块都根据其过程设置:
- 第1步: 计算每个与办公室相邻的单元格的亲和度。 - 第2步: 随机选择一个站点。选择应根据亲和度加权。
放置每个办公区块后,需要进行另一个均匀随机选择:下一个要放置的办公室。
一旦选择了,您应重新计算每个站点的亲和度,并随机选择(加权)新办公室的起始点。

0 亲和力的办公室不可添加。因此,该网格点的概率因子应为 0。亲和函数的选择是这个问题中有趣的部分。您可以尝试相邻单元格因子的加法甚至乘法。

扩展过程再次进行,直到放置完整个办公室的每个块。

所以基本上,选取办公室遵循均匀分布,之后针对所选办公室进行加权扩展过程。

什么时候会结束一次尝试?

  • 在网格中没有可以放置新办公室的点(所有点的亲和力都等于 0
  • 办公室无法扩展,因为所有亲和力权重都等于 0

如果出现以上情况,则该尝试无效,并且应该放弃并移动到一个全新的随机尝试。

否则,如果所有块都适合,则该尝试有效。

关键点在于办公室应该彼此粘在一起。这是算法的关键点,它随机尝试根据亲和力适配每个新办公室,但仍是随机过程。如果不满足条件(无效),则随机过程会重新开始选择一个新的网格点和办公室。

enter image description here

抱歉,这里只有算法而没有代码。
注意:我相信亲和计算过程可以改进,甚至您可以尝试一些不同的方法。这只是一个帮助您找到解决方案的想法。
希望它能帮到您。

在进一步调查之前,我想说我非常感谢您在回答中所付出的努力(清晰的解释和整洁的图形)。 - solub
我希望我有更多时间来实现您的建议并确保其可行性和正确性。尽管如此,我还是接受了这个答案,原因有三。首先,它在找到最适合Terzidis教授原始算法的解决方案方面最有帮助(我认为邻接矩阵应该保持不变),其次,它是最早和最整洁的答案。我已经联系了Terzidis教授以获取更多细节,如果他回复了,我会更新这个主题并分享他的答案。 - solub
@solub,请告诉我们Terzidis教授的答案是什么!!我觉得这个问题非常有趣。 - Cheche

0

我相信Kostas Terzidis教授会是一位出色的计算机理论研究员,但他的算法解释并没有什么帮助。

首先,邻接矩阵没有意义。在问题评论中,您说:

“该值越高,两个空间相邻的概率就越大”

但是m[i][i] = 0,这意味着同一个“办公室”的人更喜欢其他办公室作为邻居。这恰恰与您期望的相反,不是吗?我建议使用以下矩阵:

With 1 <= i, j <= 5:

              +----------------+
              | 10  6  1  5  2 |
              |    10  1  4  0 |
    m[i][j] = |       10  8  0 |  
              |          10  3 |
              |             10 |
              +----------------+

使用这个矩阵,

  • 最高值为10。因此m[i][i] = 10恰好表示您想要的:同一办公室的人应该在一起。
  • 最低值为0。(根本不应该有任何联系的人)

算法

步骤1:随机放置所有位置

(非常抱歉采用基于1的矩阵索引,但这是为了与邻接矩阵保持一致。)

With 1 <= x <= 5 and 1 <= y <= 7:

            +---------------------+
            | -  -  1  2  1  4  3 |
            | 1  2  4  5  1  4  3 |
  p[x][y] = | 2  4  2  4  3  2  4 |
            | -  -  -  -  3  2  4 |
            | -  -  -  -  5  3  3 |
            +---------------------+

步骤2:评分解决方案

对于所有位置p[x][y],使用邻接矩阵计算得分。例如,第一个位置1的邻居是24,因此得分为11:

score(p[1][3]) = m[1][2] + m[1][4] = 11

所有个人分数的总和将是解决方案得分

步骤3:通过交换位置来完善当前解决方案

对于每一对位置p[x1][y1], p[x2][y2],交换它们并重新评估解决方案,如果得分更高,则保留新解决方案。无论如何,重复步骤3,直到没有排列能够改善解决方案为止。

例如,如果您交换p[1][4]p[2][1]

            +---------------------+
            | -  -  1  1  1  4  3 |
            | 2  2  4  5  1  4  3 |
  p[x][y] = | 2  4  2  4  3  2  4 |
            | -  -  -  -  3  2  4 |
            | -  -  -  -  5  3  3 |
            +---------------------+

你会找到一个更好的得分解决方案:

交换前

score(p[1][3]) = m[1][2] + m[1][4] = 11
score(p[2][1]) = m[1][2] + m[1][2] = 12

交换后

score(p[1][3]) = m[1][1] + m[1][4] = 15
score(p[2][1]) = m[2][2] + m[2][2] = 20

所以保留它并继续交换位置。

一些注意事项

  • 请注意,算法始终会完成,因为在迭代的某个时刻,您将无法交换2个位置并获得更好的分数。
  • 在具有N处的矩阵中,有N x(N-1)种可能的交换方式,并且可以以高效的方式进行操作(因此不需要暴力搜索)。

希望对你有所帮助!


你错过了 ~所需的迭代次数或一些速度比较。 - PascalVKooten
@PascalVKooten 你说得对,我从来没有提到迭代的次数,但这也从未被问到过。我只是试图回答问题并帮助这个人。 - Cartucho
@Cartucho 根据您的算法,只有当两个单元从更好的解决方案得分中受益时才会发生交换。问题在于,有时一个单元的得分会较低(因为邻居较少),这将阻止尚未必要的交换 --> https://imgur.com/a/qgRyp6N - solub
@solub 不,如果整个矩阵的总分数提高了,交换应该总是进行的。我提出的是在交换之前和之后计算score(p1) + score(p2) + score(neighbors)。在你的例子中,score(p1) + score(p2) = 18+26 在交换之前,而在交换之后是 18+28(还应该计算p1p2的所有邻居的得分),因此算法会执行交换。 - Cartucho
@Cartucho,您的评论不太清楚是在交换之前和之后计算“解决方案得分”(整个网格的各个得分之和)还是计算score(p1) + score(p2) + score(neighbors)。我尝试了第一种选项(“解决方案得分”),即使总体得分有所提高,最终输出并不总是显示完全分离的空间。 - solub
概念验证:http://www.codeskulptor.org/#user45_UOQywVMEdV_1.py 您的想法在第53行至第70行实现。 - solub

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接