pygcd 0.3__py2.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.
- pygcd/__init__.py +21 -0
- pygcd/__version__.py +16 -0
- pygcd/dataset.py +203 -0
- pygcd/drivers/__init__.py +12 -0
- pygcd/drivers/_cells.py +43 -0
- pygcd/drivers/_utils.py +9 -0
- pygcd/drivers/geopandas_driver.py +148 -0
- pygcd/drivers/lasio_driver.py +74 -0
- pygcd/drivers/pyvista_driver.py +143 -0
- pygcd/objects/__init__.py +6 -0
- pygcd/objects/abstract.py +154 -0
- pygcd/objects/grid.py +29 -0
- pygcd/objects/mesh.py +55 -0
- pygcd/objects/well.py +84 -0
- pygcd/readers/__init__.py +31 -0
- pygcd/readers/_utils.py +40 -0
- pygcd/readers/grid.py +2 -0
- pygcd/readers/header.py +80 -0
- pygcd/readers/mesh.py +101 -0
- pygcd/readers/well.py +299 -0
- pygcd-0.3.dist-info/LICENSE +661 -0
- pygcd-0.3.dist-info/METADATA +858 -0
- pygcd-0.3.dist-info/RECORD +25 -0
- pygcd-0.3.dist-info/WHEEL +6 -0
- pygcd-0.3.dist-info/top_level.txt +1 -0
pygcd/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from .__version__ import __version__, __version_tuple__, version, version_tuple
|
|
3
|
+
except ImportError:
|
|
4
|
+
__version__ = version = None
|
|
5
|
+
__version_tuple__ = version_tuple = ()
|
|
6
|
+
|
|
7
|
+
from .dataset import Dataset
|
|
8
|
+
from .drivers import Drivers, to_geopandas, to_pyvista
|
|
9
|
+
from .objects import Chunk, Geometry, Grid, Mesh, Well, decode
|
|
10
|
+
|
|
11
|
+
find = Dataset.find
|
|
12
|
+
load = Dataset.load
|
|
13
|
+
read = Dataset.read
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def getCapabilities():
|
|
17
|
+
return list(Geometry.names)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def getWrappers():
|
|
21
|
+
return list(Drivers.keys())
|
pygcd/__version__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.3'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 3)
|
pygcd/dataset.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .drivers import Drivers
|
|
8
|
+
from .objects import Chunk, Geometry, Object, decode
|
|
9
|
+
from .readers import find_objects
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Dataset(list):
|
|
13
|
+
"""Container for GOCAD objects.
|
|
14
|
+
|
|
15
|
+
Each GOCAD project file is composed of (multiple) object(s).
|
|
16
|
+
All objects have a common structured header with general
|
|
17
|
+
properties (i.e. name, geometry type, version, ...).
|
|
18
|
+
These objects can be meshes, grids or wells ...
|
|
19
|
+
their geometry needs to be decoded accordingly !
|
|
20
|
+
|
|
21
|
+
A project file is read as follow:
|
|
22
|
+
- `find`: split objects (block parsing based on "GOCAD ... END" regex)
|
|
23
|
+
- `load`: identify object (parse headers, register {name, geometry, properties})
|
|
24
|
+
- `read`: extract the geometry (decode meshes/grids/wells structure and data)
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
filename (str): The file to read.
|
|
28
|
+
*! Only ASCII exports are currently supported !*
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, obj=None, filename="") -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.filename = filename
|
|
34
|
+
if isinstance(obj, Dataset):
|
|
35
|
+
self = copy(obj)
|
|
36
|
+
elif isinstance(obj, list):
|
|
37
|
+
for e in filter(None, obj):
|
|
38
|
+
self.append(copy(e))
|
|
39
|
+
|
|
40
|
+
def __getitem__(self, key) -> Chunk | Object:
|
|
41
|
+
if isinstance(key, str):
|
|
42
|
+
return self[self.names.index(key)]
|
|
43
|
+
else:
|
|
44
|
+
return super().__getitem__(key)
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
cout = f'"{self.filename}"\n↳ ' if self.filename else ""
|
|
48
|
+
if self:
|
|
49
|
+
cout += f"{len(self)} objects : " + "{\n"
|
|
50
|
+
for obj in self:
|
|
51
|
+
cout += f" {obj}\n"
|
|
52
|
+
cout += "}"
|
|
53
|
+
else:
|
|
54
|
+
cout += "[]"
|
|
55
|
+
return cout
|
|
56
|
+
|
|
57
|
+
def clear(self):
|
|
58
|
+
super().clear()
|
|
59
|
+
self.filename = ""
|
|
60
|
+
|
|
61
|
+
def empty(self):
|
|
62
|
+
super().clear()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def names(self):
|
|
66
|
+
return [el.name for el in self]
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def geometries(self):
|
|
70
|
+
return [el.geometry for el in self]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def properties(self):
|
|
74
|
+
return [el.properties for el in self]
|
|
75
|
+
|
|
76
|
+
def items(self):
|
|
77
|
+
yield from zip(self.names, self)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def objects(self):
|
|
81
|
+
return [e for e in self if isinstance(e, Object)]
|
|
82
|
+
|
|
83
|
+
def filter(self, *, indices=[], names=[], geometries=[]):
|
|
84
|
+
"""Filter a specific geometry.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
indices (Iterable[int]): The indices to select.
|
|
88
|
+
names (Union[str, Iterable[str]]): The objects to select (list or regex).
|
|
89
|
+
geometries (Union[Geometry, str, int]): The geometries to select.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dataset: A subset of the dataset mathcing filter criterions.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# geometries can be
|
|
96
|
+
geometries = [
|
|
97
|
+
Geometry[g] if isinstance(g, str) else Geometry(g) for g in geometries
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
valids = []
|
|
101
|
+
for i, el in enumerate(self):
|
|
102
|
+
if indices and i not in indices:
|
|
103
|
+
continue
|
|
104
|
+
elif names and el.name not in names:
|
|
105
|
+
continue
|
|
106
|
+
elif geometries and el.geometry not in geometries:
|
|
107
|
+
continue
|
|
108
|
+
valids.append(el)
|
|
109
|
+
|
|
110
|
+
return self.__class__(valids, self.filename)
|
|
111
|
+
|
|
112
|
+
def to(self, wrapper: str) -> Any:
|
|
113
|
+
if wrapper.lower() not in Drivers:
|
|
114
|
+
raise ValueError(f"Unsupported format: {wrapper}")
|
|
115
|
+
else:
|
|
116
|
+
driver = Drivers[wrapper.lower()]
|
|
117
|
+
return driver(self)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def find(cls, filename) -> list[str]:
|
|
121
|
+
"""Open an ascii file and split objects in it.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
filename (str): Ascii GOCAD export file.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
list[str]: List of single GOCAD objects text blocks.
|
|
128
|
+
"""
|
|
129
|
+
# TODO: handle binary/projects files
|
|
130
|
+
path = Path(filename)
|
|
131
|
+
return find_objects(path.read_bytes().decode("ascii", "replace"))
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def load(cls, filename: str, **kwargs) -> list[Chunk]:
|
|
135
|
+
"""Open an ascii file and identify objects in it.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
filename (str): Ascii GOCAD export file.
|
|
139
|
+
**kwargs: Reading options for `pathlib.Path.read_text()`.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dataset[Chunk]: Collection of identified GOCAD objects.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
blocks = cls.find(filename, **kwargs)
|
|
146
|
+
n = len(blocks)
|
|
147
|
+
if n == 0:
|
|
148
|
+
return cls()
|
|
149
|
+
|
|
150
|
+
blocks = [Chunk(b) for b in blocks]
|
|
151
|
+
|
|
152
|
+
return cls(blocks, filename=filename)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def read(
|
|
156
|
+
cls,
|
|
157
|
+
filename: str,
|
|
158
|
+
*,
|
|
159
|
+
wrapper: str = None,
|
|
160
|
+
index=None,
|
|
161
|
+
indices=[],
|
|
162
|
+
names=[],
|
|
163
|
+
geometries=[],
|
|
164
|
+
encoding=None,
|
|
165
|
+
) -> list[Object] | Object:
|
|
166
|
+
"""Open an ascii file and read objects in it.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
filename (str): Ascii GOCAD export file.
|
|
170
|
+
wrapper (str): The format to wrap the returned Dataset. Must be in `pygcd.getWrappers()`.
|
|
171
|
+
index (Union[int, str]): Index of the object to return.
|
|
172
|
+
Kwargs:
|
|
173
|
+
indices (Iterable[int]): The indices to select.
|
|
174
|
+
names (Union[str, Iterable[str]]): The objects to select (list or regex).
|
|
175
|
+
geometries (Union[Geometry, str, int]): The geometries to select.
|
|
176
|
+
|
|
177
|
+
geometries (Geometry): Filter objects to return based on the geometry.
|
|
178
|
+
**kwargs: Reading options for `pathlib.Path.read_text()`.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dataset[Object]: Collection of parsed GOCAD objects.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
kwargs = {"filename": filename}
|
|
185
|
+
|
|
186
|
+
# identify ascii chunks
|
|
187
|
+
ds = cls.load(**kwargs)
|
|
188
|
+
|
|
189
|
+
# filter identified objects (avoid extensive parsing)
|
|
190
|
+
if len(indices + names + geometries):
|
|
191
|
+
filters = {"indices": indices, "names": names, "geometries": geometries}
|
|
192
|
+
ds = ds.filter(**filters)
|
|
193
|
+
|
|
194
|
+
# filter on index
|
|
195
|
+
if index is not None:
|
|
196
|
+
return decode(ds[index], **kwargs)
|
|
197
|
+
|
|
198
|
+
ds = cls([decode(e, **kwargs) for e in ds], **kwargs)
|
|
199
|
+
|
|
200
|
+
if wrapper:
|
|
201
|
+
return ds.to(wrapper)
|
|
202
|
+
else:
|
|
203
|
+
return ds
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .geopandas_driver import to_geopandas
|
|
2
|
+
from .lasio_driver import to_lasio
|
|
3
|
+
from .pyvista_driver import to_pyvista
|
|
4
|
+
|
|
5
|
+
Drivers = {
|
|
6
|
+
"vtk": to_pyvista,
|
|
7
|
+
"pyvista": to_pyvista,
|
|
8
|
+
"gdf": to_geopandas,
|
|
9
|
+
"geopandas": to_geopandas,
|
|
10
|
+
"las": to_lasio,
|
|
11
|
+
"lasio": to_lasio,
|
|
12
|
+
}
|
pygcd/drivers/_cells.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from itertools import chain
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def ravel_cells(nested_cells: List[List]) -> List:
|
|
6
|
+
"""Converts nested lists of indices into a flat list of connectivities.
|
|
7
|
+
|
|
8
|
+
e.g. [[0,1,2],[0,2,3]] -> [3,0,1,2,3,0,2,3]
|
|
9
|
+
"""
|
|
10
|
+
flat_connectivity = chain.from_iterable([(len(c), *c) for c in nested_cells])
|
|
11
|
+
return list(flat_connectivity)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def unravel_cells(flat_cells: List) -> List[List]:
|
|
15
|
+
"""Converts a flat list of connectivities into nested lists of indices.
|
|
16
|
+
|
|
17
|
+
e.g. [3,0,1,2,3,0,2,3] -> [[0,1,2],[0,2,3]]
|
|
18
|
+
"""
|
|
19
|
+
nested_cells = []
|
|
20
|
+
i, stop = 0, len(flat_cells)
|
|
21
|
+
while i < stop:
|
|
22
|
+
n = flat_cells[i]
|
|
23
|
+
i += 1
|
|
24
|
+
nested_cells.append([flat_cells[i : i + n]])
|
|
25
|
+
i += n
|
|
26
|
+
return nested_cells
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def nested_to_offconn(cells: List[List]) -> Tuple[List, List]:
|
|
30
|
+
"""Ravel cells with new-school VTK cells format.
|
|
31
|
+
|
|
32
|
+
e.g. [[0,1,2],[0,2,3]] -> [0,1,2,0,2,3], [0,4]
|
|
33
|
+
"""
|
|
34
|
+
connectivity, offsets = [], [0]
|
|
35
|
+
for cell in cells:
|
|
36
|
+
connectivity.extend(cell)
|
|
37
|
+
offsets.append(len(cell))
|
|
38
|
+
return offsets, connectivity
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def offconn_to_nested(offsets: List, connectivity: List) -> List[List]:
|
|
42
|
+
off, conn = offsets, connectivity
|
|
43
|
+
return [conn[i : i + 1] for i in off[:-1]]
|
pygcd/drivers/_utils.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from warnings import warn
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..objects import Chunk, Grid, Mesh, Object, Well
|
|
8
|
+
from ._utils import iterdict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_df(obj: Object) -> GeoDataFrame:
|
|
12
|
+
from geopandas import GeoDataFrame
|
|
13
|
+
|
|
14
|
+
if isinstance(obj, Mesh):
|
|
15
|
+
geometries = _cast_mesh(obj)
|
|
16
|
+
elif isinstance(obj, Well):
|
|
17
|
+
geometries = _cast_well(obj)
|
|
18
|
+
elif isinstance(obj, Grid):
|
|
19
|
+
geometries = _cast_grid(obj)
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError(f"Invalid object type: {type(obj)}")
|
|
22
|
+
|
|
23
|
+
if not isinstance(geometries, dict):
|
|
24
|
+
geometries = {obj.geometry.name: geometries}
|
|
25
|
+
|
|
26
|
+
n = len(geometries)
|
|
27
|
+
|
|
28
|
+
fields = obj.fields
|
|
29
|
+
crs = fields.pop("crs", {}) # handled separatly
|
|
30
|
+
epsg = crs.get("PROJECTION", "").strip('"')
|
|
31
|
+
fields["name"] = obj.name
|
|
32
|
+
fields["type"] = list(geometries.keys())
|
|
33
|
+
# flatten nested fields
|
|
34
|
+
fields = {k: [v] * n for k, v in iterdict(fields)}
|
|
35
|
+
gdf = GeoDataFrame(fields, geometry=list(geometries.values()), crs=epsg).set_index(
|
|
36
|
+
"name"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return gdf
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cast_mesh(mesh: Mesh) -> dict[Geometry]:
|
|
43
|
+
|
|
44
|
+
from shapely.geometry import (
|
|
45
|
+
LineString,
|
|
46
|
+
MultiLineString,
|
|
47
|
+
MultiPoint,
|
|
48
|
+
MultiPolygon,
|
|
49
|
+
Point,
|
|
50
|
+
Polygon,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert isinstance(mesh, Mesh)
|
|
54
|
+
|
|
55
|
+
if mesh.geometry.name == "VSet":
|
|
56
|
+
geometry = (
|
|
57
|
+
MultiPoint(mesh.points) if len(mesh.points) > 1 else Point(mesh.points)
|
|
58
|
+
)
|
|
59
|
+
elif mesh.geometry.name == "PLine":
|
|
60
|
+
nodes = [[mesh.points[i] for i in cell] for cell in mesh.cells]
|
|
61
|
+
geometry = (
|
|
62
|
+
MultiLineString([LineString(pts) for pts in nodes])
|
|
63
|
+
if len(nodes) > 1
|
|
64
|
+
else LineString(nodes[0])
|
|
65
|
+
)
|
|
66
|
+
elif mesh.geometry.name == "TSurf":
|
|
67
|
+
points = np.asarray(mesh.points, dtype=float)
|
|
68
|
+
cells = np.asarray(mesh.cells, dtype=int)
|
|
69
|
+
nodes = points[
|
|
70
|
+
cells
|
|
71
|
+
] # 10x faster than list comprehesion `[[points[i] for i in cell] for cell in obj.cells]`
|
|
72
|
+
geometry = (
|
|
73
|
+
MultiPolygon([Polygon(pts) for pts in nodes])
|
|
74
|
+
if len(nodes) > 1
|
|
75
|
+
else Polygon(nodes[0])
|
|
76
|
+
)
|
|
77
|
+
else: # FIXME: how to cast 'TSolid' to 2D format ?!
|
|
78
|
+
raise NotImplementedError(
|
|
79
|
+
f"Ignoring unsupported GOCAD object: {mesh.geometry.name} ('{mesh.name}')"
|
|
80
|
+
)
|
|
81
|
+
return {mesh.geometry.name: geometry}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _cast_well(well: Well) -> dict[Geometry]:
|
|
85
|
+
|
|
86
|
+
from shapely.geometry import LineString, MultiLineString, MultiPoint, Point
|
|
87
|
+
|
|
88
|
+
assert isinstance(well, Well)
|
|
89
|
+
|
|
90
|
+
geometries = {}
|
|
91
|
+
if well.collar:
|
|
92
|
+
geometries["WellCollars"] = Point(well.collar)
|
|
93
|
+
if well.path:
|
|
94
|
+
geometries["WellPath"] = LineString(np.asarray(well.path)[:, :3])
|
|
95
|
+
if well.markers:
|
|
96
|
+
geometries["WellMarkers"] = MultiPoint(
|
|
97
|
+
[well.coords(m.zm) for m in well.markers]
|
|
98
|
+
)
|
|
99
|
+
if well.zones:
|
|
100
|
+
geometries["WellZones"] = MultiLineString(
|
|
101
|
+
[
|
|
102
|
+
[
|
|
103
|
+
(well.coords(z.zfrom), well.coords(z.zto)),
|
|
104
|
+
]
|
|
105
|
+
for z in well.zones
|
|
106
|
+
]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return geometries
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _cast_grid(grid: Grid) -> dict[Geometry]:
|
|
113
|
+
|
|
114
|
+
raise NotImplementedError(
|
|
115
|
+
f"Ignoring unsupported GOCAD object: {grid.geometry.name} ('{grid.name}')"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return {mesh.geometry.name: geometry}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def to_geopandas(obj):
|
|
122
|
+
|
|
123
|
+
from geopandas import GeoDataFrame
|
|
124
|
+
from pandas import concat
|
|
125
|
+
|
|
126
|
+
if isinstance(obj, list):
|
|
127
|
+
pieces = []
|
|
128
|
+
for piece in obj:
|
|
129
|
+
try:
|
|
130
|
+
pieces.append(to_geopandas(piece))
|
|
131
|
+
except NotImplementedError:
|
|
132
|
+
msg = f"Ignoring unsupported GOCAD object: {piece.geometry.name} ('{piece.name}')"
|
|
133
|
+
warn(
|
|
134
|
+
msg,
|
|
135
|
+
Warning,
|
|
136
|
+
)
|
|
137
|
+
return concat(pieces) if pieces else GeoDataFrame()
|
|
138
|
+
|
|
139
|
+
assert isinstance(obj, Object)
|
|
140
|
+
|
|
141
|
+
# build GeoDataFrame
|
|
142
|
+
if isinstance(obj, Chunk):
|
|
143
|
+
obj = obj.decode()
|
|
144
|
+
assert isinstance(obj, Object), f"Invalid object type: {type(obj)}"
|
|
145
|
+
|
|
146
|
+
gdf = build_df(obj)
|
|
147
|
+
|
|
148
|
+
return gdf
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from logging import warning
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from ..objects import Chunk, Layer, Object, Well
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _cast(well: Well):
|
|
8
|
+
|
|
9
|
+
if isinstance(well, str):
|
|
10
|
+
well = Chunk(well)
|
|
11
|
+
if isinstance(well, Chunk):
|
|
12
|
+
well = well.decode()
|
|
13
|
+
if not isinstance(well, Well):
|
|
14
|
+
raise NotImplementedError(f"Wrong geometry type: {well.geometry.name}")
|
|
15
|
+
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from lasio import LASFile
|
|
18
|
+
|
|
19
|
+
index = "DEPT"
|
|
20
|
+
|
|
21
|
+
las = LASFile()
|
|
22
|
+
|
|
23
|
+
for mnem in las.well.keys():
|
|
24
|
+
las.well[mnem] = well.fields.get(mnem.lower(), "")
|
|
25
|
+
|
|
26
|
+
las.well.WELL = well.name
|
|
27
|
+
las.well.SRVC = "GOCAD"
|
|
28
|
+
las.well.LOC = " ".join(str(e) for e in well.collar)
|
|
29
|
+
|
|
30
|
+
if len(well.curves) == 1:
|
|
31
|
+
for curve in well.curves:
|
|
32
|
+
las.append_curve(index, curve.zm, unit=curve.z_unit)
|
|
33
|
+
las.append_curve(curve.name, curve.values, unit=curve.v_unit)
|
|
34
|
+
elif len(well.curves) > 1:
|
|
35
|
+
curves = pd.concat(
|
|
36
|
+
[
|
|
37
|
+
pd.DataFrame({index: curve.zm, curve.name: curve.values}).set_index(
|
|
38
|
+
index
|
|
39
|
+
)
|
|
40
|
+
for curve in well.curves
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
las.set_data(curves)
|
|
44
|
+
for l, w in zip(las.curves[1:], well.curves):
|
|
45
|
+
l.unit = w.v_unit
|
|
46
|
+
|
|
47
|
+
extras = []
|
|
48
|
+
crs = well.fields.get("crs", {}).get("projection", None)
|
|
49
|
+
if crs:
|
|
50
|
+
extras.append(f"CRS: {crs}")
|
|
51
|
+
if well.path:
|
|
52
|
+
path = "\n\t".join(str(e) for e in well.path)
|
|
53
|
+
path = path.replace("(", "").replace(")", "")
|
|
54
|
+
extras.append(f"WELL PATH: (X Y Z)\n\t{path}")
|
|
55
|
+
|
|
56
|
+
las.other = "\n".join(extras)
|
|
57
|
+
|
|
58
|
+
return las
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def to_lasio(obj: Union[Object, list]):
|
|
62
|
+
|
|
63
|
+
if isinstance(obj, (str, Layer)):
|
|
64
|
+
obj = [obj]
|
|
65
|
+
|
|
66
|
+
files = []
|
|
67
|
+
for piece in obj:
|
|
68
|
+
try:
|
|
69
|
+
files.append(_cast(piece))
|
|
70
|
+
except NotImplementedError:
|
|
71
|
+
warning(
|
|
72
|
+
f"Ignoring unsupported GOCAD object: {piece.geometry.name} ('{piece.name}')"
|
|
73
|
+
)
|
|
74
|
+
return files
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
from warnings import warn
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..objects import Chunk, Grid, Mesh, Object, Well
|
|
7
|
+
from ._cells import ravel_cells
|
|
8
|
+
from ._utils import iterdict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def reencode(s):
|
|
12
|
+
return str(s.encode("ascii", "replace"), "ascii")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _cast_mesh(mesh: Mesh):
|
|
16
|
+
assert isinstance(mesh, Mesh)
|
|
17
|
+
|
|
18
|
+
from pyvista import CellType, PolyData, UnstructuredGrid
|
|
19
|
+
|
|
20
|
+
if mesh.geometry.name == "VSet":
|
|
21
|
+
part = PolyData(mesh.points)
|
|
22
|
+
elif mesh.geometry.name == "PLine":
|
|
23
|
+
part = PolyData(mesh.points, lines=ravel_cells(mesh.cells))
|
|
24
|
+
elif mesh.geometry.name == "TSurf":
|
|
25
|
+
part = PolyData(mesh.points, faces=ravel_cells(mesh.cells))
|
|
26
|
+
elif mesh.geometry.name == "TSolid":
|
|
27
|
+
part = UnstructuredGrid(
|
|
28
|
+
ravel_cells(mesh.cells), [CellType.TETRA] * len(mesh.cells), mesh.points
|
|
29
|
+
)
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError(f"Wrong geometry type: {mesh.geometry.name}")
|
|
32
|
+
|
|
33
|
+
# record cell_data
|
|
34
|
+
for name, value in mesh.cell_data.items():
|
|
35
|
+
part[reencode(name)] = value
|
|
36
|
+
# record point_data
|
|
37
|
+
for name, value in mesh.point_data.items():
|
|
38
|
+
part[reencode(name)] = value
|
|
39
|
+
|
|
40
|
+
return part
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cast_well(well: Well):
|
|
44
|
+
|
|
45
|
+
assert isinstance(well, Well)
|
|
46
|
+
|
|
47
|
+
# raise NotImplementedError(
|
|
48
|
+
# f"Ignoring unsupported GOCAD object: {well.geometry.name} ('{well.name}')"
|
|
49
|
+
# )
|
|
50
|
+
|
|
51
|
+
from pyvista import MultiBlock, PolyData
|
|
52
|
+
|
|
53
|
+
vtm = MultiBlock()
|
|
54
|
+
# vtm["collar"] = PolyData(well.collar)
|
|
55
|
+
# if len(well.markers) > 0:
|
|
56
|
+
# vtm["markers"] = PolyData(well.coords([wm.zm for wm in well.markers]))
|
|
57
|
+
# for prop in WellMarker.__dataclass_fields__.keys(): # forward properties
|
|
58
|
+
# array = np.array([wm.__getattribute__(prop) for wm in well.markers])
|
|
59
|
+
# if array.dtype.type is np.str_:
|
|
60
|
+
# array = np.array([reencode(el) for el in array])
|
|
61
|
+
# vtm["markers"][reencode(prop)] = array
|
|
62
|
+
# if len(well.zones) > 0:
|
|
63
|
+
# _from = well.coords([s.zfrom for s in well.zones])
|
|
64
|
+
# _to = well.coords([s.zto for s in well.zones])
|
|
65
|
+
# pts = np.empty((_from.size + _to.size,), dtype=float)
|
|
66
|
+
# pts[0::2], pts[1::2] = _from, _to
|
|
67
|
+
# lines = []
|
|
68
|
+
# for i in range(len(_from)):
|
|
69
|
+
# lines += [2, 2 * i, 2 * i + 1]
|
|
70
|
+
# vtm["stratum"] = PolyData(pts, lines=lines)
|
|
71
|
+
# for prop in WellZone.__dataclass_fields__.keys(): # forward properties
|
|
72
|
+
# array = np.array([ws.__getattribute__(prop) for ws in well.zones])
|
|
73
|
+
# if array.dtype.type is np.str_:
|
|
74
|
+
# array = np.array([reencode(el) for el in array])
|
|
75
|
+
# vtm["stratum"][reencode(prop)] = array
|
|
76
|
+
if len(well.path) > 0:
|
|
77
|
+
pts = np.asarray(well.path, float)[:, :3]
|
|
78
|
+
n = len(pts)
|
|
79
|
+
vtm["path"] = PolyData(pts, lines=[n] + list(range(n)))
|
|
80
|
+
return vtm
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cast_grid(grid: Grid):
|
|
84
|
+
from pyvista import StructuredGrid
|
|
85
|
+
|
|
86
|
+
raise NotImplementedError(
|
|
87
|
+
f"Ignoring unsupported GOCAD object: {grid.geometry.name} ('{grid.name}')"
|
|
88
|
+
)
|
|
89
|
+
return StructuredGrid()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def to_pyvista(obj: Union[Object, list]):
|
|
93
|
+
from pyvista import DataObject, MultiBlock
|
|
94
|
+
|
|
95
|
+
if isinstance(obj, list):
|
|
96
|
+
assert len(obj) > 0
|
|
97
|
+
vtm = MultiBlock()
|
|
98
|
+
for piece in obj:
|
|
99
|
+
try:
|
|
100
|
+
part = to_pyvista(piece)
|
|
101
|
+
name = piece.name
|
|
102
|
+
if name and name not in vtm.keys():
|
|
103
|
+
vtm[name] = part
|
|
104
|
+
else:
|
|
105
|
+
vtm.append(part)
|
|
106
|
+
except NotImplementedError:
|
|
107
|
+
msg = f"Ignoring unsupported GOCAD object: {piece.geometry.name} ('{piece.name}')"
|
|
108
|
+
warn(
|
|
109
|
+
msg,
|
|
110
|
+
Warning,
|
|
111
|
+
)
|
|
112
|
+
return vtm
|
|
113
|
+
|
|
114
|
+
# build pv.DataSet
|
|
115
|
+
if isinstance(obj, Chunk):
|
|
116
|
+
obj = obj.decode()
|
|
117
|
+
assert isinstance(obj, Object), f"Invalid object type: {type(obj)}"
|
|
118
|
+
|
|
119
|
+
if isinstance(obj, Mesh): # -> PolyData or UnstructuredGrid
|
|
120
|
+
part = _cast_mesh(obj)
|
|
121
|
+
elif isinstance(obj, Well): # -> MultiBlock (of PolyData)
|
|
122
|
+
part = _cast_well(obj)
|
|
123
|
+
elif isinstance(obj, Grid): # -> StructuredGrid
|
|
124
|
+
part = _cast_grid(obj)
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError(f"Invalid object type: {type(obj)}")
|
|
127
|
+
|
|
128
|
+
assert isinstance(part, DataObject), f"Invalid object type: {type(obj)}"
|
|
129
|
+
|
|
130
|
+
# record field_data
|
|
131
|
+
part.add_field_data([reencode(obj.name)], "name")
|
|
132
|
+
part.add_field_data([reencode(obj.geometry.name)], "geometry")
|
|
133
|
+
for name, value in iterdict(obj.fields):
|
|
134
|
+
name = reencode(name).strip("*").replace("*", ":")
|
|
135
|
+
if name in part.field_data.keys():
|
|
136
|
+
warn(f"Duplicated property will be overwritten: {name}", Warning)
|
|
137
|
+
if isinstance(value, str):
|
|
138
|
+
value = [reencode(value)]
|
|
139
|
+
elif not hasattr(value, "__iter__"):
|
|
140
|
+
value = [value]
|
|
141
|
+
part.add_field_data(value, name)
|
|
142
|
+
|
|
143
|
+
return part
|