Skip to content

Commit 61fe5ac

Browse files
Merge branch 'develop' into fix_tests_in_vscode
2 parents 7630156 + 1624e32 commit 61fe5ac

13 files changed

Lines changed: 354 additions & 160 deletions

src/porepy/examples/geothermal_reservoir.py

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,14 @@ def bc_values_pressure(self, bg: pp.BoundaryGrid) -> np.ndarray:
8181
if self.is_well_grid(sd):
8282
well = self.well_network.wells[sd.tags["parent_well_index"]]
8383
well_tag = well.tags["well_name"]
84-
protocol = self.well_protocols()[well_tag]
8584
# Find indices of the well boundary sides.
8685
domain_sides = self.domain_boundary_sides(bg)
8786
# The top of the domain is '.top' in 3d, '.north' in 2d.
8887
inds = domain_sides.top if self.nd == 3 else domain_sides.north
8988
# Set pressure values according to the well protocol.
9089
values[inds] = self.units.convert_units(
9190
self.get_well_value(
92-
protocol["pressures"],
91+
self.well_protocols(well_tag, "pressures"),
9392
self.time_manager.schedule,
9493
self.time_manager.time,
9594
),
@@ -109,16 +108,14 @@ def bc_values_temperature(self, bg: pp.BoundaryGrid) -> np.ndarray:
109108
sd = bg.parent
110109
values = super().bc_values_temperature(bg) # type: ignore[misc]
111110
if self.is_well_grid(sd):
112-
# Retrieve well protocol.
113111
well_tag = self.well_names[sd.tags["parent_well_index"]]
114-
protocol = self.well_protocols()[well_tag]
115112
# Find indices of the well boundary sides.
116113
domain_sides = self.domain_boundary_sides(bg)
117114
inds = domain_sides.top if self.nd == 3 else domain_sides.north
118115
# Set temperature values according to the well protocol.
119116
values[inds] = self.units.convert_units(
120117
self.get_well_value(
121-
protocol["temperatures"],
118+
self.well_protocols(well_tag, "temperatures"),
122119
self.time_manager.schedule,
123120
self.time_manager.time,
124121
),
@@ -153,45 +150,32 @@ def get_well_value(
153150
else:
154151
return float(np.interp(current_time, times, values))
155152

156-
def well_protocols(self) -> dict[str, dict[str, NDArray[np.float64]]]:
157-
"""Dictionary mapping well tags to well protocols.
153+
def well_protocols(self, well_tag: str, variable: str) -> NDArray[np.float64]:
154+
"""Return the time-dependent protocol array for a given well and variable.
155+
156+
The value is read from ``self.params`` under the key
157+
``"{well_tag}_{variable}"``. A scalar is broadcast to all schedule times; an
158+
array must match schedule length.
159+
160+
Parameters:
161+
well_tag: Name of the well (e.g. ``"injection_well"``).
162+
variable: Protocol variable name (e.g. ``"pressures"``, ``"temperatures"``,
163+
``"mass_rates"``).
158164
159165
Returns:
160-
Dictionary with well protocols, each containing a dictionary with
161-
time-dependent temperatures and pressures, with each value being an array of
162-
size equal to the number of scheduled times in the time manager.
166+
Array of protocol values, one entry per scheduled time point.
163167
"""
164168
num_times = self.time_manager.schedule.size
165-
protocols: dict[str, dict[str, NDArray[np.float64]]] = {}
166-
# Construct protocols for each well.
167-
for well_tag in self.well_names:
168-
# Initialize protocol dictionary for the well.
169-
protocols[well_tag] = {}
170-
# Set values for temperatures and pressures.
171-
for variable in ["temperatures", "pressures"]:
172-
input_values = self.params.get(f"{well_tag}_{variable}", 0.0)
173-
if isinstance(input_values, (float, int)):
174-
# Broadcast single value to all time steps for convenient user
175-
# definition of well protocols.
176-
values = np.full(num_times, input_values, dtype=float)
177-
178-
elif isinstance(input_values, (list, np.ndarray)):
179-
# Enforce array of float values.
180-
values = np.array(input_values, dtype=float)
181-
if values.size != num_times:
182-
raise ValueError(
183-
f"Well protocol for {well_tag} {variable} has size "
184-
f"{values.size}, expected {num_times}."
185-
)
186-
else:
187-
raise TypeError(
188-
f"Well protocol for {well_tag} {variable} has unsupported "
189-
f"type {type(input_values)}."
190-
)
191-
# Populate well dictionary for the current variable.
192-
protocols[well_tag][variable] = values
193-
194-
return protocols
169+
raw = self.params.get(f"{well_tag}_{variable}", 0.0)
170+
if isinstance(raw, (int, float)):
171+
return np.full(num_times, float(raw))
172+
values = np.asarray(raw, dtype=float)
173+
if values.size != num_times:
174+
raise ValueError(
175+
f"Protocol '{well_tag}_{variable}' has {values.size} entries, "
176+
f"expected {num_times} (one per schedule point)."
177+
)
178+
return values
195179

196180

197181
class NeumannWellBCsFirstTimeInterval(pp.PorePyModel):

src/porepy/fracs/elliptic_fracture.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,28 @@ class EllipticFracture(Fracture):
2525
The fracture is defined by its center position, major and minor axes, and its
2626
spatial orientation given by three rotation angles in radians.
2727
28+
Coordinate convention:
29+
The global coordinate system is right-handed in 3D with +x = east, +y = north,
30+
and +z = up.
31+
32+
Orientation convention:
33+
The orientation of the fracture is described using a strike-dip representation.
34+
35+
The strike angle is defined as the azimuth, i.e., the angle measured clockwise
36+
from the geographic north (y-axis) in the horizontal plane. This definition is
37+
widely adopted in geosciences, although alternative notation conventions also
38+
exist.
39+
40+
The strike-dip convention follows the right-hand rule in geology: when moving
41+
along the strike direction, the fracture plane dips downward to the right.
42+
43+
References:
44+
- [1] Fossen, H.: Structural Geology (2016).
45+
2846
Example:
29-
Fracture centered at ``[0, 1, 0]``, with a ratio of lengths of 2, rotation in
30-
xy-plane of 45 degrees, and an incline of 30 degrees rotated around the x-axis,
31-
due to the strike angle of 0 radians:
47+
Fracture centered at ``[0, 1, 0]``, with a ratio of lengths of 2, a rotation
48+
in the xy-plane of 45 degrees, and an incline of 30 degrees rotated around the
49+
y-axis (north), due to the strike angle of 0 radians:
3250
3351
3452
>>> import numpy as np
@@ -43,19 +61,16 @@ class EllipticFracture(Fracture):
4361
4462
Parameters:
4563
center: ``shape=(3, 1)``
46-
4764
Center coordinates of fracture.
4865
major_axis: Length of major axis (radius-like, not diameter).
49-
minor_axis: Length of minor axis.
50-
51-
There are no checks on whether the minor axis is less or equal to the major.
66+
minor_axis: Length of minor axis. There are no checks on whether the minor axis
67+
is less than or equal to the major axis.
5268
major_axis_angle: Rotation of the major axis from the x-axis in radians.
53-
Measured before strike-dip rotation, see below.
54-
strike_angle: Line of rotation for the dip. Given as angle in radians from the
55-
x-direction.
69+
Measured before the strike-dip rotation, see below.
70+
strike_angle: Geological strike angle in radians, measured clockwise from the
71+
y-axis (north) in the horizontal plane.
5672
dip_angle: Dip angle in radians, i.e., rotation around the strike direction.
5773
index: ``default=None``
58-
5974
Index to be assigned to the fracture.
6075
6176
"""
@@ -97,9 +112,9 @@ def fracture_to_gmsh(self) -> int:
97112
dimTags, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, self.major_axis_angle
98113
)
99114

100-
# 3) Rotate around the strike direction by the dip angle.
101-
strike_x = math.cos(self.strike_angle)
102-
strike_y = math.sin(self.strike_angle)
115+
# 3) Rotate around the geological strike direction by the dip angle.
116+
strike_x = math.sin(self.strike_angle)
117+
strike_y = math.cos(self.strike_angle)
103118
strike_z = 0.0
104119

105120
gmsh.model.occ.rotate(

src/porepy/fracs/fracture_network.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,16 @@ def _entity_on_domain_boundary(self, target_dim: int, ind: list[int]) -> bool:
349349
# having been split into multiple parts.
350350
domain_entities = gmsh.model.get_entities(self.nd)
351351
boundary_entities = gmsh.model.get_boundary(
352-
[(self.nd, tag) for _, tag in domain_entities]
352+
[(self.nd, tag) for _, tag in domain_entities], oriented=False
353353
)
354354
# Get hold of the boundary points of the entity to check.
355355
if target_dim == 0:
356356
boundary_points = [(target_dim, i) for i in ind]
357357
else:
358358
assert len(ind) == 1, "Only single entity indices are supported."
359-
boundary_points = gmsh.model.get_boundary([(target_dim, ind[0])])
359+
boundary_points = gmsh.model.get_boundary(
360+
[(target_dim, ind[0])], oriented=False
361+
)
360362

361363
# For each boundary surface of the domain, compute the distance between the
362364
# entity and all boundary points to check if they are all zero.
@@ -405,7 +407,7 @@ def _insert_mesh_size_control_points(
405407
### Get hold of entities representing fractures and boundaries.
406408
domain_entities = gmsh.model.get_entities(self.nd)
407409
boundaries = gmsh.model.get_boundary(
408-
[(self.nd, tag) for _, tag in domain_entities]
410+
[(self.nd, tag) for _, tag in domain_entities], oriented=False
409411
)
410412
fractures = [
411413
f for f in gmsh.model.get_entities(self.nd - 1) if f not in boundaries
@@ -433,7 +435,9 @@ def _insert_mesh_size_control_points(
433435
# Take note of the boundary points of all entities, to avoid inserting points
434436
# there (doing so may confuse Gmsh).
435437
for ent in entities:
436-
bp = gmsh.model.get_boundary([(self.nd - 1, ent)], recursive=True)
438+
bp = gmsh.model.get_boundary(
439+
[(self.nd - 1, ent)], recursive=True, oriented=False
440+
)
437441
for b in bp:
438442
if b[0] != 0:
439443
continue

src/porepy/fracs/fracture_network_2d.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,10 @@ def _impose_boundary_process_intersections(
384384
boundary_tags = []
385385
else:
386386
boundary_tags = [
387-
t for _, t in gmsh.model.get_boundary([(self.nd, domain_tag)])
387+
t
388+
for _, t in gmsh.model.get_boundary(
389+
[(self.nd, domain_tag)], oriented=False
390+
)
388391
]
389392

390393
# Mapping from the new fracture tags (gmsh assigned) to the input fractures.
@@ -441,7 +444,7 @@ def _impose_boundary_process_intersections(
441444
updated_mesh_size_points[segment[1]] = mesh_size_points[
442445
old_gmsh_tag
443446
]
444-
pt_index = gmsh.model.get_boundary([segment])
447+
pt_index = gmsh.model.get_boundary([segment], oriented=False)
445448

446449
if fi not in constraints:
447450
# If this is not a constraint, collect the boundary points for
@@ -507,7 +510,7 @@ def _set_mesh_size_fields(
507510
### Get hold of lines representing fractures and boundaries.
508511
domain_entities = gmsh.model.get_entities(2)
509512
boundaries = gmsh.model.get_boundary(
510-
[(self.nd, tag) for _, tag in domain_entities]
513+
[(self.nd, tag) for _, tag in domain_entities], oriented=False
511514
)
512515

513516
line_tags = set(tag for _, tag in gmsh.model.getEntities(self.nd - 1))
@@ -540,7 +543,9 @@ def _set_mesh_size_fields(
540543
end_points = np.array(
541544
[
542545
gmsh.model.occ.get_bounding_box(0, p[1])[:3]
543-
for p in gmsh.model.get_boundary([(1, line)], combined=False)
546+
for p in gmsh.model.get_boundary(
547+
[(1, line)], combined=False, oriented=False
548+
)
544549
]
545550
).T
546551
length = np.linalg.norm(end_points[:, 1] - end_points[:, 0])

src/porepy/fracs/fracture_network_3d.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,10 @@ def _impose_boundary_process_intersections(
419419
num_orig_subfrac.append(len(frac))
420420

421421
for sfi, sub_frac in enumerate(frac):
422-
bounding_lines = gmsh.model.get_boundary([sub_frac])
422+
bounding_lines = gmsh.model.get_boundary([sub_frac], oriented=False)
423423
bounding_points = []
424424
for line in bounding_lines:
425-
bounding_points += gmsh.model.get_boundary([line])
425+
bounding_points += gmsh.model.get_boundary([line], oriented=False)
426426

427427
if len(bounding_points) == 0:
428428
# This is most likely a disc fracture, which has no bounding
@@ -636,7 +636,7 @@ def _impose_boundary_process_intersections(
636636
# At most one of the parents was not a constraint. This line should not
637637
# produce a point.
638638
continue
639-
for bp in gmsh.model.get_boundary([(1, line)]):
639+
for bp in gmsh.model.get_boundary([(1, line)], oriented=False):
640640
points_of_intersection_lines.append(bp[1])
641641

642642
num_point_occ = np.bincount(points_of_intersection_lines)
@@ -736,7 +736,9 @@ def _set_mesh_size_fields(
736736
### Get hold of lines representing fractures and boundaries.
737737
domain_entities = gmsh.model.get_entities(nd)
738738
# Get the boundaries.
739-
boundaries = gmsh.model.get_boundary([(nd, tag) for _, tag in domain_entities])
739+
boundaries = gmsh.model.get_boundary(
740+
[(nd, tag) for _, tag in domain_entities], oriented=False
741+
)
740742

741743
surface_tags = set(tag for _, tag in gmsh.model.get_entities(nd - 1))
742744
boundary_tags = set(tag for _, tag in boundaries)

0 commit comments

Comments
 (0)