Skip to content

Commit 86209cb

Browse files
authored
Merge pull request #244 from MiraGeoscience/GEOPY-1302
GEOPY-1302: Organize geoapps.utils.testing.setup_inversion_workspace
2 parents 170f3cc + 751912a commit 86209cb

60 files changed

Lines changed: 2741 additions & 1959 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ repos:
9191
- id: mixed-line-ending
9292
exclude: ^\.idea/.*\.xml$
9393
- id: name-tests-test
94-
exclude: testing_utils.py
94+
exclude: targets.py
9595
- id: pretty-format-json
9696
args:
9797
- --autofix
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
10+
11+
import numpy as np
12+
from geoh5py import Workspace
13+
from geoh5py.data import FloatData
14+
from geoh5py.objects import DrapeModel, ObjectBase, Octree, Surface
15+
16+
from simpeg_drivers.utils.synthetics.meshes.factory import get_mesh
17+
from simpeg_drivers.utils.synthetics.models import get_model
18+
from simpeg_drivers.utils.synthetics.options import SyntheticsComponentsOptions
19+
from simpeg_drivers.utils.synthetics.surveys.factory import get_survey
20+
from simpeg_drivers.utils.synthetics.topography import (
21+
get_active,
22+
get_topography_surface,
23+
)
24+
25+
26+
class SyntheticsComponents:
27+
"""Creates a workspace populated with objects for simulation and subsequent inversion."""
28+
29+
def __init__(
30+
self,
31+
geoh5: Workspace,
32+
options: SyntheticsComponentsOptions | None = None,
33+
):
34+
if options is None:
35+
options = SyntheticsComponentsOptions()
36+
37+
self.geoh5 = geoh5
38+
self.options = options
39+
self._topography: Surface | None = None
40+
self._survey: ObjectBase | None = None
41+
self._mesh: Octree | DrapeModel | None = None
42+
self._active: FloatData | None = None
43+
self._model: FloatData | None = None
44+
45+
@property
46+
def topography(self):
47+
entity = self.geoh5.get_entity("topography")[0]
48+
assert isinstance(entity, Surface | type(None))
49+
if entity is None:
50+
assert self.options is not None
51+
entity = get_topography_surface(
52+
geoh5=self.geoh5,
53+
options=self.options.survey,
54+
)
55+
self._topography = entity
56+
return self._topography
57+
58+
@property
59+
def survey(self):
60+
entity = self.geoh5.get_entity(self.options.survey.name)[0]
61+
assert isinstance(entity, ObjectBase | type(None))
62+
if entity is None:
63+
assert self.options is not None
64+
entity = get_survey(
65+
geoh5=self.geoh5,
66+
method=self.options.method,
67+
options=self.options.survey,
68+
)
69+
self._survey = entity
70+
return self._survey
71+
72+
@property
73+
def mesh(self):
74+
entity = self.geoh5.get_entity(self.options.mesh.name)[0]
75+
assert isinstance(entity, Octree | DrapeModel | type(None))
76+
if entity is None:
77+
assert self.options is not None
78+
entity = get_mesh(
79+
self.options.method,
80+
survey=self.survey,
81+
topography=self.topography,
82+
options=self.options.mesh,
83+
)
84+
self._mesh = entity
85+
return self._mesh
86+
87+
@property
88+
def active(self):
89+
entity = self.geoh5.get_entity(self.options.active.name)[0]
90+
assert isinstance(entity, FloatData | type(None))
91+
if entity is None:
92+
entity = get_active(self.mesh, self.topography)
93+
self._active = entity
94+
return self._active
95+
96+
@property
97+
def model(self):
98+
entity = self.geoh5.get_entity(self.options.model.name)[0]
99+
assert isinstance(entity, FloatData | type(None))
100+
if entity is None:
101+
assert self.options is not None
102+
entity = get_model(
103+
method=self.options.method,
104+
mesh=self.mesh,
105+
active=self.active.values,
106+
options=self.options.model,
107+
)
108+
self._model = entity
109+
return self._model
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
10+
11+
from discretize import TensorMesh, TreeMesh
12+
from geoh5py.objects import CellObject, DrapeModel, Octree, Points, Surface
13+
14+
from simpeg_drivers.utils.synthetics.options import MeshOptions
15+
16+
from .octrees import get_octree_mesh
17+
from .tensors import get_tensor_mesh
18+
19+
20+
def get_mesh(
21+
method: str,
22+
survey: Points,
23+
topography: Surface,
24+
options: MeshOptions,
25+
) -> DrapeModel | Octree:
26+
"""Factory for mesh creation with behaviour modified by the provided method."""
27+
28+
if "2d" in method:
29+
assert isinstance(survey, CellObject)
30+
return get_tensor_mesh(
31+
survey=survey,
32+
cell_size=options.cell_size,
33+
padding_distance=options.padding_distance,
34+
name=options.name,
35+
)
36+
37+
return get_octree_mesh(
38+
survey=survey,
39+
topography=topography,
40+
cell_size=options.cell_size,
41+
refinement=options.refinement,
42+
padding_distance=options.padding_distance,
43+
refine_on_receivers=method in ["fdem", "airborne tdem"],
44+
name=options.name,
45+
)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
10+
11+
import numpy as np
12+
from discretize import TreeMesh
13+
from discretize.utils import mesh_builder_xyz
14+
from geoh5py.objects import Octree, Points, Surface
15+
from octree_creation_app.driver import OctreeDriver
16+
from octree_creation_app.utils import treemesh_2_octree
17+
18+
19+
def get_base_octree(
20+
survey: Points,
21+
topography: Surface,
22+
cell_size: tuple[float, float, float],
23+
refinement: tuple,
24+
padding: float,
25+
) -> TreeMesh:
26+
"""
27+
Generate a survey centered TreeMesh object with topography refinement.
28+
29+
:param survey: Survey object with vertices that define the core of the
30+
tensor mesh.
31+
:param topography: Surface used to refine the topography.
32+
:param cell_size: Tuple defining the cell size in all directions.
33+
:param refinement: Tuple containing the number of cells to refine at each
34+
level around the topography.
35+
:param padding: Distance to pad the mesh in all directions.
36+
37+
:return mesh: The discretize TreeMesh object for computations.
38+
"""
39+
padding_distance = np.ones((3, 2)) * padding
40+
mesh = mesh_builder_xyz(
41+
survey.vertices - np.r_[cell_size] / 2.0,
42+
cell_size,
43+
depth_core=100.0,
44+
padding_distance=padding_distance,
45+
mesh_type="TREE",
46+
tree_diagonal_balance=False,
47+
)
48+
mesh = OctreeDriver.refine_tree_from_surface(
49+
mesh, topography, levels=refinement, finalize=False
50+
)
51+
52+
return mesh
53+
54+
55+
def get_octree_mesh(
56+
survey: Points,
57+
topography: Surface,
58+
cell_size: tuple[float, float, float],
59+
refinement: tuple,
60+
padding_distance: float,
61+
refine_on_receivers: bool,
62+
name: str = "mesh",
63+
) -> Octree:
64+
"""Generate a survey centered mesh with topography and survey refinement.
65+
66+
:param survey: Survey object with vertices that define the core of the
67+
tensor mesh and the source refinement for EM methods.
68+
:param topography: Surface used to refine the topography.
69+
:param cell_size: Tuple defining the cell size in all directions.
70+
:param refinement: Tuple containing the number of cells to refine at each
71+
level around the topography.
72+
:param padding: Distance to pad the mesh in all directions.
73+
:param refine_on_receivers: Refine on the survey locations or not.
74+
75+
:return entity: The geoh5py Octree object to store the results of
76+
computation in the shared cells of the computational mesh.
77+
:return mesh: The discretize TreeMesh object for computations.
78+
"""
79+
80+
mesh = get_base_octree(survey, topography, cell_size, refinement, padding_distance)
81+
82+
if refine_on_receivers:
83+
mesh = OctreeDriver.refine_tree_from_points(
84+
mesh, survey.vertices, levels=[2], finalize=False
85+
)
86+
87+
mesh.finalize()
88+
entity = treemesh_2_octree(survey.workspace, mesh, name=name)
89+
90+
return entity
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
10+
11+
import numpy as np
12+
from geoh5py.data import IntegerData
13+
from geoh5py.objects import CellObject, DrapeModel
14+
15+
from simpeg_drivers.utils.utils import get_drape_model
16+
17+
18+
def get_tensor_mesh(
19+
survey: CellObject,
20+
cell_size: tuple[float, float, float],
21+
padding_distance: float,
22+
line_id: int = 101,
23+
name: str = "mesh",
24+
) -> DrapeModel:
25+
"""
26+
Generate a tensor mesh and the colocated DrapeModel.
27+
28+
:param survey: Survey object with vertices that define the core of the
29+
tensor mesh.
30+
:param cell_size: Tuple defining the cell size in all directions.
31+
:param padding_distance: Distance to pad the mesh in all directions.
32+
:param line_id: Chooses line from the survey to define the drape model.
33+
34+
:return entity: The DrapeModel object that shares cells with the discretize
35+
tensor mesh and which stores the results of computations.
36+
:return mesh: The discretize tensor mesh object for computations.
37+
"""
38+
39+
line_data = survey.get_entity("line_ids")[0]
40+
assert isinstance(line_data, IntegerData)
41+
lines = line_data.values
42+
entity, mesh, _ = get_drape_model( # pylint: disable=unbalanced-tuple-unpacking
43+
survey.workspace,
44+
name,
45+
survey.vertices[np.unique(survey.cells[lines == line_id, :]), :],
46+
[cell_size[0], cell_size[2]],
47+
100.0,
48+
[padding_distance] * 2 + [padding_distance] * 2,
49+
1.1,
50+
parent=None,
51+
return_colocated_mesh=True,
52+
return_sorting=True,
53+
)
54+
55+
return entity
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
2+
# Copyright (c) 2025 Mira Geoscience Ltd. '
3+
# '
4+
# This file is part of simpeg-drivers package. '
5+
# '
6+
# simpeg-drivers is distributed under the terms and conditions of the MIT License '
7+
# (see LICENSE file at the root of this source code package). '
8+
# '
9+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
10+
11+
import numpy as np
12+
from geoapps_utils.modelling.plates import make_plate
13+
from geoh5py.data import FloatData
14+
from geoh5py.objects import DrapeModel, Octree
15+
16+
from simpeg_drivers.utils.synthetics.options import ModelOptions
17+
18+
19+
def get_model(
20+
method: str,
21+
mesh: Octree | DrapeModel,
22+
active: np.ndarray,
23+
options: ModelOptions,
24+
) -> FloatData:
25+
"""
26+
Create a halfspace and plate model in the cell centers of the provided mesh.
27+
28+
:param method: The geophysical method controlling the factory behaviour
29+
:param mesh: The mesh whose cell centers the model will be defined on.
30+
:param plate: The plate object defining the location and orientation of the
31+
plate anomaly.
32+
:param background: Value given to the halfspace.
33+
:param anomaly: Value given to the plate.
34+
"""
35+
36+
cell_centers = mesh.centroids.copy()
37+
38+
model = make_plate(
39+
points=cell_centers,
40+
plate=options.plate,
41+
background=options.background,
42+
anomaly=options.anomaly,
43+
)
44+
45+
if "1d" in method:
46+
model = options.background * np.ones(mesh.n_cells)
47+
inside_anomaly = (mesh.centroids[:, 2] < 0) & (mesh.centroids[:, 2] > -20)
48+
model[inside_anomaly] = options.anomaly
49+
50+
model[~active] = np.nan
51+
model = mesh.add_data({options.name: {"values": model}})
52+
assert isinstance(model, FloatData)
53+
return model

0 commit comments

Comments
 (0)