如何连接可变数量的换热器模型段?

How to connect variable number of segments of heat exchanger model?

我在 python 中为 ANSA 建模软件编写了一个代码,它会根据给定的参数创建一个热交换器模型。您可以在下面看到模型的示例。蓝色元素代表水,灰色元素代表管道,棕色元素代表空气。但是交换器中可以有任意数量的行和列。到目前为止,我已经了解了这一点。我有一些默认段,然后我根据给定参数设置它所需的大小,然后我复制该段以创建 x 行和 y 列。

但是现在我需要连接这些段,所以会有连续的管道流水(有一个输入和一个输出),如第二张图所示。你可以看到我手动创建的,但我需要能够通过脚本参数化地创建这些连接。

我完全不知道该怎么做。准确地说,我无法弄清楚代码的逻辑。创建元素的命令没有问题。由于可以有任意数量的行和列,问题不仅在于如何连接线段,还在于连接应引向什么方向以及输入和输出应位于何处。 如果需要,我会提供更多详细信息。到目前为止,我的代码很长并且对建模软件使用了特殊命令,所以我想这不会有太大帮助。但我会在需要时包括它。但再一次 - 我不需要特定的命令,只需要代码的一般逻辑。

这个问题等同于找一个Hamiltonian path from the "input" node to the "output" node in a graph that represents the two-dimensional grid (i.e. neighboring nodes are horizontally and vertically connected). This problem is NP-complete, see this article获取更多信息

网格中两个节点的每个连接都反转水流(“进入页面”或“离开页面”)的约束只是对网格中的节点总数施加了约束。奇数个连接将使从输入节点到输出节点的流反向,而偶数个连接将保持它。由于输入和输出管道都应位于设备的同一侧,因此必须反向流动,即必须进行奇数个连接。由于路径应该访问网格中的每个节点,这意味着网格必须由偶数个节点组成(连接数比节点数少一个)。因此,如果 nrows*ncols 是偶数,则此约束将自动满足,如果 nrows*nocls 是奇数,则无法满足。

如果网格的尺寸不是太大,可以尝试通过暴力搜索找到这样的哈密顿路径,例如使用 networkx library:

from typing import Tuple

import matplotlib.pyplot as plt
import networkx as nx
from networkx.algorithms.simple_paths import all_simple_paths
import numpy as np


def find_hamiltonian_paths(
    nrows: int,
    ncols: int,
    source: Tuple[int, int],
    target: Tuple[int, int],
):
    edges = []
    for r in range(nrows):
        for c in range(ncols):
            node = r*ncols + c
            if r > 0:
                edges.append((node, (r-1)*ncols + c))
            if r < nrows-1:
                edges.append((node, (r+1)*ncols + c))
            if c > 0:
                edges.append((node, r*ncols + c-1))
            if c < ncols-1:
                edges.append((node, r*ncols + c+1))
    G = nx.Graph(edges)
    for path in all_simple_paths(G, source[0]*ncols+source[1], target[0]*ncols+target[1]):
        if len(path) == nrows*ncols:
            yield path


# Example
nrows, ncols = 4, 6
source = 1, 2
target = 2, 4
path = next(find_hamiltonian_paths(nrows, ncols, source, target))

fig, ax = plt.subplots()
ax.set_xlim([-1, ncols])
ax.set_ylim([-1, nrows])
ax.set_xticks(np.arange(ncols))
ax.set_yticks(np.arange(nrows))
ax.set_yticklabels(np.arange(nrows)[::-1])
ax.grid(lw=0.3)
for pos in ('top', 'right', 'bottom', 'left'):
    ax.spines[pos].set_visible(False)
x, y = [], []
for node in path:
    r, c = divmod(node, ncols)
    x.append(c)
    y.append(nrows-1 - r)
ax.plot(x, y, '-')
ax.plot([x[0], x[-1]], [y[0], y[-1]], 'o', ms=10)
plt.show()

产生以下路径:

特例

1)偶数行左上角输入,左下角输出

(这也适用于任何其他角,只要输入和输出节点位于一条直线上并且被偶数行或列分隔即可。)

我们可以将 (0,0) 节点连接到 (1,0) 节点,方法是从左到右连接整个第一行,然后向下移动到第二行并将其从右到左连接。通过继续这种模式,我们可以连接任意数量的 N 行对(即总行数为 2*N 偶数)。

