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 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
+ }
@@ -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]]
@@ -0,0 +1,9 @@
1
+ def iterdict(d, prefix: str = ""):
2
+ queue = list(d.items())
3
+ while queue:
4
+ k, v = queue.pop()
5
+ if isinstance(v, dict):
6
+ for _k, _v in v.items():
7
+ queue.append((f"{k}:{_k}", _v))
8
+ else:
9
+ yield k, v
@@ -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
@@ -0,0 +1,6 @@
1
+ from ..readers import read_grid, read_header, read_mesh, read_well
2
+ from ..readers.well import WellCurve, WellMarker, WellZone
3
+ from .abstract import Chunk, Geometry, Layer, Object, decode
4
+ from .grid import Grid
5
+ from .mesh import Mesh
6
+ from .well import Well