Skip to content

Commit 18387c3

Browse files
committed
Merge branch 'master' of github.com:Loop3D/LoopStructural
2 parents 0f55b5d + 0744957 commit 18387c3

File tree

13 files changed

+358
-26
lines changed

13 files changed

+358
-26
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from ._surface import Surface
22
from ._bounding_box import BoundingBox
33
from ._point import ValuePoints, VectorPoints
4+
from ._structured_grid import StructuredGrid

LoopStructural/datatypes/_bounding_box.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
2-
from typing import Optional, Union
2+
from typing import Optional, Union, Dict
33
from LoopStructural.utils.exceptions import LoopValueError
44
from LoopStructural.utils import rng
5+
from LoopStructural.datatypes._structured_grid import StructuredGrid
56
import numpy as np
67

78
from LoopStructural.utils.logging import getLogger
@@ -408,6 +409,14 @@ def vtk(self):
408409
z,
409410
)
410411

411-
@property
412-
def structured_grid(self):
413-
pass
412+
def structured_grid(
413+
self, cell_data: Dict[str, np.ndarray] = {}, vertex_data={}, name: str = "bounding_box"
414+
):
415+
return StructuredGrid(
416+
origin=self.global_origin,
417+
step_vector=self.step_vector,
418+
nsteps=self.nsteps,
419+
properties_cell=cell_data,
420+
properties_vertex=vertex_data,
421+
name=name,
422+
)

LoopStructural/datatypes/_point.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from dataclasses import dataclass
22
import numpy as np
33

4+
from typing import Optional
5+
46

57
@dataclass
68
class ValuePoints:
79
locations: np.ndarray
810
values: np.ndarray
911
name: str
12+
properties: Optional[dict] = None
1013

1114
def to_dict(self):
1215
return {
@@ -22,12 +25,40 @@ def vtk(self):
2225
points["values"] = self.values
2326
return points
2427

28+
def save(self, filename):
29+
filename = str(filename)
30+
ext = filename.split('.')[-1]
31+
if ext == 'json':
32+
import json
33+
34+
with open(filename, 'w') as f:
35+
json.dump(self.to_dict(), f)
36+
elif ext == 'vtk':
37+
self.vtk().save(filename)
38+
39+
elif ext == 'geoh5':
40+
from LoopStructural.export.geoh5 import add_points_to_geoh5
41+
42+
add_points_to_geoh5(filename, self)
43+
elif ext == 'pkl':
44+
import pickle
45+
46+
with open(filename, 'wb') as f:
47+
pickle.dump(self, f)
48+
elif ext == 'vs':
49+
from LoopStructural.export.gocad import _write_pointset
50+
51+
_write_pointset(self, filename)
52+
else:
53+
raise ValueError(f'Unknown file extension {ext}')
54+
2555

2656
@dataclass
2757
class VectorPoints:
2858
locations: np.ndarray
2959
vectors: np.ndarray
3060
name: str
61+
properties: Optional[dict] = None
3162

3263
def to_dict(self):
3364
return {

LoopStructural/datatypes/_structured_grid.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Dict
12
import numpy as np
23
from dataclasses import dataclass
34

@@ -7,7 +8,8 @@ class StructuredGrid:
78
origin: np.ndarray
89
step_vector: np.ndarray
910
nsteps: np.ndarray
10-
data: np.ndarray
11+
properties_cell: Dict[str, np.ndarray]
12+
properties_vertex: Dict[str, np.ndarray]
1113
name: str
1214

1315
def to_dict(self):
@@ -16,7 +18,8 @@ def to_dict(self):
1618
"maximum": self.maximum,
1719
"step_vector": self.step_vector,
1820
"nsteps": self.nsteps,
19-
"data": self.data,
21+
"properties_cell": self.properties_cell,
22+
"properties_vertex": self.properties_vertex,
2023
"name": self.name,
2124
}
2225

@@ -37,5 +40,51 @@ def vtk(self):
3740
y,
3841
z,
3942
)
40-
grid[self.name] = self.data.flatten(order="F")
43+
for name, data in self.properties_vertex.items():
44+
grid[name] = data.flatten(order="F")
45+
for name, data in self.properties_cell.items():
46+
grid.cell_data[name] = data.flatten(order="F")
4147
return grid
48+
49+
def merge(self, other):
50+
if not np.all(np.isclose(self.origin, other.origin)):
51+
raise ValueError("Origin of grids must be the same")
52+
if not np.all(np.isclose(self.step_vector, other.step_vector)):
53+
raise ValueError("Step vector of grids must be the same")
54+
if not np.all(np.isclose(self.nsteps, other.nsteps)):
55+
raise ValueError("Number of steps of grids must be the same")
56+
57+
for name, data in other.properties_cell.items():
58+
self.properties_cell[name] = data
59+
for name, data in other.properties_vertex.items():
60+
self.properties_vertex[name] = data
61+
62+
def save(self, filename):
63+
filename = str(filename)
64+
ext = filename.split('.')[-1]
65+
if ext == 'json':
66+
import json
67+
68+
with open(filename, 'w') as f:
69+
json.dump(self.to_dict(), f)
70+
elif ext == 'vtk':
71+
self.vtk().save(filename)
72+
73+
elif ext == 'geoh5':
74+
from LoopStructural.export.geoh5 import add_structured_grid_to_geoh5
75+
76+
add_structured_grid_to_geoh5(filename, self)
77+
elif ext == 'pkl':
78+
import pickle
79+
80+
with open(filename, 'wb') as f:
81+
pickle.dump(self, f)
82+
elif ext == 'vs':
83+
raise NotImplementedError(
84+
"Saving structured grids in gocad format is not yet implemented"
85+
)
86+
# from LoopStructural.export.gocad import _write_structued_grid
87+
88+
# _write_pointset(self, filename)
89+
else:
90+
raise ValueError(f'Unknown file extension {ext}')

