MassimoCimmino / pygfunction

An open-source toolbox for the evaluation of thermal response factors (g-functions) of geothermal borehole fields.
BSD 3-Clause "New" or "Revised" License
46 stars 21 forks source link

Create dense borefield function #270

Closed wouterpeere closed 7 months ago

wouterpeere commented 1 year ago

I wrote another function to create a 'dens borefield' where the boreholes are packed as densely as possible with a minimum distance B. Instead of creating a regular grid, where all the boreholes are underneath each other, this function moves every uneven row to the right, so it's y-coordinate can be lower.

This creates borefields like: image

MassimoCimmino commented 1 year ago

Thank you @wouterpeere. For the minimum spacing B to be correctly implemented, shouldn't the vertical spacing be B_y = sqrt(3)/2, so that the spacing B is also respected on the diagonally adjacent boreholes?

tblanke commented 1 year ago

Hi,

What about implementing both, because I think the first solution is easier to handle for the user. Considering the first solution the width and length of the borefield is the multiplication of the number of boreholes times the distance. The same distance on diagonal is indeed the denser solution. Here my code as suggestion:

def dense_field(
    number_width: int,
    number_length: int,
    distance_width: float,
    distance_length: float,
    depth: float,
    burial_depth: float,
    borehole_radius: float,
    include_last_borehole: bool = True,
    same_distance_diagonal: bool = True,
) -> list[Borehole]:
    """
    Build a list of boreholes in a dense bore field configuration.
    Here, the high density cylinder packing is used. This means that every borehole is a distance B (in meters)
    away from each other.

    Parameters
    ----------
    number_width : int
        Number of borehole in the x direction.
    number_length : int
        Number of borehole in the y direction.
    distance_width : float
        Distance (in meters) between adjacent boreholes in width direction.
    distance_length : float
        Distance (in meters) between adjacent boreholes in length direciton.
    depth : float
        Borehole length (in meters).
    burial_depth : float
        Borehole buried depth (in meters).
    borehole_radius : float
        Borehole radius (in meters).
    include_last_borehole : bool
        True if each row of boreholes should have equal lengths. False, if the uneven rows have one borehole less
        so they are contained within the imaginary 'box' around the borefield
    same_distance_diagonal: bool
        should be the same distance on the diagonal (True) or in width and length direction (False) be set

    Returns
    -------
    boreField : list of Borehole objects
        List of boreholes in the dense bore field.

    Examples
    --------
    >>> import pygfunction as gt
    >>> boreField = gt.boreholes.dense_field(number_width=3, number_length=2, distance_width=5., distance_length=5., depth=100., burial_depth=2.5,
    >>> borehole_radius=0.05, include_last_borehole=True)

    The bore field is constructed line by line. For N_1=3 and N_2=3, the bore
    field layout is as follows::

     6   7   8
       3   4   5
     0   1   2

    """
    borefield = []

    # check for line
    if number_width == 1 or number_length == 1:
        return rectangle_field(number_width, number_length, distance_width, distance_length, depth, burial_depth, borehole_radius)

    fac: float = np.sqrt(2) if same_distance_diagonal else 1

    for j in range(number_length):  # y direction
        for i in range(number_width):  # x direction
            x = i * distance_width * fac + (distance_length * fac / 2 if j % 2 == 1 else 0)
            y = j * distance_length * fac / 2
            if include_last_borehole or (j % 2 == 0 or i != number_width - 1):  # last borehole in the x direction on an oneven row
                borefield.append(Borehole(depth, burial_depth, borehole_radius, x, y))

    return borefield

@pytest.mark.parametrize(
    "N_1, N_2, B, include_last_element, same_distance_diagonal",
    [
        (n1, n2, b, last, diag)
        for n1, n2, b in [(1, 1, 5.0), (2, 1, 5.0), (1, 2, 5.0), (2, 2, 5.0), (10, 9, 7.5), (10, 10, 7.5)]
        for last in [True, False]
        for diag in [True, False]
    ],
)
def test_dense_field(N_1, N_2, B, include_last_element, same_distance_diagonal):
    H = 150.0  # Borehole length [m]
    D = 4.0  # Borehole buried depth [m]
    r_b = 0.075  # Borehole radius [m]
    # Generate the bore field
    field = gt.boreholes.dense_field(N_1, N_2, B, B, H, D, r_b, include_last_borehole=include_last_element, same_distance_diagonal=same_distance_diagonal)
    # Evaluate the borehole to borehole distances
    x = np.array([b.x for b in field])
    y = np.array([b.y for b in field])
    dis = np.sqrt(np.subtract.outer(x, x) ** 2 + np.subtract.outer(y, y) ** 2)[~np.eye(len(field), dtype=bool)]

    if include_last_element or N_1 == 1 or N_2 == 1:
        assert len(field) == N_1 * N_2
    elif N_2 % 2 == 0:
        assert len(field) == N_2 * (2 * N_1 - 1) / 2
    else:
        assert len(field) == (N_2 - 1) * (2 * N_1 - 1) / 2 + N_1

    if N_1 > 1 and N_2 > 1:
        if same_distance_diagonal:
            assert np.isclose(np.sqrt(x[y > 0.1][0] ** 2 + y[y > 0.1][0] ** 2), B)
        else:
            assert np.isclose(x[y > 0.1][0], B / 2)
            assert np.isclose(y[y > 0.1][0], B / 2)

    assert np.all(
        [
            np.allclose(H, [b.H for b in field]),
            np.allclose(D, [b.D for b in field]),
            np.allclose(r_b, [b.r_b for b in field]),
        ]
    )
MassimoCimmino commented 1 year ago

