Search code examples
pythonlogicmodeling

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


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.

enter image description here enter image description here


Solution

  • 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:

    Example

    Special cases

    1) Even number of rows and input at left upper corner, output at left lower corner

    (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))
    

    Special case 1

    2) Even number of rows and input at left upper corner, output directly below input

    (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))
    

    Special case 2

    3) Even number of rows and columns, input and output located next to each other at the center of one the edges of the grid

    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))
    

    Special case 3

    4) Input and output next to each other in the center of the grid

    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:

    Sketch case 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))
    

    Special case 4