geometallurgy 0.4.11__py3-none-any.whl → 0.4.13__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.
- elphick/geomet/__init__.py +11 -11
- elphick/geomet/base.py +1133 -1133
- elphick/geomet/block_model.py +319 -358
- elphick/geomet/config/__init__.py +1 -1
- elphick/geomet/config/config_read.py +39 -39
- elphick/geomet/config/flowsheet_example_partition.yaml +31 -31
- elphick/geomet/config/flowsheet_example_simple.yaml +25 -25
- elphick/geomet/config/mc_config.yml +35 -35
- elphick/geomet/data/downloader.py +39 -39
- elphick/geomet/data/register.csv +12 -12
- elphick/geomet/datasets/__init__.py +2 -2
- elphick/geomet/datasets/datasets.py +47 -47
- elphick/geomet/datasets/downloader.py +40 -40
- elphick/geomet/datasets/register.csv +12 -12
- elphick/geomet/datasets/sample_data.py +196 -196
- elphick/geomet/extras.py +35 -35
- elphick/geomet/flowsheet/__init__.py +1 -1
- elphick/geomet/flowsheet/flowsheet.py +1216 -1193
- elphick/geomet/flowsheet/loader.py +99 -99
- elphick/geomet/flowsheet/operation.py +256 -256
- elphick/geomet/flowsheet/stream.py +39 -38
- elphick/geomet/interval_sample.py +641 -641
- elphick/geomet/io.py +379 -379
- elphick/geomet/plot.py +147 -147
- elphick/geomet/sample.py +28 -28
- elphick/geomet/utils/amenability.py +49 -49
- elphick/geomet/utils/block_model_converter.py +93 -93
- elphick/geomet/utils/components.py +136 -136
- elphick/geomet/utils/data.py +49 -49
- elphick/geomet/utils/estimates.py +108 -108
- elphick/geomet/utils/interp.py +193 -193
- elphick/geomet/utils/interp2.py +134 -134
- elphick/geomet/utils/layout.py +72 -72
- elphick/geomet/utils/moisture.py +61 -61
- elphick/geomet/utils/output.html +617 -0
- elphick/geomet/utils/pandas.py +378 -378
- elphick/geomet/utils/parallel.py +29 -29
- elphick/geomet/utils/partition.py +63 -63
- elphick/geomet/utils/size.py +51 -51
- elphick/geomet/utils/timer.py +80 -80
- elphick/geomet/utils/viz.py +56 -56
- elphick/geomet/validate.py.hide +176 -176
- {geometallurgy-0.4.11.dist-info → geometallurgy-0.4.13.dist-info}/LICENSE +21 -21
- {geometallurgy-0.4.11.dist-info → geometallurgy-0.4.13.dist-info}/METADATA +7 -5
- geometallurgy-0.4.13.dist-info/RECORD +49 -0
- {geometallurgy-0.4.11.dist-info → geometallurgy-0.4.13.dist-info}/WHEEL +1 -1
- elphick/geomet/utils/sampling.py +0 -5
- geometallurgy-0.4.11.dist-info/RECORD +0 -49
- {geometallurgy-0.4.11.dist-info → geometallurgy-0.4.13.dist-info}/entry_points.txt +0 -0
elphick/geomet/block_model.py
CHANGED
|
@@ -1,358 +1,319 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from functools import wraps
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional, Union, Literal, TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from scipy import stats
|
|
9
|
-
|
|
10
|
-
from elphick.geomet import extras
|
|
11
|
-
from elphick.geomet.base import MassComposition
|
|
12
|
-
from elphick.geomet.extras import BlockmodelExtras
|
|
13
|
-
from elphick.geomet.utils.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return cls(data=
|
|
112
|
-
|
|
113
|
-
@import_extras
|
|
114
|
-
def to_omf(self, omf_filepath: Path, imports,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# Get the
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
dz =
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
@staticmethod
|
|
321
|
-
@log_timer
|
|
322
|
-
@import_extras
|
|
323
|
-
def voxelise(blocks, imports) -> 'pv.UnstructuredGrid':
|
|
324
|
-
|
|
325
|
-
logger = logging.getLogger(__name__)
|
|
326
|
-
msg = "Voxelising blocks requires PVGeo package."
|
|
327
|
-
logger.error(msg)
|
|
328
|
-
raise NotImplementedError(msg)
|
|
329
|
-
|
|
330
|
-
# vtkpoints = PVGeo.points_to_poly_data(centroid_data)
|
|
331
|
-
|
|
332
|
-
x_values = blocks.index.get_level_values('x').values
|
|
333
|
-
y_values = blocks.index.get_level_values('y').values
|
|
334
|
-
z_values = blocks.index.get_level_values('z').values
|
|
335
|
-
|
|
336
|
-
# Stack x, y, z values into a numpy array
|
|
337
|
-
centroids = np.column_stack((x_values, y_values, z_values))
|
|
338
|
-
|
|
339
|
-
# Create a PolyData object
|
|
340
|
-
polydata = imports.pv.PolyData(centroids)
|
|
341
|
-
|
|
342
|
-
# Add cell values as point data
|
|
343
|
-
for column in blocks.columns:
|
|
344
|
-
polydata[column] = blocks[[column]]
|
|
345
|
-
|
|
346
|
-
# Create a Voxelizer filter
|
|
347
|
-
voxelizer = PVGeo.filters.VoxelizePoints()
|
|
348
|
-
# Apply the filter to the points
|
|
349
|
-
grid = voxelizer.apply(polydata)
|
|
350
|
-
|
|
351
|
-
logger.info(f"Voxelised {blocks.shape[0]} points.")
|
|
352
|
-
logger.info("Recovered Angle (deg.): %.3f" % voxelizer.get_angle())
|
|
353
|
-
logger.info("Recovered Cell Sizes: (%.2f, %.2f, %.2f)" % voxelizer.get_spacing())
|
|
354
|
-
|
|
355
|
-
return grid
|
|
356
|
-
|
|
357
|
-
def __str__(self):
|
|
358
|
-
return f"BlockModel: {self.name}\n{self.aggregate.to_dict()}"
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Union, Literal, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from scipy import stats
|
|
9
|
+
|
|
10
|
+
from elphick.geomet import extras
|
|
11
|
+
from elphick.geomet.base import MassComposition
|
|
12
|
+
from elphick.geomet.extras import BlockmodelExtras
|
|
13
|
+
from elphick.geomet.utils.timer import log_timer
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import pyvista as pv
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Modify the import_extras decorator
|
|
20
|
+
def import_extras(func):
|
|
21
|
+
@wraps(func)
|
|
22
|
+
def wrapper(*args, **kwargs):
|
|
23
|
+
omfpandas, omfvista, pv = extras.import_blockmodel_packages()
|
|
24
|
+
extras_instance = BlockmodelExtras(omfpandas, omfvista, pv)
|
|
25
|
+
return func(*args, imports=extras_instance, **kwargs)
|
|
26
|
+
|
|
27
|
+
return wrapper
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BlockModel(MassComposition):
|
|
31
|
+
def __init__(self,
|
|
32
|
+
data: Optional[pd.DataFrame] = None,
|
|
33
|
+
name: Optional[str] = None,
|
|
34
|
+
moisture_in_scope: bool = True,
|
|
35
|
+
mass_wet_var: Optional[str] = None,
|
|
36
|
+
mass_dry_var: Optional[str] = None,
|
|
37
|
+
moisture_var: Optional[str] = None,
|
|
38
|
+
component_vars: Optional[list[str]] = None,
|
|
39
|
+
composition_units: Literal['%', 'ppm', 'ppb'] = '%',
|
|
40
|
+
components_as_symbols: bool = True,
|
|
41
|
+
ranges: Optional[dict[str, list]] = None,
|
|
42
|
+
config_file: Optional[Path] = None):
|
|
43
|
+
|
|
44
|
+
if data is not None:
|
|
45
|
+
if isinstance(data.index, pd.MultiIndex):
|
|
46
|
+
if all([n.lower() in data.index.names for n in ['x', 'y', 'z', 'dx', 'dy', 'dz']]):
|
|
47
|
+
self.is_irregular = True
|
|
48
|
+
elif all([n.lower() in data.index.names for n in ['x', 'y', 'z']]):
|
|
49
|
+
self.is_irregular = False
|
|
50
|
+
data.index.set_names([n.lower() for n in data.index.names], inplace=True)
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError("The index must be a pd.MultiIndex with names ['x', 'y', 'z'] "
|
|
54
|
+
"or [['x', 'y', 'z', 'dx', 'dy', 'dz'].")
|
|
55
|
+
|
|
56
|
+
# sort the data to ensure consistent with pyvista
|
|
57
|
+
data.sort_index(level=['z', 'y', 'x'], ascending=[True, True, True], inplace=True)
|
|
58
|
+
|
|
59
|
+
super().__init__(data=data, name=name, moisture_in_scope=moisture_in_scope,
|
|
60
|
+
mass_wet_var=mass_wet_var, mass_dry_var=mass_dry_var,
|
|
61
|
+
moisture_var=moisture_var, component_vars=component_vars,
|
|
62
|
+
composition_units=composition_units, components_as_symbols=components_as_symbols,
|
|
63
|
+
ranges=ranges, config_file=config_file)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
@import_extras
|
|
67
|
+
def from_omf(cls, omf_filepath: Path, imports,
|
|
68
|
+
element_name: Optional[str] = None,
|
|
69
|
+
columns: Optional[list[str]] = None,
|
|
70
|
+
query: Optional[str] = None,
|
|
71
|
+
density: float = 2.5) -> 'BlockModel':
|
|
72
|
+
"""Create a BlockModel from an OMF file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
omf_filepath: Path to the OMF file.
|
|
76
|
+
imports: internally used to import the necessary packages.
|
|
77
|
+
element_name: The name of the element in the OMF file.
|
|
78
|
+
columns: The columns to extract from the OMF file.
|
|
79
|
+
query: The query to filter the DataFrame.
|
|
80
|
+
density: The density of the material in g/cm3, used to calculate DMT (Dry Mass Tonnes). A workaround.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
BlockModel: The BlockModel instance.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
omfpr: imports.omfpandas.OMFPandasReader = imports.omfpandas.OMFPandasReader(filepath=omf_filepath)
|
|
88
|
+
blocks: pd.DataFrame = omfpr.read_blockmodel(blockmodel_name=element_name, attributes=columns,
|
|
89
|
+
query=query)
|
|
90
|
+
|
|
91
|
+
# get the block volume
|
|
92
|
+
|
|
93
|
+
volume: Union[float, np.ndarray[float]]
|
|
94
|
+
from omfpandas.blockmodel import OMFBlockModel
|
|
95
|
+
from omfpandas.blockmodels.convert_blockmodel import df_to_blockmodel
|
|
96
|
+
from omfpandas.blockmodels.geometry import Geometry
|
|
97
|
+
geom: Geometry = OMFBlockModel(df_to_blockmodel(blocks, blockmodel_name=element_name)).geometry
|
|
98
|
+
|
|
99
|
+
if geom.__class__.__name__ == 'RegularGeometry':
|
|
100
|
+
volume = geom.block_size[0] * geom.block_size[1] * geom.block_size[2]
|
|
101
|
+
elif geom.__class__.__name__ == 'TensorGeometry':
|
|
102
|
+
# TODO: Implement the volume calculation for TensorGeometry - this is a placeholder.
|
|
103
|
+
volume = geom.block_sizes[0][0] * geom.block_sizes[0][1] * geom.block_sizes[0][2]
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Geometry type '{geom.__class__.__name__}' not supported.")
|
|
106
|
+
|
|
107
|
+
if density is not None:
|
|
108
|
+
blocks['mass_dry'] = volume * density
|
|
109
|
+
moisture_in_scope = False
|
|
110
|
+
|
|
111
|
+
return cls(data=blocks, name=element_name, mass_dry_var='mass_dry', moisture_in_scope=moisture_in_scope)
|
|
112
|
+
|
|
113
|
+
@import_extras
|
|
114
|
+
def to_omf(self, omf_filepath: Path, imports, element_name: str = 'Block Model',
|
|
115
|
+
description: str = 'A block model'):
|
|
116
|
+
"""Write the BlockModel to an OMF file.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
omf_filepath: Path to the OMF file.
|
|
120
|
+
imports: internally used to import the necessary packages.
|
|
121
|
+
element_name: The name of the element in the OMF file.
|
|
122
|
+
description: Description of the block model.
|
|
123
|
+
"""
|
|
124
|
+
# Create an OMFPandasWriter instance
|
|
125
|
+
writer = imports.omfpandas.OMFPandasWriter(filepath=omf_filepath)
|
|
126
|
+
|
|
127
|
+
# Write the block model to the OMF file
|
|
128
|
+
writer.write_blockmodel(blockmodel_name=element_name, description=description, dataframe=self.data)
|
|
129
|
+
|
|
130
|
+
@log_timer
|
|
131
|
+
def get_blocks(self) -> Union['pv.StructuredGrid', 'pv.UnstructuredGrid']:
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Attempt to create a regular grid
|
|
135
|
+
grid = self.create_structured_grid()
|
|
136
|
+
self._logger.debug("Created a pv.StructuredGrid.")
|
|
137
|
+
except ValueError:
|
|
138
|
+
# If it fails, create an irregular grid
|
|
139
|
+
grid = self.create_unstructured_grid()
|
|
140
|
+
self._logger.debug("Created a pv.UnstructuredGrid.")
|
|
141
|
+
return grid
|
|
142
|
+
|
|
143
|
+
@import_extras
|
|
144
|
+
def plot(self, scalar: str, imports, show_edges: bool = True) -> 'pv.Plotter':
|
|
145
|
+
|
|
146
|
+
if scalar not in self.data_columns:
|
|
147
|
+
raise ValueError(f"Column '{scalar}' not found in the DataFrame.")
|
|
148
|
+
|
|
149
|
+
# Create a PyVista plotter
|
|
150
|
+
plotter = imports.pv.Plotter()
|
|
151
|
+
|
|
152
|
+
mesh = self.get_blocks()
|
|
153
|
+
|
|
154
|
+
# Add a thresholded mesh to the plotter
|
|
155
|
+
plotter.add_mesh_threshold(mesh, scalars=scalar, show_edges=show_edges)
|
|
156
|
+
|
|
157
|
+
return plotter
|
|
158
|
+
|
|
159
|
+
def is_regular(self) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Determine if the grid spacing is complete and regular
|
|
162
|
+
If it is, a pv.StructuredGrid is suitable.
|
|
163
|
+
If not, a pv.UnstructuredGrid is suitable.
|
|
164
|
+
|
|
165
|
+
:return:
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
block_sizes = np.array(self._block_sizes())
|
|
169
|
+
return np.all(np.isclose(np.mean(block_sizes, axis=1), 0))
|
|
170
|
+
|
|
171
|
+
def _block_sizes(self):
|
|
172
|
+
data = self.data
|
|
173
|
+
x_unique = data.index.get_level_values('x').unique()
|
|
174
|
+
y_unique = data.index.get_level_values('y').unique()
|
|
175
|
+
z_unique = data.index.get_level_values('z').unique()
|
|
176
|
+
|
|
177
|
+
x_spacing = np.diff(x_unique)
|
|
178
|
+
y_spacing = np.diff(y_unique)
|
|
179
|
+
z_spacing = np.diff(z_unique)
|
|
180
|
+
|
|
181
|
+
return x_spacing, y_spacing, z_spacing
|
|
182
|
+
|
|
183
|
+
def common_block_size(self):
|
|
184
|
+
data = self.data
|
|
185
|
+
x_unique = data.index.get_level_values('x').unique()
|
|
186
|
+
y_unique = data.index.get_level_values('y').unique()
|
|
187
|
+
z_unique = data.index.get_level_values('z').unique()
|
|
188
|
+
|
|
189
|
+
x_spacing = np.abs(np.diff(x_unique))
|
|
190
|
+
y_spacing = np.abs(np.diff(y_unique))
|
|
191
|
+
z_spacing = np.abs(np.diff(z_unique))
|
|
192
|
+
|
|
193
|
+
return stats.mode(x_spacing).mode, stats.mode(y_spacing).mode, stats.mode(z_spacing).mode
|
|
194
|
+
|
|
195
|
+
@import_extras
|
|
196
|
+
def create_structured_grid(self, imports) -> 'pv.StructuredGrid':
|
|
197
|
+
|
|
198
|
+
# Get the unique x, y, z coordinates (centroids)
|
|
199
|
+
data = self.data.sort_values(['z', 'y', 'x']) # ensure the data is sorted F-style
|
|
200
|
+
x_centroids = data.index.get_level_values('x').unique()
|
|
201
|
+
y_centroids = data.index.get_level_values('y').unique()
|
|
202
|
+
z_centroids = data.index.get_level_values('z').unique()
|
|
203
|
+
|
|
204
|
+
# Calculate the cell size (assuming all cells are of equal size)
|
|
205
|
+
dx = np.diff(x_centroids)[0]
|
|
206
|
+
dy = np.diff(y_centroids)[0]
|
|
207
|
+
dz = np.diff(z_centroids)[0]
|
|
208
|
+
|
|
209
|
+
# Calculate the grid points
|
|
210
|
+
x_points = np.concatenate([x_centroids - dx / 2, x_centroids[-1:] + dx / 2])
|
|
211
|
+
y_points = np.concatenate([y_centroids - dy / 2, y_centroids[-1:] + dy / 2])
|
|
212
|
+
z_points = np.concatenate([z_centroids - dz / 2, z_centroids[-1:] + dz / 2])
|
|
213
|
+
|
|
214
|
+
# Create the 3D grid of points
|
|
215
|
+
x, y, z = np.meshgrid(x_points, y_points, z_points, indexing='ij')
|
|
216
|
+
|
|
217
|
+
# Create a StructuredGrid object
|
|
218
|
+
grid = imports.pv.StructuredGrid(x, y, z)
|
|
219
|
+
|
|
220
|
+
# Add the data from the DataFrame to the grid
|
|
221
|
+
for column in data.columns:
|
|
222
|
+
grid.cell_data[column] = data[column].values
|
|
223
|
+
|
|
224
|
+
return grid
|
|
225
|
+
|
|
226
|
+
def create_voxels(self) -> 'pv.UnstructuredGrid':
|
|
227
|
+
grid = self.voxelise(self.data)
|
|
228
|
+
return grid
|
|
229
|
+
|
|
230
|
+
@import_extras
|
|
231
|
+
def create_unstructured_grid(self, imports) -> 'pv.UnstructuredGrid':
|
|
232
|
+
"""
|
|
233
|
+
Requires the index to be a pd.MultiIndex with names ['x', 'y', 'z', 'dx', 'dy', 'dz'].
|
|
234
|
+
:return:
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
# Get the x, y, z coordinates and cell dimensions
|
|
238
|
+
blocks = self.data.reset_index().sort_values(['z', 'y', 'x']) # ensure the data is sorted F-style
|
|
239
|
+
# if no dims are passed, estimate them
|
|
240
|
+
if 'dx' not in blocks.columns:
|
|
241
|
+
dx, dy, dz = self.common_block_size()
|
|
242
|
+
blocks['dx'] = dx
|
|
243
|
+
blocks['dy'] = dy
|
|
244
|
+
blocks['dz'] = dz
|
|
245
|
+
|
|
246
|
+
x, y, z, dx, dy, dz = (blocks[col].values for col in blocks.columns if col in ['x', 'y', 'z', 'dx', 'dy', 'dz'])
|
|
247
|
+
blocks.set_index(['x', 'y', 'z', 'dx', 'dy', 'dz'], inplace=True)
|
|
248
|
+
# Create the cell points/vertices
|
|
249
|
+
# REF: https://github.com/OpenGeoVis/PVGeo/blob/main/PVGeo/filters/voxelize.py
|
|
250
|
+
|
|
251
|
+
n_cells = len(x)
|
|
252
|
+
|
|
253
|
+
# Generate cell nodes for all points in data set
|
|
254
|
+
# - Bottom
|
|
255
|
+
c_n1 = np.stack(((x - dx / 2), (y - dy / 2), (z - dz / 2)), axis=1)
|
|
256
|
+
c_n2 = np.stack(((x + dx / 2), (y - dy / 2), (z - dz / 2)), axis=1)
|
|
257
|
+
c_n3 = np.stack(((x - dx / 2), (y + dy / 2), (z - dz / 2)), axis=1)
|
|
258
|
+
c_n4 = np.stack(((x + dx / 2), (y + dy / 2), (z - dz / 2)), axis=1)
|
|
259
|
+
# - Top
|
|
260
|
+
c_n5 = np.stack(((x - dx / 2), (y - dy / 2), (z + dz / 2)), axis=1)
|
|
261
|
+
c_n6 = np.stack(((x + dx / 2), (y - dy / 2), (z + dz / 2)), axis=1)
|
|
262
|
+
c_n7 = np.stack(((x - dx / 2), (y + dy / 2), (z + dz / 2)), axis=1)
|
|
263
|
+
c_n8 = np.stack(((x + dx / 2), (y + dy / 2), (z + dz / 2)), axis=1)
|
|
264
|
+
|
|
265
|
+
# - Concatenate
|
|
266
|
+
# nodes = np.concatenate((c_n1, c_n2, c_n3, c_n4, c_n5, c_n6, c_n7, c_n8), axis=0)
|
|
267
|
+
nodes = np.hstack((c_n1, c_n2, c_n3, c_n4, c_n5, c_n6, c_n7, c_n8)).ravel().reshape(n_cells * 8, 3)
|
|
268
|
+
|
|
269
|
+
# create the cells
|
|
270
|
+
# REF: https://docs/pyvista.org/examples/00-load/create-unstructured-surface.html
|
|
271
|
+
cells_hex = np.arange(n_cells * 8).reshape(n_cells, 8)
|
|
272
|
+
|
|
273
|
+
grid = imports.pv.UnstructuredGrid({imports.pv.CellType.VOXEL: cells_hex}, nodes)
|
|
274
|
+
|
|
275
|
+
# add the attributes (column) data
|
|
276
|
+
for col in blocks.columns:
|
|
277
|
+
grid.cell_data[col] = blocks[col].values
|
|
278
|
+
|
|
279
|
+
return grid
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
@log_timer
|
|
283
|
+
@import_extras
|
|
284
|
+
def voxelise(blocks, imports) -> 'pv.UnstructuredGrid':
|
|
285
|
+
|
|
286
|
+
logger = logging.getLogger(__name__)
|
|
287
|
+
msg = "Voxelising blocks requires PVGeo package."
|
|
288
|
+
logger.error(msg)
|
|
289
|
+
raise NotImplementedError(msg)
|
|
290
|
+
|
|
291
|
+
# vtkpoints = PVGeo.points_to_poly_data(centroid_data)
|
|
292
|
+
|
|
293
|
+
x_values = blocks.index.get_level_values('x').values
|
|
294
|
+
y_values = blocks.index.get_level_values('y').values
|
|
295
|
+
z_values = blocks.index.get_level_values('z').values
|
|
296
|
+
|
|
297
|
+
# Stack x, y, z values into a numpy array
|
|
298
|
+
centroids = np.column_stack((x_values, y_values, z_values))
|
|
299
|
+
|
|
300
|
+
# Create a PolyData object
|
|
301
|
+
polydata = imports.pv.PolyData(centroids)
|
|
302
|
+
|
|
303
|
+
# Add cell values as point data
|
|
304
|
+
for column in blocks.columns:
|
|
305
|
+
polydata[column] = blocks[[column]]
|
|
306
|
+
|
|
307
|
+
# Create a Voxelizer filter
|
|
308
|
+
voxelizer = PVGeo.filters.VoxelizePoints()
|
|
309
|
+
# Apply the filter to the points
|
|
310
|
+
grid = voxelizer.apply(polydata)
|
|
311
|
+
|
|
312
|
+
logger.info(f"Voxelised {blocks.shape[0]} points.")
|
|
313
|
+
logger.info("Recovered Angle (deg.): %.3f" % voxelizer.get_angle())
|
|
314
|
+
logger.info("Recovered Cell Sizes: (%.2f, %.2f, %.2f)" % voxelizer.get_spacing())
|
|
315
|
+
|
|
316
|
+
return grid
|
|
317
|
+
|
|
318
|
+
def __str__(self):
|
|
319
|
+
return f"BlockModel: {self.name}\n{self.aggregate.to_dict()}"
|