如何连接可变数量的换热器模型段?
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))
我在 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))