from dataclasses import dataclass
from math import ceil
from pathlib import Path
from tempfile import mkdtemp
import gmsh
import numpy as np
import pyvista as pv
from ogstools.meshlib import meshes_from_gmsh
[docs]
@dataclass(frozen=True)
class Groundwater:
begin: float = -30
""" depth of groundwater begin (negative) in m """
isolation_layer_id: int = 1
""" number of the groundwater isolation layer (count starts with 0)"""
flow_direction: str = "+x"
""" groundwater inflow direction as string - supported '+x', '-x', '-y', '+y' """
[docs]
@dataclass(frozen=True)
class BHE:
"""(B)orehole (H)eat (E)xchanger"""
x: float = 50.0
"""x-coordinate of the BHE in m"""
y: float = 50.0
"""y-coordinate of the BHE in m"""
z_begin: float = -1.0
"""BHE begin depth (zero or negative) in m"""
z_end: float = -60.0
"""BHE end depth (negative) in m"""
borehole_radius: float = 0.076
"""borehole radius in m"""
[docs]
def gen_bhe_mesh_gmsh(
length: float,
width: float,
layer: float | list[float],
groundwater: Groundwater | list[Groundwater],
BHE_Array: BHE | list[BHE],
target_z_size_coarse: float = 7.5,
target_z_size_fine: float = 1.5,
n_refinement_layers: int = 2,
meshing_type: str = "structured",
dist_box_x: float = 5.0,
dist_box_y: float = 10.0,
inner_mesh_size: float = 5.0,
outer_mesh_size: float = 10.0,
propagation: float = 1.1,
order: int = 1,
out_name: Path = Path("bhe_mesh.msh"),
) -> None:
"""
Create a generic BHE mesh for the Heat_Transport_BHE-Process with additionally submeshes at the top, at the bottom and the groundwater inflow, which is exported in the Gmsh .msh format. For the usage in OGS, a mesh conversion with meshes_from_gmsh with dim-Tags [1,3] is needed. The mesh is defined by multiple input parameters. Refinement layers are placed at the BHE-begin, the BHE-end and the groundwater start/end. See detailed description of the parameters below:
:param length: Length of the model area in m (x-dimension)
:param width: Width of the model area in m (y-dimension)
:param layer: List of the soil layer thickness in m
:param groundwater: List of groundwater layers, where every is specified by a tuple of three entries: [depth of groundwater begin (negative), number of the groundwater isolation layer (count starts with 0), groundwater inflow direction as string - supported '+x', '-x', '-y', '+y'], empty list [] for no groundwater flow
:param BHE_Array: List of BHEs, where every BHE is specified by a tuple of five floats: [x-coordinate BHE, y-coordinate BHE, BHE begin depth (zero or negative), BHE end depth (negative), borehole radius in m]
:param target_z_size_coarse: maximum edge length of the elements in m in z-direction, if no refinemnt needed
:param target_z_size_fine: maximum edge length of the elements in the refinement zone in m in z-direction
:param n_refinement_layers: number of refinement layers which are evenly set above and beneath the refinemnt depths (see general description above)
:param meshing_type: 'structured' and 'prism' are supported
:param dist_box_x: distance in m in x-direction of the refinemnt box according to the BHE's
:param dist_box_y: distance in m in y-direction of the refinemnt box according to the BHE's
:param inner_mesh_size: mesh size inside the refinement box in m
:param outer_mesh_size: mesh size outside of the refinement box in m
:param propagation: growth of the outer_mesh_size, only supported by meshing_type 'structured'
:param order: Define the order of the mesh: 1 for linear finite elements / 2 for quadratic finite elements
:param out_name: name of the exported mesh, must end with .msh
:returns: a gmsh .msh file
"""
def _compute_layer_spacing(
z_min: float, z_max: float, z_min_id: int, id_j: float, id_j_max: float
) -> None:
delta_z = np.abs(z_min - z_max)
size_fine = n_refinement_layers * target_z_size_fine
target_layer: list = number_of_layers[len(number_of_layers) - 1]
if z_min_id == 3: # and id_j==1 - not needed, but logically safer
if id_j == id_j_max:
space = delta_z - size_fine - space_next_layer_refined
space_fine_next = space + size_fine + space_next_layer_refined
if space_next_layer_refined == 0:
if space <= target_z_size_fine:
absolute_height_of_layers.append(delta_z)
target_layer.append(ceil(delta_z / target_z_size_fine))
else:
absolute_height_of_layers.extend([size_fine, space])
target_layer.append(n_refinement_layers)
target_layer.append(ceil(space / target_z_size_coarse))
else:
if space <= target_z_size_fine:
absolute_height_of_layers.append(space_fine_next)
target_layer.append(
ceil(space_fine_next / target_z_size_fine)
)
else:
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
absolute_height_of_layers.append(space)
target_layer.append(ceil(space / target_z_size_coarse))
absolute_height_of_layers.append(
space_next_layer_refined
)
target_layer.append(
ceil(space_next_layer_refined / target_z_size_fine)
)
else:
# all negative values, because of the extrusion in negative z-direction -> z_min -- End Layer, z_max -- start layer
space = delta_z - size_fine
if (
np.abs(space)
<= (n_refinement_layers + 1) * target_z_size_fine
):
absolute_height_of_layers.append(space + size_fine)
target_layer.append(
ceil((space + size_fine) / target_z_size_fine)
)
else:
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
absolute_height_of_layers.append(space - size_fine)
target_layer.append(
ceil((space - size_fine) / target_z_size_coarse)
)
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
# erstes Mesh in jeweiligen Soil-Layer
elif id_j == 1 and id_j != id_j_max:
space = delta_z - space_last_layer_refined
space_last = space + space_last_layer_refined
if np.abs(space) <= (n_refinement_layers + 1) * target_z_size_fine:
absolute_height_of_layers.append(space_last)
target_layer.append(ceil(space_last / target_z_size_fine))
else:
if space_last_layer_refined == 0:
absolute_height_of_layers.append(space - size_fine)
target_layer.append(
ceil((space - size_fine) / target_z_size_coarse)
)
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
else:
absolute_height_of_layers.append(space_last_layer_refined)
target_layer.append(
ceil(space_last_layer_refined / target_z_size_fine)
)
absolute_height_of_layers.append(space - size_fine)
target_layer.append(
ceil((space - size_fine) / target_z_size_coarse)
)
absolute_height_of_layers.append(2 * target_z_size_fine)
target_layer.append(n_refinement_layers)
elif id_j == id_j_max and id_j != 1:
space = delta_z - space_next_layer_refined
space_next = space + space_next_layer_refined
if space <= (n_refinement_layers + 1) * target_z_size_fine:
absolute_height_of_layers.append(space_next)
target_layer.append(ceil(space_next / target_z_size_fine))
else:
if space_next_layer_refined == 0:
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
absolute_height_of_layers.append(space - size_fine)
target_layer.append(
ceil((space - size_fine) / target_z_size_coarse)
)
else:
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
absolute_height_of_layers.append(space - size_fine)
target_layer.append(
ceil((space - size_fine) / target_z_size_coarse)
)
absolute_height_of_layers.append(space_next_layer_refined)
target_layer.append(
ceil(space_next_layer_refined / target_z_size_fine)
)
# Layer without a needed depth of BHE or Groundwater
elif id_j == id_j_max and id_j == 1:
space = delta_z
space_next = space - space_next_layer_refined
space_last = space - space_last_layer_refined
space_nextlast = space_next - space_last_layer_refined
if space_last_layer_refined == 0 and space_next_layer_refined == 0:
absolute_height_of_layers.append(space)
target_layer.append(ceil(space / target_z_size_coarse))
elif space_next_layer_refined == 0:
if space_last <= target_z_size_fine:
absolute_height_of_layers.append(space)
target_layer.append(ceil(space / target_z_size_fine))
else:
absolute_height_of_layers.append(space_last_layer_refined)
target_layer.append(
ceil(space_last_layer_refined / target_z_size_fine)
)
absolute_height_of_layers.append(space_last)
target_layer.append(ceil(space_last / target_z_size_coarse))
elif space_last_layer_refined == 0:
if space_next <= target_z_size_fine:
absolute_height_of_layers.append(space)
target_layer.append(ceil(space / target_z_size_fine))
else:
absolute_height_of_layers.append(space_next)
target_layer.append(ceil(space_next / target_z_size_coarse))
absolute_height_of_layers.append(space_next_layer_refined)
target_layer.append(
ceil(space_next_layer_refined / target_z_size_fine)
)
else:
if space_nextlast <= target_z_size_fine:
absolute_height_of_layers.append(space)
target_layer.append(ceil(space / target_z_size_fine))
else:
absolute_height_of_layers.append(space_next_layer_refined)
target_layer.append(
ceil(space_next_layer_refined / target_z_size_fine)
)
absolute_height_of_layers.append(space_nextlast)
target_layer.append(
ceil(space_nextlast / target_z_size_coarse)
)
absolute_height_of_layers.append(space_last_layer_refined)
target_layer.append(
ceil(space_last_layer_refined / target_z_size_fine)
)
else:
space = delta_z
if space <= (2 * n_refinement_layers + 1) * target_z_size_fine:
absolute_height_of_layers.append(space)
target_layer.append(ceil(space / target_z_size_fine))
else:
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
absolute_height_of_layers.append(space - 2 * size_fine)
target_layer.append(
ceil((space - 2 * size_fine) / target_z_size_coarse)
)
absolute_height_of_layers.append(size_fine)
target_layer.append(n_refinement_layers)
# to flat a list, seems not so easy with a ready to use function --> this code is from https://realpython.com/python-flatten-list/
def _flatten_concatenation(matrix: list[list[float]]) -> list:
flat_list = []
for row in matrix:
flat_list += row
return flat_list
def _insert_BHE(inTag: int) -> list:
bhe_top_nodes = []
BHE_i: BHE
for BHE_i in BHE_array:
x, y, z = (BHE_i.x, BHE_i.y, 0)
# meshsize at BHE and distance of the surrounding optimal mesh points
# see Diersch et al. 2011 Part 2 for 6 surrounding nodes, not to be defined by user
delta = 6.134 * BHE_i.borehole_radius
bhe_center = gmsh.model.geo.addPoint(x, y, z, delta)
gmsh.model.geo.addPoint(x, y - delta, z, delta)
gmsh.model.geo.addPoint(x, y + delta, z, delta)
dx, dy = (0.866 * delta, 0.5 * delta)
gmsh.model.geo.addPoint(x + dx, y + dy, z, delta)
gmsh.model.geo.addPoint(x - dx, y + dy, z, delta)
gmsh.model.geo.addPoint(x + dx, y - dy, z, delta)
gmsh.model.geo.addPoint(x - dx, y - dy, z, delta)
if BHE_i.z_begin != 0:
bhe_top_nodes.append(
gmsh.model.geo.addPoint(x, y, BHE_i.z_begin, delta)
)
else:
bhe_top_nodes.append(bhe_center)
gmsh.model.geo.synchronize()
gmsh.model.mesh.embed(
0, list(range(bhe_center, bhe_center + 7)), 2, inTag
)
return bhe_top_nodes
def _mesh_structured() -> None:
for y_value in [0.0, y_min, y_max, width]:
for x_value in [0.0, x_min, x_max, length]:
gmsh.model.geo.addPoint(x_value, y_value, 0.0)
# x-direction
for i in range(14)[1::4]:
for j in range(3):
gmsh.model.geo.addLine(i + j, i + j + 1)
# y-direction
for i in range(1, 13):
gmsh.model.geo.addLine(i, i + 4)
# add surfaces
for i in range(1, 10):
i2 = i + 13 + (i - 1) // 3
gmsh.model.geo.addCurveLoop([i, i2, -i - 3, -i2 + 1])
gmsh.model.geo.addPlaneSurface([i])
gmsh.model.geo.synchronize()
bhe_top_nodes = _insert_BHE(inTag=5)
# Extrude the surface mesh according to the previously evaluated structure
volumes_list_for_layers = []
bounds_gw: dict[str, list] = {"+x": [], "-x": [], "+y": [], "-y": []}
boundaries_surfaces = {
"+x": [23, 41, 5],
"-x": [33, 15, 51],
"+y": [2, 8, 14],
"-y": [40, 46, 52],
}
top_surface = list(range(1, 10))
surface_list = [(2, tag) for tag in top_surface]
for j, num_elements in enumerate(number_of_layers):
# spacing of the each layer must be evaluated according to the implementation of the bhe
extrusion_tags = gmsh.model.geo.extrude(
surface_list,
0,
0,
-depth_of_extrusion[j],
num_elements,
cummulative_height_of_layers[j],
True,
) # soil 1
# list of volume numbers and new bottom surfaces, which were extruded by the five surfaces
volume_list = [extrusion_tags[1 + i * 6][1] for i in range(9)]
surface_list = [extrusion_tags[i * 6] for i in range(9)]
for direction, boundary_tags in bounds_gw.items():
for surf in boundaries_surfaces[direction]:
boundary_tags.append(extrusion_tags[surf][1])
volumes_list_for_layers.append(volume_list)
k = 0
BHE_group = []
for i, BHE_i in enumerate(BHE_array):
extrusion_tags = gmsh.model.geo.extrude(
[(0, bhe_top_nodes[i])],
0,
0,
BHE_i.z_end - BHE_i.z_begin,
BHE_extrusion_layers[i],
BHE_extrusion_depths[i],
True,
)
BHE_group.append(extrusion_tags[1][1])
gmsh.model.geo.synchronize()
for i, volume_i in enumerate(volumes_list_for_layers):
gmsh.model.addPhysicalGroup(3, volume_i, i)
for k, BHE_group_k in enumerate(BHE_group, start=i + 1):
gmsh.model.addPhysicalGroup(1, [BHE_group_k], k)
gmsh.model.addPhysicalGroup(2, top_surface, k + 1, name="Top_Surface")
gmsh.model.addPhysicalGroup(
2,
np.array(surface_list)[:, 1].tolist(),
k + 2,
name="Bottom_Surface",
)
gw_counter = 0 # counter_for_gw_start_at_soil_layer
for i, groundwater in enumerate(groundwater_list):
# add loop for different groundwater flow directions
offset = np.abs(groundwater[2]) in np.cumsum(layer)
start_id = 3 * (groundwater[0] + i + int(not offset) - gw_counter)
end_id = 3 * (groundwater[3] + i + int(not offset) - gw_counter)
if offset:
gw_counter += 1
gmsh.model.addPhysicalGroup(
2,
bounds_gw[groundwater[4]][start_id:end_id],
tag=k + 3,
name=f"Groundwater_Inflow_{i}",
)
k += 1
mesh = gmsh.model.mesh
# Sizing Functions and Transfinite Algorithm for Hexahedron meshing in wanted zones
# inner square
delta_x, delta_y = (x_max - x_min, y_max - y_min)
mesh.setTransfiniteCurve(5, ceil(delta_x / inner_mesh_size) + 1)
mesh.setTransfiniteCurve(8, ceil(delta_x / inner_mesh_size) + 1)
mesh.setTransfiniteCurve(17, ceil(delta_y / inner_mesh_size) + 1)
mesh.setTransfiniteCurve(18, ceil(delta_y / inner_mesh_size) + 1)
mesh.setTransfiniteCurve(19, ceil(delta_y / inner_mesh_size) + 1)
mesh.setTransfiniteCurve(20, ceil(delta_y / inner_mesh_size) + 1)
mesh.setTransfiniteCurve(13, ceil(y_min / outer_mesh_size_inner) + 1)
mesh.setTransfiniteCurve(14, ceil(y_min / outer_mesh_size_inner) + 1)
mesh.setTransfiniteCurve(15, ceil(y_min / outer_mesh_size_inner) + 1)
mesh.setTransfiniteCurve(16, ceil(y_min / outer_mesh_size_inner) + 1)
ny = ceil((width - y_max) / outer_mesh_size_inner) + 1
mesh.setTransfiniteCurve(21, ny)
mesh.setTransfiniteCurve(22, ny)
mesh.setTransfiniteCurve(23, ny)
mesh.setTransfiniteCurve(24, ny)
mesh.setTransfiniteCurve(2, ceil(delta_x / outer_mesh_size_inner) + 1)
mesh.setTransfiniteCurve(11, ceil(delta_x / outer_mesh_size_inner) + 1)
# rectangular squares bgw
mesh.setTransfiniteCurve(1, ceil(x_min / outer_mesh_size) + 1)
mesh.setTransfiniteCurve(4, ceil(x_min / outer_mesh_size) + 1)
mesh.setTransfiniteCurve(7, ceil(x_min / outer_mesh_size) + 1)
mesh.setTransfiniteCurve(10, ceil(x_min / outer_mesh_size) + 1)
num_nodes = ceil((length - x_max) / outer_mesh_size) + 1
mesh.setTransfiniteCurve(3, num_nodes, "Progression", propagation)
mesh.setTransfiniteCurve(6, num_nodes, "Progression", propagation)
mesh.setTransfiniteCurve(9, num_nodes, "Progression", propagation)
mesh.setTransfiniteCurve(12, num_nodes, "Progression", propagation)
for i, surface_tag in enumerate([1, 3, 4, 6, 7, 9]):
j = 2 * i
corner_tags = [1 + j, 2 + j, 5 + j, 6 + j]
mesh.setTransfiniteSurface(surface_tag, cornerTags=corner_tags)
mesh.setRecombine(2, surface_tag)
mesh.recombine()
def _mesh_prism() -> None:
# define the outer boundaries square
gmsh.model.geo.addPoint(0.0, 0.0, 0.0, outer_mesh_size)
gmsh.model.geo.addPoint(length, 0.0, 0.0, outer_mesh_size)
gmsh.model.geo.addPoint(length, width, 0.0, outer_mesh_size)
gmsh.model.geo.addPoint(0.0, width, 0.0, outer_mesh_size)
gmsh.model.geo.addLine(1, 2)
gmsh.model.geo.addLine(2, 3)
gmsh.model.geo.addLine(3, 4)
gmsh.model.geo.addLine(4, 1)
# inner points
gmsh.model.geo.addPoint(x_min, y_min, 0.0, inner_mesh_size)
gmsh.model.geo.addPoint(x_max, y_min, 0.0, inner_mesh_size)
gmsh.model.geo.addPoint(x_max, y_max, 0.0, inner_mesh_size)
gmsh.model.geo.addPoint(x_min, y_max, 0.0, inner_mesh_size)
gmsh.model.geo.addLine(5, 6)
gmsh.model.geo.addLine(6, 7)
gmsh.model.geo.addLine(7, 8)
gmsh.model.geo.addLine(8, 5)
gmsh.model.geo.addCurveLoop([1, 2, 3, 4], 1)
gmsh.model.geo.addPlaneSurface([1], 1)
gmsh.model.geo.synchronize()
# embed the four lines of the inner sizing box
gmsh.model.mesh.embed(1, [5, 6, 7, 8], 2, 1)
bhe_top_nodes = _insert_BHE(inTag=1)
# Extrude the surface mesh according to the previously evaluated structure
volumes_list_for_layers = []
top_surface = [1]
surface_list = [(2, 1)]
bounds_gw: dict[str, list] = {"+x": [], "-x": [], "+y": [], "-y": []}
boundaries_surfaces = {"+x": [5], "-x": [3], "+y": [2], "-y": [4]}
for j, num_elements in enumerate(number_of_layers):
# spacing of the each layer must be evaluated according to the implementation of the bhe
extrusion_tags = gmsh.model.geo.extrude(
surface_list,
0,
0,
-depth_of_extrusion[j],
num_elements,
cummulative_height_of_layers[j],
True,
) # soil 1
# list of new bottom surfaces, extruded by the five surfaces
surface_list = [extrusion_tags[0]]
for direction, boundary_tags in bounds_gw.items():
for surf in boundaries_surfaces[direction]:
boundary_tags.append(extrusion_tags[surf][1])
volumes_list_for_layers.append([extrusion_tags[1][1]])
BHE_group = []
for i, BHE_i in enumerate(BHE_array):
extrusion_tags = gmsh.model.geo.extrude(
[(0, bhe_top_nodes[i])],
0,
0,
BHE_i.z_end - BHE_i.z_begin,
BHE_extrusion_layers[i],
BHE_extrusion_depths[i],
)
BHE_group.append(extrusion_tags[1][1])
gmsh.model.geo.synchronize()
for i, volume_i in enumerate(volumes_list_for_layers):
gmsh.model.addPhysicalGroup(3, volume_i, i)
for k, BHE_group_k in enumerate(BHE_group, start=i + 1):
gmsh.model.addPhysicalGroup(1, [BHE_group_k], k)
gmsh.model.addPhysicalGroup(2, top_surface, k + 1, name="Top_Surface")
gmsh.model.addPhysicalGroup(
2,
np.array(surface_list)[:, 1].tolist(),
k + 2,
name="Bottom_Surface",
)
gw_counter = 0 # counter_for_gw_start_at_soil_layer
for i, groundwater in enumerate(groundwater_list):
# add loop for different groundwater flow directions
offset = np.abs(groundwater[2]) in np.cumsum(layer)
start_id = groundwater[0] + i + int(not offset) - gw_counter
end_id = groundwater[3] + i + int(not offset) - gw_counter
if offset:
gw_counter += 1
gmsh.model.addPhysicalGroup(
2,
bounds_gw[groundwater[4]][start_id:end_id],
tag=k + 3,
name=f"Groundwater_Inflow_{i}",
)
k += 1
layer = layer if isinstance(layer, list) else [layer]
groundwaters: list[Groundwater] = (
[groundwater] if isinstance(groundwater, Groundwater) else groundwater
)
BHE_Array = [BHE_Array] if isinstance(BHE_Array, BHE) else BHE_Array
# detect the soil layer, in which the groundwater flow starts
groundwater_list: list = []
for groundwater in groundwaters:
start_groundwater = -1000
# Index for critical layer structure, 0: not critical, 1: top critical,
# 2: bottom critical, 3: groundwater at layer transition
icl: float = -1
# needed_medias_in_ogs=len(layer)+1
for i, _ in enumerate(layer):
if (
np.abs(groundwater.begin) < np.sum(layer[: i + 1])
and start_groundwater == -1000
):
start_groundwater = i
if ( # previous elif, one semantic block of different cases -> switch to if, because of ruff error
np.abs(groundwater.begin)
- np.sum(layer[:start_groundwater])
< n_refinement_layers * target_z_size_fine
):
# print('difficult meshing at the top of the soil layer - GW')
icl = 1
# beginning of groundwater at a transition of two soil layers - special case
if np.abs(groundwater.begin) == np.sum(
layer[:start_groundwater]
):
icl = 3
# needed_medias_in_ogs=len(layer)
# needed_extrusions=len(layer)
elif (
np.sum(layer[: start_groundwater + 1])
- np.abs(groundwater.begin)
< n_refinement_layers * target_z_size_fine
):
# for layers, which are top and bottom critical
icl = 1.2
elif (
np.sum(layer[: start_groundwater + 1])
- np.abs(groundwater.begin)
< n_refinement_layers * target_z_size_fine
and icl != 1.2
):
# print('critical at the bottom of the soil layer-GW')
icl = 2
else:
icl = 0
groundwater_list.append(
[
start_groundwater,
icl,
groundwater.begin,
groundwater.isolation_layer_id,
groundwater.flow_direction,
]
)
### Start of the algorithm ###
BHE_array = np.asarray(BHE_Array)
i = 0
# [:,0] - index of BHE; Array, in welcher Schicht die jeweilige BHE anfängt [:,1] endet [:,3] und wo ein kritischer Übergang folgt [:,2] für BHE_Begin und [:,4] für BHE_End mit 0 - not critical, 1 - top critical, 2 - bottom critical, 3 - bhe at layer transition
BHE_to_soil = np.zeros(shape=(len(BHE_array), 5), dtype=np.int8)
# detect the soil layer, in which the BHE ends
for j, BHE_j in enumerate(BHE_array):
if BHE_j.z_end >= BHE_j.z_begin: # pragma: no cover
msg = f"BHE end depth must be smaller than BHE begin depth for BHE {j}"
raise ValueError(msg)
if BHE_j.z_begin > 0: # pragma: no cover
msg = "BHE begin depth must be zero or negative for BHE {j}"
raise ValueError(msg)
for i, _ in enumerate(layer):
# detect the soil layer, in which the BHE starts - for the moment only for detection
if np.abs(BHE_j.z_begin) < np.sum(layer[: i + 1]) and np.abs(
BHE_j.z_begin
) >= np.sum(layer[:i]):
BHE_to_soil[j, 0] = j
BHE_to_soil[j, 1] = i
if (
np.abs(BHE_j.z_end) - np.abs(BHE_j.z_begin)
<= n_refinement_layers * target_z_size_fine
): # pragma: no cover
msg = "BHE to short, must be longer than n_refinement_layers * target_z_size_fine!"
raise ValueError(msg)
if ( # previous elif, one semantic block of different cases -> switch to if, because of ruff error
np.abs(BHE_j.z_begin) - np.sum(layer[:i])
< n_refinement_layers * target_z_size_fine
):
# print('difficult meshing at the top of the soil layer - BHE %d'%j)
# beginning a transition of two soil layers - special case
BHE_to_soil[j, 2] = 1
if np.abs(BHE_j.z_begin) == np.sum(layer[:i]):
BHE_to_soil[j, 2] = 3
elif (
np.sum(layer[: i + 1]) - np.abs(BHE_j.z_begin)
< n_refinement_layers * target_z_size_fine
):
# print('critical at the bottom of the soil layer - BHE %d'%j)
BHE_to_soil[j, 2] = 2
else:
BHE_to_soil[j, 2] = 0
# detect the soil layer, in which the BHE ends
if np.abs(BHE_j.z_end) < np.sum(layer[: i + 1]) and np.abs(
BHE_j.z_end
) >= np.sum(layer[:i]):
BHE_to_soil[j, 3] = i
if (
np.abs(BHE_j.z_end) - np.sum(layer[:i])
< n_refinement_layers * target_z_size_fine
):
# print('difficult meshing at the top of the soil layer - BHE %d'%j)
BHE_to_soil[j, 4] = 1
# beginning at a transition of two soil layers - special case
if np.abs(BHE_j.z_end) == np.sum(layer[:i]):
BHE_to_soil[j, 4] = 3
elif (
np.sum(layer[: i + 1]) - np.abs(BHE_j.z_end)
< n_refinement_layers * target_z_size_fine
):
# for layers, which are top and bottom critical
BHE_to_soil[j, 4] = 1.2
elif (
np.sum(layer[: i + 1]) - np.abs(BHE_j.z_end)
< n_refinement_layers * target_z_size_fine
):
# print('critical at the bottom of the soil layer - BHE %d'%j)
BHE_to_soil[j, 4] = 2
else:
BHE_to_soil[j, 4] = 0
elif np.abs(BHE_j.z_end) >= np.sum(layer): # pragma: no cover
msg = f"BHE {j} ends at bottom boundary or outside of the model"
raise ValueError(msg)
needed_depths: list = [] # interesting depths
for i, _ in enumerate(layer):
# only the interesting depths in the i-th layer
# TODO: Rename the variable
BHE_end_depths = []
# filter, which BHE's ends in this layer
BHE_end_in_Layer = BHE_to_soil[BHE_to_soil[:, 3] == i]
for k in BHE_end_in_Layer[:, 0]:
BHE_end_depths.append([BHE_array[k].z_end, BHE_to_soil[k, 4]])
# filter, which BHE's starts in this layer
BHE_starts_in_Layer = BHE_to_soil[BHE_to_soil[:, 1] == i]
for k in BHE_starts_in_Layer[:, 0]:
BHE_end_depths.append([BHE_array[k].z_begin, BHE_to_soil[k, 2]])
groundwater_list_0 = np.array(
[inner_list[0] for inner_list in groundwater_list]
)
if i in groundwater_list_0:
if len(np.argwhere(groundwater_list_0 == i)) == 1:
BHE_end_depths.append(
[
groundwaters[
np.argwhere(groundwater_list_0 == i)[0, 0]
].begin,
icl,
]
)
else: # pragma: no cover
msg = "Two or more groundwater flows starts in the same soil layer, this is not allowed !"
raise Exception(msg)
# print(BHE_end_depths)
groundwater_list_3 = np.array(
[inner_list[3] for inner_list in groundwater_list]
)
if i in groundwater_list_3:
BHE_end_depths.append([-np.sum(layer[:i]), 3])
BHE_end_depths.append([-np.sum(layer[:i]), 0])
BHE_end_depths.append([-np.sum(layer[: i + 1]), 0])
depths = np.unique(BHE_end_depths, axis=0) # [::-1]
test, counts = np.unique(depths[:, 0], return_counts=True)
arg_indexes = np.argwhere(counts > 1)
if arg_indexes.size != 0:
new_test = depths[depths[:, 0] == test[arg_indexes[0]]][:, 1]
if np.argwhere(new_test > 0) == 1:
duplicate_depth = np.argwhere(
depths[:, 0] == test[arg_indexes[0]]
)
not_needed_icl = np.argwhere(depths[:, 1] == 0)
depths = np.delete(
depths,
np.intersect1d(duplicate_depth, not_needed_icl),
axis=0,
)
else: # pragma: no cover
msg = "Layering to difficult, groundwater, BHE depths and needed layers are very close - behaviour currently not implemented"
raise Exception(msg)
BHE_end_depths = depths[::-1]
needed_depths.append(BHE_end_depths)
number_of_layers: list = []
cummulative_height_of_layers: list = []
depth_of_extrusion: list = []
for i, _ in enumerate(layer): # Schleife zum Berechnen der Layer-Struktur
# all depths, which needs a node in the mesh
list_of_needed_depths = needed_depths[i]
# vorheriger_layer - Abstand für top-critical etc.
if i > 0:
if (
2 in needed_depths[i - 1][:, 1]
or 1.2 in needed_depths[i - 1][:, 1]
):
space_last_layer_refined = (
needed_depths[i - 1][-1, 0]
- needed_depths[i - 1][-2, 0]
+ n_refinement_layers * target_z_size_fine
)
if space_last_layer_refined < target_z_size_fine:
space_last_layer_refined = target_z_size_fine
else:
space_last_layer_refined = 0
else:
space_last_layer_refined = 0
# nächster_Layer - Abstand für top-critical etc.
if i < len(layer) - 1:
if (
1 in needed_depths[i + 1][:, 1]
or 3 in needed_depths[i + 1][:, 1]
or 1.2 in needed_depths[i + 1][:, 1]
):
if 3 in needed_depths[i + 1][:, 1]:
space_next_layer_refined = (
n_refinement_layers * target_z_size_fine
)
else:
space_next_layer_refined = (
needed_depths[i + 1][1, 0]
- needed_depths[i + 1][0, 0]
+ n_refinement_layers * target_z_size_fine
)
if space_next_layer_refined < target_z_size_fine:
space_next_layer_refined = target_z_size_fine
else:
space_next_layer_refined = 0 # nichts beim layering zu beachten
elif i + 1 in groundwater_list_3: # i+1 == groundwater[1]:
space_next_layer_refined = (
n_refinement_layers * target_z_size_fine
) # if, groundwater isolator is deeper than the model area
else:
space_next_layer_refined = 0
absolute_height_of_layers: list = []
number_of_layers.append([])
# Evaluate Mesh for the Soil-Layers
for j in range(
1, len(list_of_needed_depths)
): # not sure, hope to run up to the bounds of the layer
groundwater_list_2 = np.array(
[inner_list[2] for inner_list in groundwater_list]
)
if (
list_of_needed_depths[j, 0] in groundwater_list_2
and list_of_needed_depths[j, 0] != list_of_needed_depths[-1, 0]
):
_compute_layer_spacing(
list_of_needed_depths[j - 1, 0],
list_of_needed_depths[j, 0],
list_of_needed_depths[j - 1, 1],
j,
len(list_of_needed_depths) - 1,
)
# Befehle zum Anlegen einer neuen Extrusion
cummulative_height_of_layers.append(
(
np.cumsum(
np.array(absolute_height_of_layers)
/ np.sum(absolute_height_of_layers)
)
).tolist()
)
depth_of_extrusion.append(np.sum(absolute_height_of_layers))
absolute_height_of_layers = []
number_of_layers.append([])
else:
_compute_layer_spacing(
list_of_needed_depths[j - 1, 0],
list_of_needed_depths[j, 0],
list_of_needed_depths[j - 1, 1],
j,
len(list_of_needed_depths) - 1,
)
# print('no fine mesh at layer transition')
cummulative_height_of_layers.append(
(
np.cumsum(
np.array(absolute_height_of_layers)
/ np.sum(absolute_height_of_layers)
)
).tolist()
)
depth_of_extrusion.append(np.sum(absolute_height_of_layers))
# evaluate all extrusion depths with according num of layers
All_extrusion_depths: list = []
All_extrusion_layers: list = []
last_height = 0
for i, num_layer in enumerate(number_of_layers):
All_extrusion_depths.append(
(
depth_of_extrusion[i]
* np.array(cummulative_height_of_layers[i])
+ last_height
).tolist()
)
last_height = All_extrusion_depths[-1][-1]
All_extrusion_layers.append(num_layer)
all_extrusion = np.array(
[
_flatten_concatenation(All_extrusion_depths),
_flatten_concatenation(All_extrusion_layers),
]
).transpose()
BHE_extrusion_layers: list = []
BHE_extrusion_depths: list = []
# evaluate the extrusion for the BHE's
for BHE_i in BHE_array:
# add little relax tolerance 0.001
needed_extrusion = all_extrusion[
(
(all_extrusion[:, 0] >= np.abs(BHE_i.z_begin))
& (all_extrusion[:, 0] <= np.abs(BHE_i.z_end) + 0.001)
)
]
BHE_extrusion_layers.append(needed_extrusion[:, 1])
BHE_extrusion_depths.append(
(needed_extrusion[:, 0] - np.abs(BHE_i.z_begin))
/ (needed_extrusion[-1, 0] - np.abs(BHE_i.z_begin))
)
# define the inner square with BHE inside
# compute the box size from the BHE-Coordinates
x_BHE = [BHE_i.x for BHE_i in BHE_array]
y_BHE = [BHE_i.y for BHE_i in BHE_array]
x_min = np.min(x_BHE) - dist_box_x
x_max = np.max(x_BHE) + dist_box_x
y_min = np.min(y_BHE) - dist_box_y
y_max = np.max(y_BHE) + dist_box_y
outer_mesh_size_inner = (outer_mesh_size + inner_mesh_size) / 2
gmsh.initialize()
gmsh.option.setNumber("General.Verbosity", 2)
gmsh.model.add(Path(out_name).stem)
if meshing_type == "structured":
_mesh_structured()
elif meshing_type == "prism":
_mesh_prism()
else: # pragma: no cover
gmsh.finalize()
msg = "Unknown meshing type! prism and structured supported"
raise Exception(msg)
gmsh.model.mesh.generate(3)
gmsh.option.setNumber("Mesh.SecondOrderIncomplete", 1)
gmsh.model.mesh.setOrder(order)
gmsh.model.mesh.removeDuplicateNodes()
# delete zero-volume elements
# 1 for line elements --> BHE's are the reason
elem_tags, node_tags = gmsh.model.mesh.getElementsByType(1)
elem_qualities = gmsh.model.mesh.getElementQualities(
elementTags=elem_tags, qualityName="volume"
)
zero_volume_elements_id = np.argwhere(elem_qualities == 0)
# only possible with the hack over the visibilitiy, see https://gitlab.onelab.info/gmsh/gmsh/-/issues/2006
gmsh.model.mesh.setVisibility(
elem_tags[zero_volume_elements_id].ravel().tolist(), 0
)
gmsh.plugin.setNumber("Invisible", "DeleteElements", 1)
gmsh.plugin.run("Invisible")
gmsh.write(str(out_name))
gmsh.finalize()
[docs]
def gen_bhe_mesh(
length: float, # e.g. 150.0
width: float, # e.g. 100
layer: float | list[float], # e.g. 100
groundwater: Groundwater | list[Groundwater],
BHE_Array: BHE | list[BHE],
target_z_size_coarse: float = 7.5,
target_z_size_fine: float = 1.5,
n_refinement_layers: int = 2,
meshing_type: str = "structured",
dist_box_x: float = 5.0,
dist_box_y: float = 10.0,
inner_mesh_size: float = 5.0,
outer_mesh_size: float = 10.0,
propagation: float = 1.1,
order: int = 1,
out_name: Path = Path("bhe_mesh.vtu"),
) -> list[str]:
"""
Create a generic BHE mesh for the Heat_Transport_BHE-Process with additionally
submeshes at the top, at the bottom and the groundwater inflow, which is exported
in the OGS readable .vtu format. Refinement layers are placed at the BHE-begin, the BHE-end and the groundwater start/end. See detailed description of the parameters below:
:param length: Length of the model area in m (x-dimension)
:param width: Width of the model area in m (y-dimension)
:param layer: List of the soil layer thickness in m
:param groundwater: List of groundwater layers, where every is specified by a tuple
of three entries: [depth of groundwater begin (negative), number of the groundwater
isolation layer (count starts with 0), groundwater inflow direction, as string - supported '+x', '-x', '-y', '+y'], empty list [] for no groundwater flow
:param BHE_Array: List of BHEs, where every BHE is specified by a tuple of five floats:
[x-coordinate BHE, y-coordinate BHE, BHE begin depth (zero or negative),
BHE end depth (negative), borehole radius in m]
:param target_z_size_coarse: maximum edge length of the elements in m in z-direction,
if no refinemnt needed
:param target_z_size_fine: maximum edge length of the elements in the refinement zone
in m in z-direction
:param n_refinement_layers: number of refinement layers which are evenly set above and
beneath the refinemnt depths (see general description above)
:param meshing_type: 'structured' and 'prism' are supported
:param dist_box_x: distance in m in x-direction of the refinemnt box according to the BHE's
:param dist_box_y: distance in m in y-direction of the refinemnt box according to the BHE's
:param inner_mesh_size: mesh size inside the refinement box in m
:param outer_mesh_size: mesh size outside of the refinement box in m
:param propagation: growth of the outer_mesh_size, only supported by meshing_type
'structured'
:param order: Define the order of the mesh: 1 for linear finite elements / 2 for quadratic finite elements
:param out_name: name of the exported mesh, must end with .vtu
:returns: list of filenames of the created vtu mesh files
"""
tmp_dir = Path(mkdtemp())
mesh_name = out_name.stem
msh_file = tmp_dir / f"{mesh_name}.msh"
# using gen_bhe_mesh_gmsh as basis function
gen_bhe_mesh_gmsh(
length=length,
width=width,
layer=layer,
groundwater=groundwater,
BHE_Array=BHE_Array,
target_z_size_coarse=target_z_size_coarse,
target_z_size_fine=target_z_size_fine,
n_refinement_layers=n_refinement_layers,
meshing_type=meshing_type,
dist_box_x=dist_box_x,
dist_box_y=dist_box_y,
inner_mesh_size=inner_mesh_size,
outer_mesh_size=outer_mesh_size,
propagation=propagation,
order=order,
out_name=msh_file,
)
meshes = meshes_from_gmsh(msh_file, dim=[1, 3], log=False)
for name, mesh in meshes.items():
pv.save_meshio(Path(out_name.parents[0], name + ".vtu"), mesh)
mesh_names = [
"domain.vtu",
"physical_group_Top_Surface.vtu",
"physical_group_Bottom_Surface.vtu",
]
groundwater = (
[groundwater] if isinstance(groundwater, Groundwater) else groundwater
)
for i, _ in enumerate(groundwater):
mesh_names.append(f"physical_group_Groundwater_Inflow_{i}.vtu")
return mesh_names