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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractclassmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from logging import warning
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import read_header
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Geometry(Enum):
|
|
13
|
+
"""GOCAD object geometry type."""
|
|
14
|
+
|
|
15
|
+
Invalid = -1
|
|
16
|
+
# 0 <= mesh data < 10
|
|
17
|
+
VSet = 0
|
|
18
|
+
PLine = 1
|
|
19
|
+
TSurf = 2
|
|
20
|
+
TSolid = 3
|
|
21
|
+
# well data == 10
|
|
22
|
+
Well = 10
|
|
23
|
+
# grid data > 10
|
|
24
|
+
Voxet = 11
|
|
25
|
+
GSurf = 12
|
|
26
|
+
SGrid = 13
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def names(cls):
|
|
30
|
+
return [el.name for el in cls]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def instance(self):
|
|
34
|
+
from . import Grid, Mesh, Well
|
|
35
|
+
|
|
36
|
+
if self.value > 10:
|
|
37
|
+
return Grid
|
|
38
|
+
elif self.value == 10:
|
|
39
|
+
return Well
|
|
40
|
+
elif self.value >= 0:
|
|
41
|
+
return Mesh
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(f"{self.name} geometry !")
|
|
44
|
+
|
|
45
|
+
def read(self, text, *args, **kwargs):
|
|
46
|
+
return self.instance.from_chunk(text, *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Layer:
|
|
51
|
+
"""Generic GOCAD object (abstract class)"""
|
|
52
|
+
|
|
53
|
+
name: str = "Unknown object"
|
|
54
|
+
geometry: Geometry = Geometry(-1)
|
|
55
|
+
version: str = "?"
|
|
56
|
+
fields: dict = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
"""Make it abstract, any Layer() will fail"""
|
|
60
|
+
if self.__class__ == Layer:
|
|
61
|
+
raise TypeError("Cannot instantiate abstract class.")
|
|
62
|
+
|
|
63
|
+
def __getattr__(self, name: str) -> Any:
|
|
64
|
+
"""Make self.fields accessible as class attributes.
|
|
65
|
+
|
|
66
|
+
Any failed `self.name` attempt will trigger a lookup in
|
|
67
|
+
self.fields.keys() and return self.fields[name] if match.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name (str): Field name.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
AttributeError: Non existing keys will raise AttributeError.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Any: Identical to `self.fields[name]`.
|
|
77
|
+
"""
|
|
78
|
+
if name in self.__getattribute__("fields"):
|
|
79
|
+
return self.fields[name]
|
|
80
|
+
else:
|
|
81
|
+
raise AttributeError(f"'{self.__class__}' object has no attribute '{name}'")
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str:
|
|
84
|
+
"""Textual representation of an object.
|
|
85
|
+
|
|
86
|
+
self.geometry.name ("self.name")
|
|
87
|
+
N Fields: len(self.fields)
|
|
88
|
+
-> child classes will add informations
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: Object string representation.
|
|
92
|
+
"""
|
|
93
|
+
s = f'{self.geometry.name} ("{self.name}")\n'
|
|
94
|
+
s += f"\tN Fields:\t{len(self.fields)}"
|
|
95
|
+
return s
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Object(Layer):
|
|
99
|
+
"""Geometric object (abstract class)"""
|
|
100
|
+
|
|
101
|
+
def __post_init__(self):
|
|
102
|
+
"""make it abstract"""
|
|
103
|
+
if self.__class__ == Object:
|
|
104
|
+
raise TypeError("Cannot instantiate abstract class.")
|
|
105
|
+
|
|
106
|
+
@abstractclassmethod
|
|
107
|
+
def from_chunk(cls, chunk: str, *args, **kwargs) -> Object:
|
|
108
|
+
return cls()
|
|
109
|
+
|
|
110
|
+
def to(self, wrapper: str):
|
|
111
|
+
from ..drivers import Drivers
|
|
112
|
+
|
|
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
|
+
|
|
120
|
+
class Chunk(Layer):
|
|
121
|
+
"""Identified object (i.e. decoded header)"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, raw: str):
|
|
124
|
+
self.load(raw)
|
|
125
|
+
|
|
126
|
+
def load(self, chunk: str):
|
|
127
|
+
header = read_header(chunk)
|
|
128
|
+
self.name = header.pop("name", self.name)
|
|
129
|
+
self.geometry = Geometry[header.pop("geometry", self.geometry.name)]
|
|
130
|
+
self.version = header.pop("version", self.version)
|
|
131
|
+
self.fields = header
|
|
132
|
+
self.chunk = chunk
|
|
133
|
+
|
|
134
|
+
def read(self, *args, **kwargs) -> Object:
|
|
135
|
+
new = self.geometry.read(self.chunk, *args, **kwargs)
|
|
136
|
+
for attr in self.__dataclass_fields__.keys():
|
|
137
|
+
if hasattr(new, attr):
|
|
138
|
+
new.__setattr__(attr, self.__getattribute__(attr))
|
|
139
|
+
return new
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def decode(chunk: str, *args, **kwargs) -> Chunk:
|
|
143
|
+
return Chunk(chunk).read(*args, **kwargs)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def decode(chunk, *args, **kwargs):
|
|
147
|
+
if isinstance(chunk, str):
|
|
148
|
+
return Chunk.decode(str, *args, **kwargs)
|
|
149
|
+
if isinstance(chunk, Chunk):
|
|
150
|
+
return chunk.read(*args, **kwargs)
|
|
151
|
+
if isinstance(chunk, Object):
|
|
152
|
+
return chunk
|
|
153
|
+
warning(f"Ignoring unsupported GOCAD object: {chunk})")
|
|
154
|
+
return None
|
pygcd/objects/grid.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from . import Object, read_grid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Grid(Object):
|
|
10
|
+
"""Grid-geometry type object (Voxet, GSurf, SGrid)"""
|
|
11
|
+
|
|
12
|
+
origin: tuple[float] = field(default_factory=tuple)
|
|
13
|
+
dimension: tuple[float] = field(default_factory=tuple)
|
|
14
|
+
spacing: tuple[float] = field(default_factory=tuple)
|
|
15
|
+
data: list[float] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_chunk(cls, chunk: str, *args, **kwargs) -> Object:
|
|
19
|
+
self = cls()
|
|
20
|
+
params, self.data = read_grid(chunk, *args, **kwargs)
|
|
21
|
+
self.origin, self.dimension, self.spacing = params
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
s = super().__repr__() + "\n"
|
|
26
|
+
s += f"\tN Points:\t{len(self.points)}\n"
|
|
27
|
+
s += f"\tN Cells:\t{len(self.cells)}\n"
|
|
28
|
+
s += f"\tN Arrays:\t{len(self.data)}\n"
|
|
29
|
+
return s
|
pygcd/objects/mesh.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from . import Object, read_mesh
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Mesh(Object):
|
|
10
|
+
"""Mesh-geometry type object (VSet, TSurf, TSolid)"""
|
|
11
|
+
|
|
12
|
+
points: list[tuple[float]] = field(default_factory=list)
|
|
13
|
+
cells: list[list[int]] = field(default_factory=list)
|
|
14
|
+
point_data: dict[str, list[str]] = field(default_factory=dict)
|
|
15
|
+
cell_data: dict[str, list[str]] = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_chunk(cls, chunk, *args, **kwargs) -> Object:
|
|
19
|
+
self = cls()
|
|
20
|
+
self.points, self.cells, self.point_data, self.cell_data = read_mesh(
|
|
21
|
+
chunk, *args, **kwargs
|
|
22
|
+
)
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
# WIP: Make sure Layer.__getattr__ is called first !
|
|
26
|
+
# def __getattr__(self, name: str) -> Any:
|
|
27
|
+
# """Make self.data accessible as class attributes.
|
|
28
|
+
|
|
29
|
+
# Any failed `self.name` attempt will trigger a lookup in
|
|
30
|
+
# self.data.keys() and return self.data[name] if match.
|
|
31
|
+
|
|
32
|
+
# Args:
|
|
33
|
+
# name (str): Point data attribute.
|
|
34
|
+
|
|
35
|
+
# Raises:
|
|
36
|
+
# AttributeError: Non existing keys will raise AttributeError.
|
|
37
|
+
|
|
38
|
+
# Returns:
|
|
39
|
+
# Any: Identical to `self.data[name]`.
|
|
40
|
+
# """
|
|
41
|
+
# if name in self.__getattribute__('data'):
|
|
42
|
+
# return self.data[name]
|
|
43
|
+
# else:
|
|
44
|
+
# raise AttributeError(f"'{self.__class__}' object has no attribute '{name}'")
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
s = super().__repr__() + "\n"
|
|
48
|
+
s += f"\tN Points:\t{len(self.points)}\n"
|
|
49
|
+
s += f"\tN Cells:\t{len(self.cells)}\n"
|
|
50
|
+
s += f"\tN Arrays:\t{len(self.arrays)}"
|
|
51
|
+
return s
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def arrays(self):
|
|
55
|
+
return list(self.point_data.keys()) + list(self.cell_data.keys())
|
pygcd/objects/well.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
from scipy.interpolate import splev, splprep
|
|
9
|
+
|
|
10
|
+
from . import Object, WellCurve, WellMarker, WellZone, read_well
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Well(Object):
|
|
15
|
+
"""Well object"""
|
|
16
|
+
|
|
17
|
+
collar: tuple[float] = field(default_factory=tuple)
|
|
18
|
+
path: list[tuple[float]] = field(default_factory=list)
|
|
19
|
+
markers: list[WellMarker] = field(default_factory=list)
|
|
20
|
+
zones: list[WellZone] = field(default_factory=list)
|
|
21
|
+
curves: list[WellCurve] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_chunk(cls, chunk, *args, **kwargs) -> Object:
|
|
25
|
+
self = cls()
|
|
26
|
+
self.collar, self.path, self.markers, self.zones, self.curves = read_well(
|
|
27
|
+
chunk, *args, **kwargs
|
|
28
|
+
)
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __setattr__(self, __name: str, __value: list[tuple[float]]) -> None:
|
|
32
|
+
"""Manage cached properties"""
|
|
33
|
+
if __name == "path" and "spline" in self.__dict__:
|
|
34
|
+
del self.__dict__["spline"]
|
|
35
|
+
return super().__setattr__(__name, __value)
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
s = super().__repr__() + "\n"
|
|
39
|
+
s += f"\tCollar:\t{self.collar}\n"
|
|
40
|
+
s += f"\tN Path:\t{len(self.path)}\n"
|
|
41
|
+
s += f"\tN Markers:\t{len(self.markers)}\n"
|
|
42
|
+
s += f"\tN Strata:\t{len(self.zones)}\n"
|
|
43
|
+
s += f"\tN Curves:\t{len(self.curves)}\n"
|
|
44
|
+
return s
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def spline(self):
|
|
48
|
+
points = np.array(self.path, float)
|
|
49
|
+
if len(points) == 0: # no path : use vertical hole from collar
|
|
50
|
+
points = np.array((*self.collar, 0), float)
|
|
51
|
+
if len(points) == 0: # empty well ... raise ValueError
|
|
52
|
+
raise ValueError(f"Well is empty: {self}")
|
|
53
|
+
|
|
54
|
+
# remove duplicates (should not exists ... but ...)
|
|
55
|
+
points = np.unique(points, axis=0)
|
|
56
|
+
# single point path : use vertical hole
|
|
57
|
+
if len(points) == 1:
|
|
58
|
+
ref = points.squeeze()
|
|
59
|
+
|
|
60
|
+
def spline(zm): # wrapper around interpolator
|
|
61
|
+
xy = np.tile(ref[:2], (np.asarray(zm).size, 1))
|
|
62
|
+
z = ref[2] + ref[3] - zm
|
|
63
|
+
return np.c_[xy, z]
|
|
64
|
+
|
|
65
|
+
# multi point path : spline interpolation
|
|
66
|
+
else:
|
|
67
|
+
# splprep.u must be sorted
|
|
68
|
+
order = np.argsort(points[:, -1])
|
|
69
|
+
points = points[order, :]
|
|
70
|
+
# interpolate using spline
|
|
71
|
+
x = [points[:, 0], points[:, 1], points[:, 2]]
|
|
72
|
+
u = points[:, 3].flatten()
|
|
73
|
+
k = min(len(points) - 1, 3)
|
|
74
|
+
tck, _ = splprep(x, u=u, k=k, s=0)
|
|
75
|
+
|
|
76
|
+
def spline(zm): # wrapper around interpolator
|
|
77
|
+
return np.column_stack(splev(zm, tck))
|
|
78
|
+
|
|
79
|
+
return spline
|
|
80
|
+
|
|
81
|
+
def coords(self, zm: ArrayLike) -> ArrayLike:
|
|
82
|
+
u = np.asarray(zm, float).flatten()
|
|
83
|
+
interp = self.spline
|
|
84
|
+
return interp(u)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
"""GOCAD Object file template:
|
|
4
|
+
|
|
5
|
+
GOCAD <type> <version>
|
|
6
|
+
HEADER {
|
|
7
|
+
name: <name>
|
|
8
|
+
[<key>: <value>]
|
|
9
|
+
}
|
|
10
|
+
[PROPERTIES <name> ... <name>]
|
|
11
|
+
ATOM <ID> <X> <Y> <Z> [<PV> ...]
|
|
12
|
+
[<SUBSET_TYPE>]
|
|
13
|
+
[<CELL_TYPE> <ATOM> ... <ATOM>]
|
|
14
|
+
END
|
|
15
|
+
|
|
16
|
+
based on : http://paulbourke.net/dataformats/gocad/gocad.pdf
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
OBJECT = re.compile(r"(?P<object>GOCAD.*?END)\s*?$", re.M | re.S)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_objects(raw: str) -> list:
|
|
23
|
+
"""Split raw text into object chunks"""
|
|
24
|
+
return OBJECT.findall(raw)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# geometry readers
|
|
28
|
+
from .grid import read_grid
|
|
29
|
+
from .header import read_header
|
|
30
|
+
from .mesh import read_mesh
|
|
31
|
+
from .well import read_well
|
pygcd/readers/_utils.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
def safesplit(string: str, splitchar: str = " ", escaping: str = "'\"") -> list:
|
|
2
|
+
"""Split string with escaping capabilities.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
string (str): The string to parse.
|
|
6
|
+
splitchar (str): The separator.
|
|
7
|
+
ignorechar (str): The character escaping splits.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
list: Splitted string
|
|
11
|
+
"""
|
|
12
|
+
if splitchar in escaping:
|
|
13
|
+
raise ValueError("Cannot escape on splitting character !")
|
|
14
|
+
|
|
15
|
+
result = []
|
|
16
|
+
buffer = ""
|
|
17
|
+
escape = ""
|
|
18
|
+
|
|
19
|
+
for c in string:
|
|
20
|
+
if c in escaping:
|
|
21
|
+
if not escape:
|
|
22
|
+
escape = c
|
|
23
|
+
continue
|
|
24
|
+
elif escape == c:
|
|
25
|
+
escape = ""
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
if c == splitchar and not escape:
|
|
29
|
+
if buffer:
|
|
30
|
+
result.append(buffer)
|
|
31
|
+
buffer = ""
|
|
32
|
+
else:
|
|
33
|
+
continue
|
|
34
|
+
else:
|
|
35
|
+
buffer += c
|
|
36
|
+
|
|
37
|
+
if buffer:
|
|
38
|
+
result.append(buffer)
|
|
39
|
+
|
|
40
|
+
return result
|
pygcd/readers/grid.py
ADDED
pygcd/readers/header.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ._utils import safesplit
|
|
4
|
+
|
|
5
|
+
# regex to parse ascii files
|
|
6
|
+
|
|
7
|
+
HEADER = re.compile(r"HEADER\s*?{\s*?(?P<header>.*?)\s*?}", re.M | re.S)
|
|
8
|
+
HDR = re.compile(r"HDR\s+(?P<property>.*?)\s*?$", re.M | re.S)
|
|
9
|
+
CRS = re.compile(
|
|
10
|
+
r"GOCAD_ORIGINAL_COORDINATE_SYSTEM(?P<crs>.+?)END_ORIGINAL_COORDINATE_SYSTEM",
|
|
11
|
+
re.M | re.S,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_properties(block: str) -> dict:
|
|
16
|
+
"""Parse GOCAD Object attributes.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
block (str): Single GOCAD Object string "GOCAD ... END".
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
dict: Object attributes.
|
|
23
|
+
"""
|
|
24
|
+
attribs = {}
|
|
25
|
+
# get header block(s) and lines
|
|
26
|
+
properties = "\n".join(HEADER.findall(block) + HDR.findall(block))
|
|
27
|
+
for line in properties.splitlines():
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if not line:
|
|
30
|
+
continue
|
|
31
|
+
key, value = line.split(":")
|
|
32
|
+
key = key.strip("*").strip().lower()
|
|
33
|
+
value = value.strip()
|
|
34
|
+
attribs[key] = value
|
|
35
|
+
return attribs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_coordinate_system(block: str) -> dict:
|
|
39
|
+
crs = {}
|
|
40
|
+
match = CRS.search(block)
|
|
41
|
+
if match:
|
|
42
|
+
for line in match["crs"].strip().splitlines():
|
|
43
|
+
key, *value = safesplit(line)
|
|
44
|
+
crs[key.lower()] = " ".join(value)
|
|
45
|
+
return crs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_geologic_information(block: str) -> dict:
|
|
49
|
+
info = {}
|
|
50
|
+
flags = ("GEOLOGICAL_TYPE", "GEOLOGICAL_FEATURE")
|
|
51
|
+
for flag in flags:
|
|
52
|
+
match = re.search(flag + r"\s+?(.+?)\s*?$", block, re.MULTILINE)
|
|
53
|
+
if match:
|
|
54
|
+
info[flag.lower()] = match.group(1)
|
|
55
|
+
strati = re.search(r"STRATIGRAPHIC_POSITION\s+?(.+?)\s*?$", block, re.MULTILINE)
|
|
56
|
+
if strati:
|
|
57
|
+
age, time = strati.group(1).split()
|
|
58
|
+
info["stratigraphic_age"], info["stratigraphic_time"] = age, float(time)
|
|
59
|
+
return info
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_header(block: str, *args, **kwargs):
|
|
63
|
+
block = block.strip()
|
|
64
|
+
if not block.startswith("GOCAD "):
|
|
65
|
+
raise OSError("Invalid GOCAD object")
|
|
66
|
+
|
|
67
|
+
first, block = block.split("\n", 1)
|
|
68
|
+
_, geometry, version = first.split()
|
|
69
|
+
|
|
70
|
+
attributes = _parse_properties(block)
|
|
71
|
+
header = {
|
|
72
|
+
"name": attributes.pop("name", "Unknown block"),
|
|
73
|
+
"geometry": geometry,
|
|
74
|
+
"version": version,
|
|
75
|
+
}
|
|
76
|
+
header.update(_parse_geologic_information(block))
|
|
77
|
+
header["crs"] = _parse_coordinate_system(block)
|
|
78
|
+
header.update(attributes)
|
|
79
|
+
|
|
80
|
+
return header
|
pygcd/readers/mesh.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
POINTS = ("VRTX", "PVRTX", "ATOM", "PATOM")
|
|
2
|
+
FIELDS = (
|
|
3
|
+
"PROPERTIES",
|
|
4
|
+
"FIELDS",
|
|
5
|
+
"NO_DATA_VALUES",
|
|
6
|
+
"ESIZES",
|
|
7
|
+
) # ignore "UNITS" since it wont be forwarded
|
|
8
|
+
CELLS = ("SEG", "TRGL", "TETRA")
|
|
9
|
+
SEP = ("ILINE", "TFACE", "TVOLUME")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_mesh(block: str, *args, **kwargs):
|
|
13
|
+
nbp, nbc = 0, 0
|
|
14
|
+
points, cells = [], []
|
|
15
|
+
names, ndims, no_data_values, units = [], [], [], []
|
|
16
|
+
point_props, cell_splits = [], []
|
|
17
|
+
for line in block.splitlines():
|
|
18
|
+
line = line.strip()
|
|
19
|
+
if not line:
|
|
20
|
+
continue
|
|
21
|
+
what, *stuff = line.split()
|
|
22
|
+
if what in FIELDS:
|
|
23
|
+
if what in ("PROPERTIES", "FIELDS"):
|
|
24
|
+
assert not names, "Duplicated point properties definition"
|
|
25
|
+
names = stuff
|
|
26
|
+
elif what == "NO_DATA_VALUES":
|
|
27
|
+
assert not no_data_values, "Duplicated point data default values"
|
|
28
|
+
no_data_values = [float(x) for x in stuff]
|
|
29
|
+
if ndims:
|
|
30
|
+
no_data_values = [
|
|
31
|
+
x if y == 1 else [x] * y for x, y in zip(no_data_values, ndims)
|
|
32
|
+
]
|
|
33
|
+
elif what == "UNITS":
|
|
34
|
+
assert not units, "Duplicated point data units"
|
|
35
|
+
units = stuff
|
|
36
|
+
elif what == "ESIZES":
|
|
37
|
+
assert not ndims, "Duplicated point data dimensions"
|
|
38
|
+
ndims = (int(x) for x in stuff)
|
|
39
|
+
if no_data_values:
|
|
40
|
+
no_data_values = [
|
|
41
|
+
x if y == 1 else [x] * y for x, y in zip(no_data_values, ndims)
|
|
42
|
+
]
|
|
43
|
+
elif what in POINTS:
|
|
44
|
+
nbp += 1
|
|
45
|
+
i, *stuff = stuff
|
|
46
|
+
assert int(i) == nbp, f"Wrong indexing in points indices"
|
|
47
|
+
if what.endswith("VRTX"):
|
|
48
|
+
x, y, z, *props = stuff
|
|
49
|
+
points.append((float(x), float(y), float(z)))
|
|
50
|
+
else:
|
|
51
|
+
assert what.endswith("ATOM"), "Wrong point identifier"
|
|
52
|
+
idx, *props = stuff
|
|
53
|
+
points.append(points[int(idx) - 1])
|
|
54
|
+
if props or len(no_data_values) > 0:
|
|
55
|
+
point_props.append(props or no_data_values)
|
|
56
|
+
elif what in CELLS:
|
|
57
|
+
indices = [int(i) - 1 for i in stuff]
|
|
58
|
+
if what == "SEG": # merge zones to build lines
|
|
59
|
+
if not cells: # fist segment of first line
|
|
60
|
+
cells.append(indices)
|
|
61
|
+
nbc += 1
|
|
62
|
+
elif not cells[-1]: # new line detected
|
|
63
|
+
cells[-1] = indices
|
|
64
|
+
nbc += 1
|
|
65
|
+
else: # next zones extend last cell
|
|
66
|
+
assert cells[-1][-1] == indices[0], "Inconsistent PLine !"
|
|
67
|
+
cells[-1] += indices[1:]
|
|
68
|
+
else:
|
|
69
|
+
cells.append(indices)
|
|
70
|
+
nbc += 1
|
|
71
|
+
|
|
72
|
+
elif what in SEP:
|
|
73
|
+
cell_splits.append(nbc)
|
|
74
|
+
if what == "ILINE": # new line forces a new cell
|
|
75
|
+
cells.append([])
|
|
76
|
+
else:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if cells and not cells[-1]: # clean up possibly empty last cell (only with PLine)
|
|
80
|
+
cells = cells[:-1]
|
|
81
|
+
|
|
82
|
+
assert nbp == len(points), "Number of points missmatch counter"
|
|
83
|
+
assert nbc == len(cells), "Number of cells missmatch counter"
|
|
84
|
+
|
|
85
|
+
cell_data = {}
|
|
86
|
+
if cell_splits: # create a cell_data attribute with part index
|
|
87
|
+
cell_splits = cell_splits[::-1]
|
|
88
|
+
parts, rank, idx = [], 0, cell_splits.pop()
|
|
89
|
+
for i in range(nbc):
|
|
90
|
+
if i == idx and cell_splits:
|
|
91
|
+
idx = cell_splits.pop()
|
|
92
|
+
rank += 1
|
|
93
|
+
parts.append(rank)
|
|
94
|
+
cell_data = {"block_id": parts}
|
|
95
|
+
|
|
96
|
+
point_data = {}
|
|
97
|
+
if point_props:
|
|
98
|
+
for key, values in zip(names, zip(*point_props)):
|
|
99
|
+
point_data[key] = values
|
|
100
|
+
|
|
101
|
+
return points, cells, point_data, cell_data
|