LoopStructural/datatypes/_surface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def save(self, filename):
9595
with open(filename, 'w') as f:
9696
json.dump(self.to_dict(), f)
9797
elif ext == 'vtk':
98-
self.vtk.save(filename)
98+
self.vtk().save(filename)
9999
elif ext == 'obj':
100100
import meshio
101101

LoopStructural/export/geoh5.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import geoh5py
2+
import geoh5py.workspace
3+
import numpy as np
4+
from LoopStructural.datatypes import ValuePoints, VectorPoints
25

36

47
def add_surface_to_geoh5(filename, surface, overwrite=True, groupname="Loop"):
@@ -27,4 +30,71 @@ def add_surface_to_geoh5(filename, surface, overwrite=True, groupname="Loop"):
2730
surface.add_data(data)
2831

2932

30-
# def add_points_to_geoh5(filename, points, overwrite=True, groupname="Loop"):
33+
def add_points_to_geoh5(filename, point, overwrite=True, groupname="Loop"):
34+
with geoh5py.workspace.Workspace(filename) as workspace:
35+
group = workspace.get_entity(groupname)[0]
36+
if not group:
37+
group = geoh5py.groups.ContainerGroup.create(
38+
workspace, name=groupname, allow_delete=True
39+
)
40+
if point.name in workspace.list_entities_name.values():
41+
existing_point = workspace.get_entity(point.name)
42+
existing_point[0].allow_delete = True
43+
if overwrite:
44+
workspace.remove_entity(existing_point[0])
45+
data = {}
46+
if point.properties is not None:
47+
for k, v in point.properties.items():
48+
data[k] = {'association': "VERTEX", "values": v}
49+
if isinstance(point, VectorPoints):
50+
data['vx'] = {'association': "VERTEX", "values": point.vectors[:, 0]}
51+
data['vy'] = {'association': "VERTEX", "values": point.vectors[:, 1]}
52+
data['vz'] = {'association': "VERTEX", "values": point.vectors[:, 2]}
53+
54+
if isinstance(point, ValuePoints):
55+
data['val'] = {'association': "VERTEX", "values": point.values}
56+
point = geoh5py.objects.Points.create(
57+
workspace,
58+
name=point.name,
59+
vertices=point.locations,
60+
parent=group,
61+
)
62+
point.add_data(data)
63+
64+
65+
def add_structured_grid_to_geoh5(filename, structured_grid, overwrite=True, groupname="Loop"):
66+
with geoh5py.workspace.Workspace(filename) as workspace:
67+
group = workspace.get_entity(groupname)[0]
68+
if not group:
69+
group = geoh5py.groups.ContainerGroup.create(
70+
workspace, name=groupname, allow_delete=True
71+
)
72+
if structured_grid.name in workspace.list_entities_name.values():
73+
existing_block = workspace.get_entity(structured_grid.name)
74+
existing_block[0].allow_delete = True
75+
if overwrite:
76+
workspace.remove_entity(existing_block[0])
77+
data = {}
78+
if structured_grid.properties_cell is not None:
79+
for k, v in structured_grid.properties_cell.items():
80+
data[k] = {
81+
'association': "CELL",
82+
"values": np.rot90(v.reshape(structured_grid.nsteps - 1, order="F")).flatten(),
83+
}
84+
block = geoh5py.objects.BlockModel.create(
85+
workspace,
86+
name=structured_grid.name,
87+
origin=structured_grid.origin,
88+
u_cell_delimiters=np.cumsum(
89+
np.ones(structured_grid.nsteps[0]) * structured_grid.step_vector[0]
90+
), # Offsets along u
91+
v_cell_delimiters=np.cumsum(
92+
np.ones(structured_grid.nsteps[1]) * structured_grid.step_vector[1]
93+
), # Offsets along v
94+
z_cell_delimiters=np.cumsum(
95+
np.ones(structured_grid.nsteps[2]) * structured_grid.step_vector[2]
96+
), # Offsets along z (down)
97+
rotation=0.0,
98+
parent=group,
99+
)
100+
block.add_data(data)

LoopStructural/modelling/core/geological_model.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import numpy as np
88
import pandas as pd
99
from typing import List
10-
10+
import pathlib
1111
from ...modelling.features.fault import FaultSegment
1212

