Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Version 2.4 (in development)

### Enhancements

* [Issue 211](https://github.com/MassimoCimmino/pygfunction/issues/211) - Refactored the `heat_transfer` module. The `finite_line_source`, `finite_line_source_vertical` and `finite_line_source_inclined` functions now use `Borefield` objects as arguments instead of the geometrical parameters of the boreholes. This simplifies calls to these functions.

### Other changes

* [Issue 319](https://github.com/MassimoCimmino/pygfunction/issues/319) - Created `solvers` module. `Solver` classes are moved out of the `gfunction` module and into the new module.
Expand All @@ -16,7 +20,7 @@

### Bug fixes

* [Issue 305](https://github.com/MassimoCimmino/pygfunction/issues/305) - Fixed `ClaessonJaved` to return a float when the *g*-function is a vector (i.e. when there is only one heat source). This is required for compatibility with `numpy` version `2.x`.
* [Issue 307](https://github.com/MassimoCimmino/pygfunction/issues/307) - Fixed `ClaessonJaved` to return a float when the *g*-function is a vector (i.e. when there is only one heat source). This is required for compatibility with `numpy` version `2.x`.

### Other changes

Expand Down
1 change: 1 addition & 0 deletions doc/source/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Modules
.. toctree::
:maxdepth: 2

modules/borefield
modules/boreholes
modules/gfunction
modules/heat_transfer
Expand Down
10 changes: 10 additions & 0 deletions doc/source/modules/borefield.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. borefield:

****************
Borefield Module
****************

.. automodule:: pygfunction.borefield
:members:
:undoc-members:
:show-inheritance:
1 change: 0 additions & 1 deletion doc/source/modules/heat_transfer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ Heat Transfer Module

.. automodule:: pygfunction.heat_transfer
:members:
:undoc-members:
:show-inheritance:
277 changes: 259 additions & 18 deletions pygfunction/borefield.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
from collections.abc import Callable
from typing import Union, List, Dict, Tuple
from typing_extensions import Self # for compatibility with Python <= 3.10

import numpy as np
import numpy.typing as npt
from typing_extensions import Self # for compatibility with Python <= 3.10

from .boreholes import Borehole
from .boreholes import Borehole, _EquivalentBorehole
from .utilities import _initialize_figure, _format_axes, _format_axes_3d


Expand Down Expand Up @@ -43,30 +44,29 @@ def __init__(
self, H: npt.ArrayLike, D: npt.ArrayLike, r_b: npt.ArrayLike,
x: npt.ArrayLike, y: npt.ArrayLike, tilt: npt.ArrayLike = 0.,
orientation: npt.ArrayLike = 0.):
# Convert x and y coordinates to arrays
x = np.atleast_1d(x)
y = np.atleast_1d(y)
self.nBoreholes = np.maximum(len(x), len(y))
# Extract arguments
arg_names = ("H", "D", "r_b", "x", "y", "tilt", "orientation")
args = (H, D, r_b, x, y, tilt, orientation)

# Check if arguments are broadcastable to a valid common shape
b = np.broadcast(*args)
assert b.ndim <= 1, "All inputs must be scalars or 1D arrays."
self.nBoreholes = b.size

# Broadcast all variables to arrays of length `nBoreholes`
self.H = np.broadcast_to(H, self.nBoreholes)
self.D = np.broadcast_to(D, self.nBoreholes)
self.r_b = np.broadcast_to(r_b, self.nBoreholes)
self.x = np.broadcast_to(x, self.nBoreholes)
self.y = np.broadcast_to(y, self.nBoreholes)
self.tilt = np.broadcast_to(tilt, self.nBoreholes)
for name, value in zip(arg_names, args):
setattr(self, name, np.broadcast_to(value, self.nBoreholes))

# Identify tilted boreholes
self._is_tilted = np.broadcast_to(
np.greater(np.abs(tilt), 1e-6),
self.nBoreholes)
# Vertical boreholes default to an orientation of zero
if not np.any(self._is_tilted):
self.orientation = np.broadcast_to(0., self.nBoreholes)
elif np.all(self._is_tilted):
self.orientation = np.broadcast_to(orientation, self.nBoreholes)
else:
self.orientation = np.multiply(orientation, self._is_tilted)
self.orientation = np.where(
np.broadcast_to(self._is_tilted, self.nBoreholes),
orientation,
0.
)

def __getitem__(self, key):
if isinstance(key, (int, np.integer)):
Expand Down Expand Up @@ -107,6 +107,58 @@ def __ne__(
check = not self == other_field
return check

@property
def is_tilted(self) -> np.ndarray:
"""Return an boolean array. True if the corresponding borehole is inclined."""
return self._is_tilted

@property
def is_vertical(self) -> np.ndarray:
"""Return an boolean array. True if the corresponding borehole is vertical."""
return np.logical_not(self._is_tilted)

def distance(
self, other_field: Union[Borehole, Self, List[Borehole]],
outer: bool = True
) -> np.ndarray:
"""
Evaluate horizontal distances to boreholes of another borefield.

Parameters
----------
other_field : Borehole or Borefield object
Borefield with which to evaluate distances.
outer : , optional
True if the horizontal distance is to be evaluated for all
boreholes of the borefield onto all boreholes of the other
borefield to return a (nBoreholes_other_field, nBoreholes,)
array. If false, the horizontal distance is evaluated pairwise
between boreholes of the borefield and boreholes of the other
borefield. The numbers of boreholes should be the same
(i.e. nBoreholes == nBoreholes_other_field) and a (nBoreholes,)
array is returned.
Default is True.

Returns
-------
dis : array, shape (nBoreholes_other_field, nBoreholes,) or (nBoreholes,)
Horizontal distances between boreholes (in meters).

"""
if outer:
dis = np.maximum(
np.sqrt(
np.subtract.outer(other_field.x, self.x)**2
+ np.subtract.outer(other_field.y, self.y)**2
),
self.r_b)
else:
dis = np.maximum(
np.sqrt(
(other_field.x - self.x)**2 + (other_field.y - self.y)**2),
self.r_b)
return dis

def evaluate_g_function(
self,
alpha: float,
Expand Down Expand Up @@ -304,6 +356,77 @@ def evaluate_g_function(

return gfunc.gFunc

def segments(
self, nSegments: Union[int, npt.ArrayLike],
segment_ratios: Union[
None, npt.ArrayLike, List[npt.ArrayLike], Callable[[int], npt.ArrayLike]
] = None) -> Self:
"""
Split boreholes in the bore field into segments.

Parameters
----------
nSegments : int or (nBoreholes,) array
Number of segments per borehole.
segment_ratios : array or list of arrays, optional
Ratio of the borehole length represented by each segment. The sum
of ratios must be equal to 1. The shape of the array is of
(nSegments,). If all boreholes have the same number of segments
and the same ratios, a single (nSegments,) array can be provided.
Otherwise, a list of (nSegments_i,) arrays with the ratios
associated with each borehole must be provided. If
segment_ratios==None, segments of equal lengths are considered.
Default is None.

Returns
-------
segments : Borefield object
Segments in the bore field.

Examples
--------
>>> borefield = gt.borefield.Borefield.rectangle_field(
N_1=2, N_2=3, B_1 = 7.5, B_2=7.5, H=150., D=4., r_b=0.075)
>>> borefield.segments(5)

"""
nSegments = np.broadcast_to(nSegments, self.nBoreholes)
if callable(segment_ratios):
segment_ratios = [segment_ratios(nSeg) for nSeg in nSegments]
if segment_ratios is None:
# Local coordinates of the top-edges of the segments
xi = np.concatenate(
[np.arange(nSeg_i) / nSeg_i for nSeg_i in nSegments])
# Segment ratios
segment_ratios = np.concatenate(
[np.full(nSeg_i, 1. / nSeg_i) for nSeg_i in nSegments])
elif isinstance(segment_ratios, list):
# Local coordinates of the top-edges of the segments
xi = np.concatenate(
[np.cumsum(np.concatenate([[0.], seg_rat_i[:-1]]))
for seg_rat_i in segment_ratios])
# Segment ratios
segment_ratios = np.concatenate(segment_ratios)
else:
# Local coordinates of the top-edges of the segments
xi = np.tile(
np.cumsum(np.concatenate([[0.], segment_ratios[:-1]])),
self.nBoreholes)
# Segment ratios
segment_ratios = np.tile(segment_ratios, self.nBoreholes)
xi = xi * np.repeat(self.H, nSegments)

# Geometrical parameters of the segments
H = np.repeat(self.H, nSegments) * segment_ratios
r_b = np.repeat(self.r_b, nSegments)
tilt = np.repeat(self.tilt, nSegments)
orientation = np.repeat(self.orientation, nSegments)
D = np.repeat(self.D, nSegments) + xi * np.cos(tilt)
x = np.repeat(self.x, nSegments) + xi * np.sin(tilt) * np.cos(orientation)
y = np.repeat(self.y, nSegments) + xi * np.sin(tilt) * np.sin(orientation)

return Borefield(H, D, r_b, x, y, tilt=tilt, orientation=orientation)

def visualize_field(
self, viewTop: bool = True, view3D: bool = True,
labels: bool = True, showTilt: bool = True):
Expand Down Expand Up @@ -1098,3 +1221,121 @@ def circle_field(
# Create the bore field
borefield = cls(H, D, r_b, x, y, tilt=tilt, orientation=orientation)
return borefield


class _EquivalentBorefield(object):
"""
Contains information regarding the dimensions and positions of boreholes within a borefield.

Attributes
----------
H : (nEqBoreholes,) array
Borehole lengths (in meters).
D : (nEqBoreholes,) array
Borehole buried depths (in meters).
r_b : (nEqBoreholes,) array
Borehole radii (in meters).
x : (nEqBoreholes,) list of (nBoreholes_i,) arrays
Position (in meters) of the head of the boreholes along the x-axis.
y : (nEqBoreholes,) list of (nBoreholes_i,) arrays
Position (in meters) of the head of the boreholes along the y-axis.
tilt : (nEqBoreholes,) array, optional
Angle (in radians) from vertical of the axis of the boreholes.
Default is 0.
orientation : (nEqBoreholes,) list of (nBoreholes_i,) arrays, optional
Direction (in radians) of the tilt of the boreholes. Defaults to zero
if the borehole is vertical.
Default is 0.

"""

def __init__(
self, H: npt.ArrayLike, D: npt.ArrayLike, r_b: npt.ArrayLike,
x: List[npt.ArrayLike], y: List[npt.ArrayLike],
tilt: npt.ArrayLike, orientation: List[npt.ArrayLike]):
self.H = H
self.D = D
self.r_b = r_b
self.x = x
self.y = y
self.tilt = tilt
self.orientation = orientation
self.nBoreholes = len(x)

# Identify tilted boreholes
self._is_tilted = np.broadcast_to(
np.greater(np.abs(tilt), 1e-6),
self.nBoreholes)
# Vertical boreholes default to an orientation of zero
if not np.any(self._is_tilted):
self.orientation = [np.broadcast_to(0., len(x_i)) for x_i in x]
else:
self.orientation = [
np.multiply(orientation_i, _is_tilted_i)
for (orientation_i, _is_tilted_i)
in zip(self.orientation, self._is_tilted)]

def __getitem__(self, key):
if isinstance(key, (int, np.integer)):
# Returns a borehole object if only one borehole is indexed
output_class = _EquivalentBorehole
else:
# Returns a _EquivalentBorehole object for slices and lists of
# indexes
output_class = _EquivalentBorefield
return output_class(
self.H[key], self.D[key], self.r_b[key], self.x[key], self.y[key],
tilt=self.tilt[key], orientation=self.orientation[key])

def __len__(self) -> int:
"""Return the number of equivalent boreholes."""
return self.nBoreholes

def segments(
self, nSegments: int,
segment_ratios: Union[
None, npt.ArrayLike, Callable[[int], npt.ArrayLike]
] = None) -> Self:
"""
Split boreholes in the bore field into segments.

Parameters
----------
nSegments : int or (nBoreholes,) array
Number of segments per borehole.
segment_ratios : array or list of arrays, optional
Ratio of the borehole length represented by each segment. The sum
of ratios must be equal to 1. The shape of the array is of
(nSegments,). If all boreholes have the same number of segments
and the same ratios, a single (nSegments,) array can be provided.
Otherwise, a list of (nSegments_i,) arrays with the ratios
associated with each borehole must be provided. If
segment_ratios==None, segments of equal lengths are considered.
Default is None.

Returns
-------
segments : Borefield object
Segments in the bore field.

Examples
--------
>>> borefield = gt.borefield.Borefield.rectangle_field(
N_1=2, N_2=3, B_1 = 7.5, B_2=7.5, H=150., D=4., r_b=0.075)
>>> borefield.segments(5)

"""
return _EquivalentBorefield.from_equivalent_boreholes(
[seg for b in self for seg in b.segments(nSegments, segment_ratios=segment_ratios)
])

@classmethod
def from_equivalent_boreholes(cls, equivalent_boreholes):
H = np.array([b.H for b in equivalent_boreholes])
D = np.array([b.D for b in equivalent_boreholes])
r_b = np.array([b.r_b for b in equivalent_boreholes])
x = [b.x for b in equivalent_boreholes]
y = [b.y for b in equivalent_boreholes]
tilt = np.array([b.tilt for b in equivalent_boreholes])
orientation = [b.orientation for b in equivalent_boreholes]
return cls(H, D, r_b, x, y, tilt, orientation)
Loading