grid-apps 0.1.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grid_apps/__init__.py +37 -0
- grid_apps/_version.py +2 -0
- grid_apps/block_model_to_octree/__init__.py +8 -0
- grid_apps/block_model_to_octree/driver.py +234 -0
- grid_apps/block_model_to_octree/options.py +60 -0
- grid_apps/block_models/__init__.py +8 -0
- grid_apps/block_models/driver.py +207 -0
- grid_apps/block_models/options.py +104 -0
- grid_apps/driver.py +91 -0
- grid_apps/octree_creation/__init__.py +8 -0
- grid_apps/octree_creation/driver.py +460 -0
- grid_apps/octree_creation/options.py +212 -0
- grid_apps/utils.py +498 -0
- grid_apps-0.1.0b2.dist-info/METADATA +105 -0
- grid_apps-0.1.0b2.dist-info/RECORD +22 -0
- grid_apps-0.1.0b2.dist-info/WHEEL +4 -0
- grid_apps-0.1.0b2.dist-info/licenses/LICENSE +21 -0
- grid_apps-0.1.0b2.dist-info/licenses/docs/source/THIRD_PARTY_SOFTWARE.rst +28 -0
- grid_apps-assets/__init__.py +8 -0
- grid_apps-assets/uijson/block_model_to_octree.ui.json +47 -0
- grid_apps-assets/uijson/block_models.ui.json +87 -0
- grid_apps-assets/uijson/octree_mesh.ui.json +241 -0
grid_apps/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2022-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps 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
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from ._version import __version__
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
__date_str = datetime.today().strftime("%Y%m%d")
|
|
22
|
+
__version__ = "0.0.0.dev0+" + __date_str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logging.basicConfig(level=logging.INFO)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def assets_path() -> Path:
|
|
29
|
+
"""Return the path to the assets folder."""
|
|
30
|
+
|
|
31
|
+
parent = Path(__file__).parent
|
|
32
|
+
folder_name = f"{parent.name}-assets"
|
|
33
|
+
assets_folder = parent.parent / folder_name
|
|
34
|
+
if not assets_folder.is_dir():
|
|
35
|
+
raise RuntimeError(f"Assets folder not found: {assets_folder}")
|
|
36
|
+
|
|
37
|
+
return assets_folder
|
grid_apps/_version.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps is distributed under the terms and conditions of the MIT License '
|
|
7
|
+
# (see LICENSE file at the root of this source code package). '
|
|
8
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps 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
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from discretize import TreeMesh
|
|
18
|
+
from geoh5py.data import FloatData, ReferencedData
|
|
19
|
+
from geoh5py.objects import BlockModel, Octree
|
|
20
|
+
from geoh5py.ui_json.utils import fetch_active_workspace
|
|
21
|
+
from scipy.spatial import cKDTree
|
|
22
|
+
|
|
23
|
+
from grid_apps.block_model_to_octree.options import BlockModel2OctreeOptions
|
|
24
|
+
from grid_apps.driver import BaseGridDriver
|
|
25
|
+
from grid_apps.utils import (
|
|
26
|
+
block_model_to_discretize,
|
|
27
|
+
boundary_value_indices,
|
|
28
|
+
tensor_mesh_ordering,
|
|
29
|
+
treemesh_2_octree,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Driver(BaseGridDriver):
|
|
37
|
+
"""
|
|
38
|
+
Convert a BlockModel object to Octree with various refinement strategies.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_params_class = BlockModel2OctreeOptions
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def block_model_to_treemesh(
|
|
45
|
+
entity: BlockModel, diagonal_balance=True, finalize=True
|
|
46
|
+
) -> TreeMesh:
|
|
47
|
+
"""
|
|
48
|
+
Convert a block model to an octree mesh with the same base cell size and
|
|
49
|
+
centered.
|
|
50
|
+
|
|
51
|
+
:param entity: BlockModel object to be converted
|
|
52
|
+
:param diagonal_balance: Whether to balance the mesh diagonally.
|
|
53
|
+
:param finalize: Whether to finalize the treemesh after creation.
|
|
54
|
+
|
|
55
|
+
:return: TreeMesh object.
|
|
56
|
+
"""
|
|
57
|
+
origin = []
|
|
58
|
+
octree_cells = []
|
|
59
|
+
for ii, ax in zip("xyz", "uvz", strict=True):
|
|
60
|
+
cell_sizes = np.abs(getattr(entity, f"{ax}_cells"))
|
|
61
|
+
h_core = cell_sizes.min()
|
|
62
|
+
|
|
63
|
+
# Compute number of octree cells to span the extent
|
|
64
|
+
n_c = np.ceil(np.log2(np.sum(cell_sizes) / h_core))
|
|
65
|
+
cell_sizes_octree = np.ones(int(2**n_c)) * h_core
|
|
66
|
+
octree_cells.append(cell_sizes_octree)
|
|
67
|
+
|
|
68
|
+
# Colocate the center of the octree with the center of the block model
|
|
69
|
+
ind_core = np.where(cell_sizes == h_core)[0]
|
|
70
|
+
center = (
|
|
71
|
+
entity.origin[ii]
|
|
72
|
+
+ entity.local_axis_centers(ax)[ind_core[len(ind_core) // 2]]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
axis_center = len(cell_sizes_octree) // 2
|
|
76
|
+
origin.append(center - np.sum(cell_sizes_octree[:axis_center]) - h_core / 2)
|
|
77
|
+
|
|
78
|
+
treemesh = TreeMesh(
|
|
79
|
+
octree_cells,
|
|
80
|
+
x0=origin,
|
|
81
|
+
finalize=finalize,
|
|
82
|
+
diagonal_balance=diagonal_balance,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return treemesh
|
|
86
|
+
|
|
87
|
+
def make_grid(self) -> Octree:
|
|
88
|
+
"""
|
|
89
|
+
Convert the block model and output the octree mesh.
|
|
90
|
+
|
|
91
|
+
:return: Octree object refined by the cell volumes or gradient of the data.
|
|
92
|
+
"""
|
|
93
|
+
with fetch_active_workspace(self.params.geoh5, mode="r+"):
|
|
94
|
+
entity = self.params.entity
|
|
95
|
+
|
|
96
|
+
treemesh = Driver.block_model_to_treemesh(entity, finalize=False)
|
|
97
|
+
model = None
|
|
98
|
+
if self.params.data is None:
|
|
99
|
+
treemesh = Driver.refine_by_cell_volumes(
|
|
100
|
+
treemesh, entity, finalize=True
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
treemesh = Driver.refine_by_values(
|
|
104
|
+
treemesh, self.params.data, finalize=True
|
|
105
|
+
)
|
|
106
|
+
# Transfer the model
|
|
107
|
+
ind = treemesh.get_containing_cells(entity.centroids)
|
|
108
|
+
model = (
|
|
109
|
+
np.ones(treemesh.n_cells, dtype=self.params.data.values.dtype)
|
|
110
|
+
* self.params.data.nan_value
|
|
111
|
+
)
|
|
112
|
+
model[ind] = self.params.data.values
|
|
113
|
+
|
|
114
|
+
nan_vals = (model == self.params.data.nan_value) | np.isnan(model)
|
|
115
|
+
if np.any(nan_vals):
|
|
116
|
+
tree = cKDTree(entity.centroids)
|
|
117
|
+
ind = tree.query(treemesh.cell_centers[nan_vals])[1]
|
|
118
|
+
model[nan_vals] = self.params.data.values[ind]
|
|
119
|
+
|
|
120
|
+
octree = treemesh_2_octree(
|
|
121
|
+
self.params.geoh5,
|
|
122
|
+
treemesh,
|
|
123
|
+
parent=self.params.output.out_group,
|
|
124
|
+
name=self.params.output.export_as or entity.name + "_octree",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if model is not None and self.params.data is not None:
|
|
128
|
+
octree.add_data(
|
|
129
|
+
{
|
|
130
|
+
self.params.data.name: {
|
|
131
|
+
"values": model,
|
|
132
|
+
"entity_type": self.params.data.entity_type,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return octree
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def refine_by_cell_volumes(
|
|
141
|
+
mesh: TreeMesh,
|
|
142
|
+
entity: BlockModel,
|
|
143
|
+
finalize: bool = True,
|
|
144
|
+
mask: np.ndarray | None = None,
|
|
145
|
+
) -> TreeMesh:
|
|
146
|
+
"""
|
|
147
|
+
Refine the octree mesh by the cell volumes of the block model.
|
|
148
|
+
|
|
149
|
+
:param mesh: TreeMesh object to be refined.
|
|
150
|
+
:param entity: BlockModel object to be used for refinement.
|
|
151
|
+
:param finalize: Whether to finalize the treemesh after refinement.
|
|
152
|
+
:param mask: Optional mask on the block model centroids to apply the refinement over.
|
|
153
|
+
|
|
154
|
+
:return: TreeMesh object with refined levels.
|
|
155
|
+
"""
|
|
156
|
+
if not isinstance(entity, BlockModel):
|
|
157
|
+
raise TypeError("entity must be an instance of BlockModel.")
|
|
158
|
+
|
|
159
|
+
tensor_oct_level = []
|
|
160
|
+
for ax in "uvz":
|
|
161
|
+
cell_sizes = np.abs(getattr(entity, f"{ax}_cells"))
|
|
162
|
+
h_core = cell_sizes.min()
|
|
163
|
+
# Find the core region
|
|
164
|
+
tensor_oct_level.append(np.log2(cell_sizes / h_core).astype(int))
|
|
165
|
+
|
|
166
|
+
e_x, e_y, e_z = np.meshgrid(*tensor_oct_level)
|
|
167
|
+
max_level = np.c_[np.ravel(e_x), np.ravel(e_y), np.ravel(e_z)].max(axis=1)
|
|
168
|
+
|
|
169
|
+
locations = entity.centroids
|
|
170
|
+
if mask is not None:
|
|
171
|
+
locations = locations[mask]
|
|
172
|
+
max_level = max_level[mask]
|
|
173
|
+
|
|
174
|
+
mesh.insert_cells(locations, mesh.max_level - max_level, finalize=finalize)
|
|
175
|
+
|
|
176
|
+
return mesh
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def refine_by_values(
|
|
180
|
+
mesh: TreeMesh, data: FloatData | ReferencedData, finalize=True
|
|
181
|
+
) -> TreeMesh:
|
|
182
|
+
"""
|
|
183
|
+
Increase the mesh resolution based on the gradient of data values.
|
|
184
|
+
|
|
185
|
+
:param mesh: Input TreeMesh object.
|
|
186
|
+
:param data: FloatData or ReferencedData object containing the values to
|
|
187
|
+
be used for refinement.
|
|
188
|
+
:param finalize: Whether to finalize the treemesh after refinement.
|
|
189
|
+
|
|
190
|
+
:return: TreeMesh object with refined levels.
|
|
191
|
+
"""
|
|
192
|
+
if not isinstance(data, FloatData | ReferencedData):
|
|
193
|
+
raise TypeError(
|
|
194
|
+
"Argument 'data' must be an instance of FloatData or ReferencedData."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
entity = data.parent
|
|
198
|
+
if not isinstance(entity, BlockModel):
|
|
199
|
+
raise TypeError("The parent of 'data' must be an instance of BlockModel.")
|
|
200
|
+
|
|
201
|
+
tensor = block_model_to_discretize(entity)
|
|
202
|
+
indices = tensor_mesh_ordering(entity)
|
|
203
|
+
|
|
204
|
+
gradients = np.abs(tensor.cell_gradient @ data.values[indices])
|
|
205
|
+
levels = np.zeros(gradients.shape, dtype=int)
|
|
206
|
+
isnan = np.isnan(gradients)
|
|
207
|
+
|
|
208
|
+
if isinstance(data, FloatData):
|
|
209
|
+
actives = gradients[~isnan]
|
|
210
|
+
bins = np.percentile(
|
|
211
|
+
actives[actives > 0], np.linspace(5, 95, mesh.max_level)
|
|
212
|
+
)
|
|
213
|
+
levels[~isnan] = np.searchsorted(bins, actives)
|
|
214
|
+
else:
|
|
215
|
+
levels[gradients > 0] = mesh.max_level
|
|
216
|
+
|
|
217
|
+
# Refine on the value/nan interface, without boundary cells
|
|
218
|
+
if any(isnan):
|
|
219
|
+
horizon = boundary_value_indices(
|
|
220
|
+
tensor, data.values[indices], data.nan_value
|
|
221
|
+
)
|
|
222
|
+
mesh = Driver.refine_by_cell_volumes(
|
|
223
|
+
mesh, entity, finalize=False, mask=horizon[np.argsort(indices)]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
locs = tensor.average_cell_to_face @ tensor.cell_centers
|
|
227
|
+
mesh.insert_cells(locs[~isnan], levels[~isnan].astype(int), finalize=finalize)
|
|
228
|
+
|
|
229
|
+
return mesh
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
file = Path(sys.argv[1]).resolve()
|
|
234
|
+
Driver.start(file)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps 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
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import ClassVar
|
|
14
|
+
|
|
15
|
+
from geoapps_utils.driver.data import BaseData
|
|
16
|
+
from geoh5py.data import FloatData, ReferencedData
|
|
17
|
+
from geoh5py.groups import UIJsonGroup
|
|
18
|
+
from geoh5py.objects import BlockModel
|
|
19
|
+
from pydantic import BaseModel, ConfigDict
|
|
20
|
+
|
|
21
|
+
from grid_apps import assets_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OutputOptions(BaseModel):
|
|
25
|
+
"""
|
|
26
|
+
Output parameters for block model creation.
|
|
27
|
+
|
|
28
|
+
:param export_as: Name of the output entity.
|
|
29
|
+
:param out_group: Output UIJson group.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
33
|
+
|
|
34
|
+
export_as: str | None = None
|
|
35
|
+
out_group: UIJsonGroup | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BlockModel2OctreeOptions(BaseData):
|
|
39
|
+
"""
|
|
40
|
+
Block model parameters for use with `block_models.driver`.
|
|
41
|
+
|
|
42
|
+
:param entity: BlockModel source object.
|
|
43
|
+
:param data: Optional data to refine the octree mesh.
|
|
44
|
+
:param output: Output options.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
48
|
+
|
|
49
|
+
name: ClassVar[str] = "block_model_to_octree"
|
|
50
|
+
default_ui_json: ClassVar[Path] = (
|
|
51
|
+
assets_path() / "uijson/block_model_to_octree.ui.json"
|
|
52
|
+
)
|
|
53
|
+
title: ClassVar[str] = "Block Model to Octree Conversion"
|
|
54
|
+
run_command: ClassVar[str] = "grid_apps.block_model_to_octree.driver"
|
|
55
|
+
conda_environment: str = "grid_apps"
|
|
56
|
+
|
|
57
|
+
entity: BlockModel
|
|
58
|
+
data: FloatData | ReferencedData | None = None
|
|
59
|
+
|
|
60
|
+
output: OutputOptions = OutputOptions()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps is distributed under the terms and conditions of the MIT License '
|
|
7
|
+
# (see LICENSE file at the root of this source code package). '
|
|
8
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps 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
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from discretize.utils import mesh_utils
|
|
18
|
+
from geoapps_utils.driver.data import BaseData
|
|
19
|
+
from geoh5py.objects import BlockModel
|
|
20
|
+
from geoh5py.shared.utils import fetch_active_workspace
|
|
21
|
+
from geoh5py.workspace import Workspace
|
|
22
|
+
from scipy.spatial import cKDTree
|
|
23
|
+
|
|
24
|
+
from grid_apps.block_models.options import BlockModelOptions
|
|
25
|
+
from grid_apps.driver import BaseGridDriver
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Driver(BaseGridDriver):
|
|
32
|
+
"""
|
|
33
|
+
Create BlockModel from parameters.
|
|
34
|
+
|
|
35
|
+
:param parameters: BlockModelOptions or InputFile containing the parameters.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_params_class = BlockModelOptions
|
|
39
|
+
|
|
40
|
+
def make_grid(self):
|
|
41
|
+
"""
|
|
42
|
+
Make block model object from input data.
|
|
43
|
+
"""
|
|
44
|
+
with fetch_active_workspace(self.params.geoh5, mode="r+"):
|
|
45
|
+
source_locations = self.params.source.objects.locations
|
|
46
|
+
if source_locations is None:
|
|
47
|
+
raise ValueError("Input object has no centroids or vertices.")
|
|
48
|
+
|
|
49
|
+
tree = cKDTree(source_locations)
|
|
50
|
+
|
|
51
|
+
logger.info("Creating block model . . .")
|
|
52
|
+
|
|
53
|
+
block_model = Driver.get_block_model(
|
|
54
|
+
workspace=self.params.geoh5,
|
|
55
|
+
locs=source_locations,
|
|
56
|
+
h=self.params.creation.cell_sizes,
|
|
57
|
+
depth_core=self.params.creation.depth_core,
|
|
58
|
+
pads=self.params.creation.padding,
|
|
59
|
+
expansion_factor=self.params.creation.expansion_factor,
|
|
60
|
+
name=self.params.output.export_as,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if self.params.output.out_group is not None:
|
|
64
|
+
block_model.parent = self.params.output.out_group
|
|
65
|
+
|
|
66
|
+
# Try to recenter on nearest
|
|
67
|
+
# Find nearest cells
|
|
68
|
+
if block_model.centroids is None:
|
|
69
|
+
raise ValueError("Block model has no centroids.")
|
|
70
|
+
# TODO: Remove once GEOPY-1602 is merged
|
|
71
|
+
|
|
72
|
+
neighbor_distances, neighbor_indices = tree.query(block_model.centroids)
|
|
73
|
+
nearest_neighbor = np.argmin(neighbor_distances)
|
|
74
|
+
source_to_nearest_neighbor = (
|
|
75
|
+
block_model.centroids[nearest_neighbor, :]
|
|
76
|
+
- source_locations[neighbor_indices[nearest_neighbor], :]
|
|
77
|
+
)
|
|
78
|
+
block_model.origin = (
|
|
79
|
+
np.r_[block_model.origin.tolist()] - source_to_nearest_neighbor
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return block_model
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def truncate_locs_depths(locs: np.ndarray, depth_core: float) -> np.ndarray:
|
|
86
|
+
"""
|
|
87
|
+
Sets locations below core to core bottom.
|
|
88
|
+
|
|
89
|
+
:param locs: Location points.
|
|
90
|
+
:param depth_core: Depth of core mesh below locs.
|
|
91
|
+
|
|
92
|
+
:return locs: locs with depths truncated.
|
|
93
|
+
"""
|
|
94
|
+
zmax = locs[:, -1].max() # top of locs
|
|
95
|
+
below_core_ind = (zmax - locs[:, -1]) > depth_core
|
|
96
|
+
core_bottom_elev = zmax - depth_core
|
|
97
|
+
locs[below_core_ind, -1] = (
|
|
98
|
+
core_bottom_elev # sets locations below core to core bottom
|
|
99
|
+
)
|
|
100
|
+
return locs
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def minimum_depth_core(
|
|
104
|
+
locs: np.ndarray, depth_core: float, core_z_cell_size: int
|
|
105
|
+
) -> float:
|
|
106
|
+
"""
|
|
107
|
+
Get minimum depth core.
|
|
108
|
+
|
|
109
|
+
:param locs: Location points.
|
|
110
|
+
:param depth_core: Depth of core mesh below locs.
|
|
111
|
+
:param core_z_cell_size: Cell size in z direction.
|
|
112
|
+
|
|
113
|
+
:return depth_core: Minimum depth core.
|
|
114
|
+
"""
|
|
115
|
+
zrange = locs[:, -1].max() - locs[:, -1].min() # locs z range
|
|
116
|
+
if depth_core >= zrange:
|
|
117
|
+
return depth_core - zrange + core_z_cell_size
|
|
118
|
+
|
|
119
|
+
return depth_core
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def find_top_padding(obj: BlockModel, core_z_cell_size: int) -> float:
|
|
123
|
+
"""
|
|
124
|
+
Loop through cell spacing and sum until core_z_cell_size is reached.
|
|
125
|
+
|
|
126
|
+
:param obj: Block model.
|
|
127
|
+
:param core_z_cell_size: Cell size in z direction.
|
|
128
|
+
|
|
129
|
+
:return pad_sum: Top padding.
|
|
130
|
+
"""
|
|
131
|
+
pad_sum = 0.0
|
|
132
|
+
|
|
133
|
+
if obj.z_cell_delimiters is None:
|
|
134
|
+
raise ValueError("Block model has no z_cell_delimiters.")
|
|
135
|
+
|
|
136
|
+
for h in np.abs(np.diff(obj.z_cell_delimiters)):
|
|
137
|
+
if h != core_z_cell_size:
|
|
138
|
+
pad_sum += h
|
|
139
|
+
else:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
return pad_sum
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def get_block_model( # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
146
|
+
workspace: Workspace,
|
|
147
|
+
locs: np.ndarray,
|
|
148
|
+
h: list,
|
|
149
|
+
depth_core: float,
|
|
150
|
+
pads: list,
|
|
151
|
+
expansion_factor: float,
|
|
152
|
+
name: str = "BlockModel",
|
|
153
|
+
) -> BlockModel:
|
|
154
|
+
"""
|
|
155
|
+
Create a BlockModel object from parameters.
|
|
156
|
+
|
|
157
|
+
:param workspace: Workspace.
|
|
158
|
+
:param locs: Location points.
|
|
159
|
+
:param h: Cell size(s) for the core mesh.
|
|
160
|
+
:param depth_core: Depth of core mesh below locs.
|
|
161
|
+
:param pads: len(6) Padding distances [W, E, N, S, Down, Up]
|
|
162
|
+
:param expansion_factor: Expansion factor for padding cells.
|
|
163
|
+
:param name: Block model name.
|
|
164
|
+
|
|
165
|
+
:return object_out: Output block model.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
locs = Driver.truncate_locs_depths(locs, depth_core)
|
|
169
|
+
depth_core = Driver.minimum_depth_core(locs, depth_core, h[2])
|
|
170
|
+
mesh = mesh_utils.mesh_builder_xyz(
|
|
171
|
+
locs,
|
|
172
|
+
h,
|
|
173
|
+
padding_distance=[
|
|
174
|
+
[pads[0], pads[1]],
|
|
175
|
+
[pads[2], pads[3]],
|
|
176
|
+
[pads[4], pads[5]],
|
|
177
|
+
],
|
|
178
|
+
depth_core=depth_core,
|
|
179
|
+
expansion_factor=expansion_factor,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
object_out = BlockModel.create(
|
|
183
|
+
workspace,
|
|
184
|
+
origin=[mesh.x0[0], mesh.x0[1], mesh.x0[2] + mesh.h[2].sum()],
|
|
185
|
+
u_cell_delimiters=mesh.nodes_x - mesh.x0[0],
|
|
186
|
+
v_cell_delimiters=mesh.nodes_y - mesh.x0[1],
|
|
187
|
+
z_cell_delimiters=-(mesh.x0[2] + mesh.h[2].sum() - mesh.nodes_z[::-1]),
|
|
188
|
+
name=name,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return object_out
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def params(self) -> BaseData:
|
|
195
|
+
"""Application parameters."""
|
|
196
|
+
return self._params
|
|
197
|
+
|
|
198
|
+
@params.setter
|
|
199
|
+
def params(self, val: BaseData):
|
|
200
|
+
if not isinstance(val, BaseData):
|
|
201
|
+
raise TypeError("Parameters must be a BaseData subclass.")
|
|
202
|
+
self._params = val
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
file = Path(sys.argv[1]).resolve()
|
|
207
|
+
Driver.start(file)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
|
|
2
|
+
# Copyright (c) 2024-2025 Mira Geoscience Ltd. '
|
|
3
|
+
# '
|
|
4
|
+
# This file is part of grid-apps package. '
|
|
5
|
+
# '
|
|
6
|
+
# grid-apps 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
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import ClassVar
|
|
14
|
+
|
|
15
|
+
from geoapps_utils.driver.data import BaseData
|
|
16
|
+
from geoh5py.groups import UIJsonGroup
|
|
17
|
+
from geoh5py.objects import CellObject, Points
|
|
18
|
+
from geoh5py.objects.grid_object import GridObject
|
|
19
|
+
from pydantic import BaseModel, ConfigDict
|
|
20
|
+
|
|
21
|
+
from grid_apps import assets_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BlockModelSourceOptions(BaseModel):
|
|
25
|
+
"""
|
|
26
|
+
Source parameters providing input data to the driver.
|
|
27
|
+
|
|
28
|
+
:param objects: A Grid2D, Octree, BlockModel, Points, Curve or
|
|
29
|
+
Surface source object.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
33
|
+
|
|
34
|
+
objects: Points | CellObject | GridObject
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BlockModelCreationOptions(BaseModel):
|
|
38
|
+
"""
|
|
39
|
+
Block model specification parameters.
|
|
40
|
+
|
|
41
|
+
:param cell_size_x: Cell size in x direction.
|
|
42
|
+
:param cell_size_y: Cell size in y direction.
|
|
43
|
+
:param cell_size_z: Cell size in z direction.
|
|
44
|
+
:param depth_core: Depth of core mesh below locs.
|
|
45
|
+
:param horizontal_padding: Horizontal padding.
|
|
46
|
+
:param bottom_padding: Bottom padding.
|
|
47
|
+
:param expansion_factor: Expansion factor for padding cells.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
cell_size_x: float
|
|
51
|
+
cell_size_y: float
|
|
52
|
+
cell_size_z: float
|
|
53
|
+
depth_core: float
|
|
54
|
+
horizontal_padding: float
|
|
55
|
+
bottom_padding: float
|
|
56
|
+
expansion_factor: float
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def cell_sizes(self) -> list[float]:
|
|
60
|
+
"""
|
|
61
|
+
Cell sizes in x, y and z directions.
|
|
62
|
+
"""
|
|
63
|
+
return [self.cell_size_x, self.cell_size_y, self.cell_size_z]
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def padding(self) -> list[float]:
|
|
67
|
+
"""
|
|
68
|
+
Padding distances in west, east, south, north, down and up directions.
|
|
69
|
+
"""
|
|
70
|
+
return [self.horizontal_padding] * 4 + [self.bottom_padding, 0.0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BlockModelOutputOptions(BaseModel):
|
|
74
|
+
"""
|
|
75
|
+
Output parameters for block model creation.
|
|
76
|
+
|
|
77
|
+
:param export_as: Name of the output entity.
|
|
78
|
+
:param out_group: Name of the output group.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
82
|
+
|
|
83
|
+
export_as: str = "block_model"
|
|
84
|
+
out_group: UIJsonGroup | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BlockModelOptions(BaseData):
|
|
88
|
+
"""
|
|
89
|
+
Block model parameters for use with `block_models.driver`.
|
|
90
|
+
|
|
91
|
+
:param source: Source data parameters.
|
|
92
|
+
:param creation: Block Model creation parameters.
|
|
93
|
+
:param output: Block Model output parameters.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: ClassVar[str] = "block_model"
|
|
97
|
+
default_ui_json: ClassVar[Path] = assets_path() / "uijson/block_models.ui.json"
|
|
98
|
+
title: ClassVar[str] = "Block Model Creation"
|
|
99
|
+
run_command: ClassVar[str] = "grid_apps.block_models.driver"
|
|
100
|
+
|
|
101
|
+
conda_environment: str = "grid_apps"
|
|
102
|
+
source: BlockModelSourceOptions
|
|
103
|
+
creation: BlockModelCreationOptions
|
|
104
|
+
output: BlockModelOutputOptions = BlockModelOutputOptions()
|