@tblanke I like the idea of having both options. I would prefer having two functions instead of the same_distance_diagonal argument, which I believe would be more intuitive for the user. B_2 (distance_length) should be the distance between rows, rather than twice the distance.

Different spacings in both directions (there is probably a better name for this function)

def rectangle_field_triangular(N_1, N_2, B_1, B_2, H, D, r_b, include_last_borehole, tilt=0., origin=None):
    """
    Build a list of boreholes in a rectangular bore field configuration, with boreholes
    placed in a triangular pattern.

    Parameters
    ----------
    N_1 : int
        Number of borehole in the x direction.
    N_2 : int
        Number of borehole in the y direction.
    B_1 : float
        Distance (in meters) between adjacent boreholes in the x direction.
    B_2 : float
        Distance (in meters) between adjacent boreholes in the y direction.
    H : float
        Borehole length (in meters).
    D : float
        Borehole buried depth (in meters).
    r_b : float
        Borehole radius (in meters).
    include_last_borehole : bool
        True if each row of boreholes should have equal lengths. False, if the uneven rows have one borehole less
        so they are contained within the imaginary 'box' around the borefield
    tilt : float, optional
        Angle (in radians) from vertical of the axis of the borehole. The
        orientation of the tilt is orthogonal to the origin coordinate.
        Default is 0.
    origin : tuple, optional
        A coordinate indicating the origin of reference for orientation of
        boreholes.
        Default is the center of the rectangle.

    Returns
    -------
    boreField : list of Borehole objects
        List of boreholes in the rectangular bore field.

    Notes
    -----
    Boreholes located at the origin will remain vertical.

    Examples
    --------
    >>> boreField = gt.boreholes.rectangle_field(N_1=3, N_2=2, B_1=5., B_2=5.,
                                                 H=100., D=2.5, r_b=0.05)

    The bore field is constructed line by line. For N_1=3 and N_2=3, the bore
    field layout is as follows, if `include_last_borehole` is True::

     6    7    8
       3    4    5
     0    1    2

    and if `include_last_borehole` is False::

     5    6    7
       3    4 
     0    1    2

    """
    borefield = []

    if origin is None:
        # When no origin is supplied, compute the origin to be at the center of
        # the rectangle
        x0 = (N_1 - 1) / 2 * B_1
        y0 = (N_2 - 1) / 2 * B_2
    else:
        x0, y0 = origin

    for j in range(N_2):
        for i in range(N_1):
            x = i * B_1 + (B_1 / 2 if j % 2 == 1 else 0)
            y = j * B_2
            # The borehole is inclined only if it does not lie on the origin
            if np.sqrt((x - x0)**2 + (y - y0)**2) > r_b:
                orientation = np.arctan2(y - y0, x - x0)
                if i < (N_1 - 1) or include_last_borehole or (j % 2 == 0):
                    borefield.append(
                        Borehole(
                            H, D, r_b, x, y, tilt=tilt, orientation=orientation))
            else:
                if i < (N_1 - 1) or include_last_borehole or (j % 2 == 0):
                    borefield.append(Borehole(H, D, r_b, x, y))

    return borefield

Dense field with equal distance along the diagonals

def dense_rectangle_field(N_1, N_2, B, H, D, r_b, include_last_borehole, tilt=0., origin=None):
    """
    Build a list of boreholes in a rectangular bore field configuration, with boreholes
    placed in a hexagonal pattern.

    Parameters
    ----------
    N_1 : int
        Number of borehole in the x direction.
    N_2 : int
        Number of borehole in the y direction.
    B_1 : float
        Distance (in meters) between adjacent boreholes.
    H : float
        Borehole length (in meters).
    D : float
        Borehole buried depth (in meters).
    r_b : float
        Borehole radius (in meters).
    include_last_borehole : bool
        True if each row of boreholes should have equal lengths. False, if the uneven rows have one borehole less
        so they are contained within the imaginary 'box' around the borefield
    tilt : float, optional
        Angle (in radians) from vertical of the axis of the borehole. The
        orientation of the tilt is orthogonal to the origin coordinate.
        Default is 0.
    origin : tuple, optional
        A coordinate indicating the origin of reference for orientation of
        boreholes.
        Default is the center of the rectangle.

    Returns
    -------
    boreField : list of Borehole objects
        List of boreholes in the rectangular bore field.

    Notes
    -----
    Boreholes located at the origin will remain vertical.

    Examples
    --------
    >>> boreField = gt.boreholes.rectangle_field(N_1=3, N_2=2, B_1=5., B_2=5.,
                                                 H=100., D=2.5, r_b=0.05, include_last_borehole=True)

    The bore field is constructed line by line. For N_1=3 and N_2=3, the bore
    field layout is as follows, if `include_last_borehole` is True::

     6    7    8
       3    4    5
     0    1    2

    and if `include_last_borehole` is False::

     5    6    7
       3    4 
     0    1    2

    """
    borefield = rectangle_field_triangular(N_1, N_2, B, sqrt(3)/2 * B, H, D, r_b, include_last_borehole, tilt=tilt, origin=origin)

    return borefield
wouterpeere commented 12 months ago

Hi @tblanke

Good idea indeed!

@MassimoCimmino I will implement the code you wrote above and add some tests.

Best, Wouter

MassimoCimmino commented 7 months ago

@all-contributors please add @wouterpeere for code, ideas, bug. please add @tblanke for code, ideas, bug.

allcontributors[bot] commented 7 months ago

@MassimoCimmino

I've put up a pull request to add @wouterpeere! :tada:

I've put up a pull request to add @tblanke! :tada:

MassimoCimmino commented 7 months ago

@all-contributors please add @tblanke for code, ideas, bug.

allcontributors[bot] commented 7 months ago

@MassimoCimmino

I've put up a pull request to add @tblanke! :tada: