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.
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ def read_grid(raw: str, *args, **kwargs):
2
+ raise NotImplementedError("Work in progress")
@@ -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