1313
from ...modelling.features.builders import (
@@ -111,7 +111,6 @@ def __init__(
111111
112112
113113
"""
114-
# print('tet')
115114
if logfile:
116115
self.logfile = logfile
117116
log_to_file(logfile, level=loglevel)
@@ -1540,6 +1539,9 @@ def evaluate_model(self, xyz: np.ndarray, scale: bool = True) -> np.ndarray:
15401539
strat_id = np.zeros(xyz.shape[0], dtype=int)
15411540
# set strat id to -1 to identify which areas of the model aren't covered
15421541
strat_id[:] = -1
1542+
if self.stratigraphic_column is None:
1543+
logger.warning("No stratigraphic column defined")
1544+
return strat_id
15431545
for group in reversed(self.stratigraphic_column.keys()):
15441546
if group == "faults":
15451547
continue
@@ -1757,6 +1759,9 @@ def stratigraphic_ids(self):
17571759
list of unique stratigraphic ids, featurename, unit name and min and max scalar values
17581760
"""
17591761
ids = []
1762+
if self.stratigraphic_column is None:
1763+
logger.warning('No stratigraphic column defined')
1764+
return ids
17601765
for group in self.stratigraphic_column.keys():
17611766
if group == "faults":
17621767
continue
@@ -1777,6 +1782,8 @@ def get_stratigraphic_surfaces(self, units: List[str] = [], bottoms: bool = True
17771782
## TODO change the stratigraphic column to its own class and have methods to get the relevant surfaces
17781783
surfaces = []
17791784
units = []
1785+
if self.stratigraphic_column is None:
1786+
return []
17801787
for group in self.stratigraphic_column.keys():
17811788
if group == "faults":
17821789
continue
@@ -1800,10 +1807,60 @@ def get_stratigraphic_surfaces(self, units: List[str] = [], bottoms: bool = True
18001807

18011808
return surfaces
18021809

1803-
def get_block_model(self):
1804-
grid = self.bounding_box.vtk()
1810+
def get_block_model(self, name='block model'):
1811+
grid = self.bounding_box.structured_grid(name=name)
18051812

1806-
grid.cell_data['stratigraphy'] = self.evaluate_model(
1813+
grid.properties_cell['stratigraphy'] = self.evaluate_model(
18071814
self.bounding_box.cell_centers(), scale=False
18081815
)
18091816
return grid, self.stratigraphic_ids()
1817+
1818+
def save(
1819+
self,
1820+
filename: str,
1821+
block_model: bool = True,
1822+
stratigraphic_surfaces=True,
1823+
fault_surfaces=True,
1824+
stratigraphic_data=True,
1825+
fault_data=True,
1826+
):
1827+
path = pathlib.Path(filename)
1828+
extension = path.suffix
1829+
name = path.stem
1830+
stratigraphic_surfaces = self.get_stratigraphic_surfaces()
1831+
if fault_surfaces:
1832+
for s in self.get_fault_surfaces():
1833+
## geoh5 can save everything into the same file
1834+
if extension == ".geoh5":
1835+
s.save(filename)
1836+
else:
1837+
s.save(f'{name}_{s.name}.{extension}')
1838+
if stratigraphic_surfaces:
1839+
for s in self.get_stratigraphic_surfaces():
1840+
if extension == ".geoh5":
1841+
s.save(filename)
1842+
else:
1843+
s.save(f'{name}_{s.name}.{extension}')
1844+
if block_model:
1845+
grid, ids = self.get_block_model()
1846+
if extension == ".geoh5":
1847+
grid.save(filename)
1848+
else:
1849+
grid.save(f'{name}_block_model.{extension}')
1850+
if stratigraphic_data:
1851+
if self.stratigraphic_column is not None:
1852+
for group in self.stratigraphic_column.keys():
1853+
if group == "faults":
1854+
continue
1855+
for series in self.stratigraphic_column[group].keys():
1856+
if extension == ".geoh5":
1857+
self.__getitem__(series).save(filename)
1858+
else:
1859+
self.__getitem__(series).save(f'{name}_{series}.{extension}')
1860+
if fault_data:
1861+
for f in self.fault_names():
1862+
for d in self.__getitem__(f).get_data():
1863+
if extension == ".geoh5":
1864+
d.save(filename)
1865+
else:
1866+
d.save(f'{name}_{group}.{extension}')

LoopStructural/modelling/features/_base_geological_feature.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,14 @@ def scalar_field(self, bounding_box=None):
314314
if self.model is None:
315315
raise ValueError("Must specify bounding box")
316316
bounding_box = self.model.bounding_box
317-
grid = bounding_box.vtk()
317+
grid = bounding_box.structured_grid(name=self.name)
318318
value = self.evaluate_value(
319319
self.model.scale(bounding_box.regular_grid(local=False, order='F'))
320320
)
321-
grid[self.name] = value
321+
grid.properties_vertex[self.name] = value
322+
323+
value = self.evaluate_value(bounding_box.cell_centers(order='F'))
324+
grid.properties_cell[self.name] = value
322325
return grid
323326

324327
def vector_field(self, bounding_box=None, tolerance=0.05, scale=1.0):

0 commit comments

Comments
 (0)