I write a code in python for ANSA modeling software, which, based on given parameters, will create a model of heat exchanger. You can see the example of the model below. The blue colored elements represent water, the grey ones represent the pipe, the brown one represent air. But there can be any number of rows and columns in the exchanger. I got this covered so far. I got some default segment, then I set it desired size based on given parameters and then I copy the segment to create x rows and y columns.
But now I need to connect these segments, so there will be continuous pipe with water flowing (with one input and one output), as seen on the second image. What you can see I created manually, but I need to be able to create these connections parametrically by script.
I have absolutely no idea how to do this. To be precise, I can´t figure out the logic of the code. The command that creates the elements is not a problem. Since there can be any number of rows and columns, the problem is not only how to connect the segments, but also in what direction should the connections lead and where should be the input and output. I will provide more details if needed. And my code is pretty long so far and uses special commands for the modeling software, so I guess it wouldn´t be much help. But I will include it in case of need. But once again - I don´t need specific commands, but only general logic of the code.
This problem is equivalent to finding a 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 for more information.
The constraint that each connection of two nodes in the grid reverses the flow of the water ("into the page" or "out of the page") simply puts a constraint on the total number of nodes in the grid. An odd number of connections will reverse the flow from input to output node while an even number will maintain it. Since both input and output pipe should be on the same side of the device, the flow must be reversed, i.e an odd number of connections must be made. Since the path should visit every node in the grid, this means that the grid must consist of an even number of nodes (the number of connections is one less than the number of nodes). Hence, if nrows*ncols
is even then this constraint will be automatically satisfied and if nrows*nocls
is odd then it cannot be satisfied.
If the size of the grid is not too large, one can try to find such a Hamiltonian path via brute force search, e.g. using the 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()
Which produces the following path:
(This generalizes to any other corner as well, as long as input and output nodes lie on a straight line and are separated by an even number of rows or columns.)
We can connect the (0,0)
node to the (1,0)
node by connecting the entire first row left to right, then moving down into the second row and connect it right to left. By continuing this pattern, we can connect any number N of row pairs (i.e. total number of rows is 2*N even).
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))
(Similar to case (1), this generalizes to any corner.)
From case (1) we know how to fully connect a (2*M, N)
grid. Now case (2) can be split into two sub-problems, namely the first column and the remainder of the grid (denoted with R). We can connect R in a similar fashion as for case (1). So all we need to do is to connect the first column to R. Connecting the input node at (0,0)
to the left upper corner of R at (0,1)
is trivial. Then we can connect the left lower corner of R to the bottom node of the first column and then move all the way up to the output node at (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))
For the sake of the example, let's assume they are located at the center of the top edge. We can split this problem into two sub-problems, left and right, and connect both input and output to their corresponding node on the bottom row in a fashion similar to case (1). Then, connecting the two problems amounts to simply connecting these two nodes.
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))
For this we require only that the number of columns is even. The procedure is best explained by looking at the sketch and code and especially at the example plot below:
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))