def special_case_1(
    nrows: int,
    ncols: int,
    source: Tuple[int, int],
    target: Tuple[int, int],
):
    assert source == (0, 0) and target == (nrows-1, 0)
    nodes = np.arange(nrows*ncols).reshape(nrows, ncols)
    for r in range(1, len(nodes), 2):
        nodes[r] = nodes[r, ::-1]
    yield nodes.ravel()


# Example
nrows, ncols = 6, 5
source = 0, 0
target = 5, 0
path = next(special_case_1(nrows, ncols, source, target))

2) 左上角输入偶数行,输入正下方输出

(与情况(1)类似,这推广到任何角落。)

从案例(1)我们知道如何完全连接一个(2*M, N)网格。现在情况(2)可以拆分为两个 sub-problems,即第一列和网格的其余部分(用 R 表示)。我们可以用与案例 (1) 类似的方式连接 R。所以我们需要做的就是将第一列连接到 R。将 (0,0) 处的输入节点连接到 (0,1) 处 R 的左上角是微不足道的。然后我们可以将R的左下角连接到第一列的底部节点,然后一直移动到输出节点(1,0)

def special_case_2(
    nrows: int,
    ncols: int,
    source: Tuple[int, int],
    target: Tuple[int, int],
):
    assert source == (0, 0) and target == (1, 0)
    nodes = np.arange(nrows*ncols).reshape(nrows, ncols)
    column = nodes[:, 0]
    nodes = nodes[:, 1:]
    for r in range(1, len(nodes), 2):
        nodes[r] = nodes[r, ::-1]
    yield [0, *nodes.ravel(), *column[:0:-1]]


# Example
nrows, ncols = 6, 5
source = 0, 0
target = 1, 0
path = next(special_case_2(nrows, ncols, source, target))

3) 偶数行数和偶数列,输入和输出相邻,位于网格边缘的中心

为了示例,我们假设它们位于顶部边缘的中心。我们可以将此问题拆分为左右两个 sub-problems,并以类似于案例 (1) 的方式将输入和输出都连接到底行的相应节点。那么,连接这两个问题就相当于简单的连接了这两个节点。

def special_case_3(
    nrows: int,
    ncols: int,
    source: Tuple[int, int],
    target: Tuple[int, int],
):
    assert source == (0, ncols//2-1) and target == (0, ncols//2)
    nodes = np.arange(nrows*ncols).reshape(nrows, ncols)
    left = nodes[:, :ncols//2]
    right = nodes[:, ncols//2:]
    for r in range(len(nodes)):
        if r % 2 == 0:
            left[r] = left[r, ::-1]
        else:
            right[r] = right[r, ::-1]
    yield [*left.ravel(), *right.ravel()[::-1]]


# Example
nrows, ncols = 6, 8
source = 0, 3
target = 0, 4
path = next(special_case_3(nrows, ncols, source, target))

4) 输入和输出在网格中心相邻

为此,我们只要求列数为偶数。通过查看草图和代码,尤其是下面的示例图,可以最好地解释该过程:

def special_case_4(
    nrows: int,
    ncols: int,
    source: Tuple[int, int],
    target: Tuple[int, int],
):
    r2, c2 = nrows//2, ncols//2
    assert source == (r2, c2-1) and target == (r2, c2)
    nodes = np.arange(nrows*ncols).reshape(nrows, ncols)
    top = nodes[:r2]
    for c in range(0, top.shape[1], 2):
        top[:, c] = top[::-1, c]
    bottom = nodes[r2+1:-1, 1:-1]
    for c in range(1, bottom.shape[1], 2):
        bottom[:, c] = bottom[::-1, c]
    yield [
        *nodes[r2, c2-1::-1],    # (1)
        *top.T.ravel(),          # (2)
        *nodes[r2:, -1],         # (3)
        *nodes[-1, -2::-1],      # (4)
        *nodes[:r2:-1, 0],       # (5)
        *bottom.T.ravel(),       # (6)
        *nodes[r2, -2:c2-1:-1],  # (7)
    ]


# Example
nrows, ncols = 7, 8
source = 3, 3
target = 3, 4
path = next(special_case_4(nrows, ncols, source, target))