log2frame 0.2.1__tar.gz → 0.2.3__tar.gz

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.
Files changed (28) hide show
  1. {log2frame-0.2.1/src/log2frame.egg-info → log2frame-0.2.3}/PKG-INFO +9 -3
  2. {log2frame-0.2.1 → log2frame-0.2.3}/README.md +8 -2
  3. {log2frame-0.2.1 → log2frame-0.2.3}/pyproject.toml +1 -1
  4. log2frame-0.2.3/src/log2frame/__init__.py +162 -0
  5. log2frame-0.2.3/src/log2frame/dictionaries/units.py +69 -0
  6. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/dlis.py +41 -12
  7. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/las.py +46 -10
  8. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/lis.py +94 -10
  9. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/log.py +233 -7
  10. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/pack.py +218 -7
  11. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/rft.py +146 -2
  12. log2frame-0.2.3/src/log2frame/writer.py +192 -0
  13. {log2frame-0.2.1 → log2frame-0.2.3/src/log2frame.egg-info}/PKG-INFO +9 -3
  14. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame.egg-info/SOURCES.txt +3 -2
  15. {log2frame-0.2.1 → log2frame-0.2.3}/test/test_readers.py +1 -2
  16. log2frame-0.2.3/test/test_writer.py +215 -0
  17. log2frame-0.2.1/src/log2frame/__init__.py +0 -74
  18. log2frame-0.2.1/src/log2frame/debug.py +0 -5
  19. log2frame-0.2.1/src/log2frame/dictionaries/units.py +0 -28
  20. {log2frame-0.2.1 → log2frame-0.2.3}/LICENSE +0 -0
  21. {log2frame-0.2.1 → log2frame-0.2.3}/setup.cfg +0 -0
  22. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/dictionaries/__init__.py +0 -0
  23. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame/dictionaries/units_dictionary.json +0 -0
  24. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame.egg-info/dependency_links.txt +0 -0
  25. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame.egg-info/requires.txt +0 -0
  26. {log2frame-0.2.1 → log2frame-0.2.3}/src/log2frame.egg-info/top_level.txt +0 -0
  27. {log2frame-0.2.1 → log2frame-0.2.3}/test/test_log.py +0 -0
  28. {log2frame-0.2.1 → log2frame-0.2.3}/test/test_pack.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log2frame
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: A function to facilitate loading well log data, in las or dlis format, as dataframe.
5
5
  Author-email: Martin Carlos Araya <martinaraya@gmail.com>
6
6
  License: MIT License
@@ -51,8 +51,8 @@ Dynamic: license-file
51
51
 
52
52
  A function to read well log data in LAS, LIS and DLIS formats and extract curves data as DataFrame.
53
53
 
54
- The curves and header data are stored in an instance of `Log` class, designed to hold this data and operate with it.
55
- If multiple log files are read together, the __Log__ instances are packed in a `Pack` instance, designed to hold `Log` instances and operate with them.
54
+ The curves and header data are stored in an instance of ``Log`` class, designed to hold this data and operate with it. It provides convenient access to metadata like well name, UWI, and curve units (via ``.units`` or the safer ``.units_dict()``).
55
+ If multiple log files are read together, the __Log__ instances are packed in a ``Pack`` instance, designed to hold ``Log`` instances and operate with them.
56
56
 
57
57
  # unit-aware mode
58
58
  When `simpandas` is installed, `log2frame` can preserve curve units and index units through the `use_simpandas` option.
@@ -63,6 +63,12 @@ When `simpandas` is installed, `log2frame` can preserve curve units and index un
63
63
  # to read log files
64
64
  Simply call the function `read()` with the path or path pattern as argument:
65
65
 
66
+ # to export / write LAS files
67
+ Use `write_las()` to export a `Log` back to a LAS file:
68
+ - `log2frame.write_las( log, path_to_file )` — writes LAS 2.0 by default
69
+ - `log2frame.write_las( log, path_to_file, version=1.2 )` — LAS 1.2
70
+ - `log.to_las( path_to_file )` — equivalent convenience method on `Log`
71
+
66
72
  ## a single log file
67
73
  To load a single _.las_, _.lis_ or _.dlis_ file. The function will return a `Log` instance containing the data:
68
74
  - `log2frame.read( path_to_file )`
@@ -4,8 +4,8 @@
4
4
 
5
5
  A function to read well log data in LAS, LIS and DLIS formats and extract curves data as DataFrame.
6
6
 
7
- The curves and header data are stored in an instance of `Log` class, designed to hold this data and operate with it.
8
- If multiple log files are read together, the __Log__ instances are packed in a `Pack` instance, designed to hold `Log` instances and operate with them.
7
+ The curves and header data are stored in an instance of ``Log`` class, designed to hold this data and operate with it. It provides convenient access to metadata like well name, UWI, and curve units (via ``.units`` or the safer ``.units_dict()``).
8
+ If multiple log files are read together, the __Log__ instances are packed in a ``Pack`` instance, designed to hold ``Log`` instances and operate with them.
9
9
 
10
10
  # unit-aware mode
11
11
  When `simpandas` is installed, `log2frame` can preserve curve units and index units through the `use_simpandas` option.
@@ -16,6 +16,12 @@ When `simpandas` is installed, `log2frame` can preserve curve units and index un
16
16
  # to read log files
17
17
  Simply call the function `read()` with the path or path pattern as argument:
18
18
 
19
+ # to export / write LAS files
20
+ Use `write_las()` to export a `Log` back to a LAS file:
21
+ - `log2frame.write_las( log, path_to_file )` — writes LAS 2.0 by default
22
+ - `log2frame.write_las( log, path_to_file, version=1.2 )` — LAS 1.2
23
+ - `log.to_las( path_to_file )` — equivalent convenience method on `Log`
24
+
19
25
  ## a single log file
20
26
  To load a single _.las_, _.lis_ or _.dlis_ file. The function will return a `Log` instance containing the data:
21
27
  - `log2frame.read( path_to_file )`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "log2frame"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  authors = [
9
9
  { name="Martin Carlos Araya", email="martinaraya@gmail.com" },
10
10
  ]
@@ -0,0 +1,162 @@
1
+ # -*- coding: utf-8 -*-
2
+ """log2frame — well-log data reader and writer.
3
+
4
+ This package reads well-log files in LAS, DLIS, and LIS formats and
5
+ returns the curve data as unit-aware :class:`Log` (single file) or
6
+ :class:`Pack` (multiple files) containers built on top of
7
+ :class:`pandas.DataFrame` (or :class:`simpandas.SimDataFrame` when the
8
+ optional ``simpandas`` package is installed).
9
+
10
+ The package also provides :func:`write_las` to export any :class:`Log`
11
+ back to a LAS 1.2 / 2.0 file.
12
+
13
+ Public API
14
+ ----------
15
+ read(path, ...) Read one or many log files.
16
+ write_las(log, path) Write a Log to a LAS file.
17
+ concat(logs) Concatenate a list of Log objects into a single DataFrame.
18
+ """
19
+ # Created on Thu Jan 19 19:01:21 2023
20
+ # @author: Martín Carlos Araya <martinaraya@gmail.com>
21
+ from .las import las2frame
22
+ from .dlis import dlis2frame
23
+ from .lis import lis2frame
24
+ from .pack import Pack, concat
25
+ from .log import Log
26
+ from .writer import write_las
27
+ import glob
28
+ import os.path
29
+ import logging
30
+ from .rft import *
31
+
32
+ for name in ("lasio", "lasio.reader", "lasio.las", "unyts"):
33
+ logging.getLogger(name).setLevel(logging.WARNING)
34
+
35
+ __version__ = '0.2.3'
36
+ __release__ = 20260503
37
+ __all__ = ['read', 'concat', 'write_las']
38
+
39
+
40
+ class _Log2FrameParams(object):
41
+ """Package-level singleton holding runtime configuration flags.
42
+
43
+ Attributes
44
+ ----------
45
+ simpandas_ : bool
46
+ ``True`` when the optional ``simpandas`` package is importable.
47
+ Used by all readers to decide whether to return a
48
+ :class:`simpandas.SimDataFrame` or a plain
49
+ :class:`pandas.DataFrame`.
50
+ raise_error_ : bool
51
+ Default value for the *raise_error* parameter of :func:`read`.
52
+ Set to ``True`` to propagate parse errors; ``False`` silently
53
+ skips unreadable files.
54
+ """
55
+ def __init__(self):
56
+ try:
57
+ import simpandas as spd
58
+ self.simpandas_ = True
59
+ except ModuleNotFoundError:
60
+ self.simpandas_ = False
61
+ self.raise_error_ = True
62
+
63
+
64
+ _params_ = _Log2FrameParams()
65
+
66
+
67
+ def _read_one(path: str, raise_error=True, use_simpandas=None, correct_units=True):
68
+ """Dispatch a single file path to the appropriate format reader.
69
+
70
+ Parameters
71
+ ----------
72
+ path : str
73
+ Path to a ``.las``, ``.dlis``, or ``.lis`` / ``.lti`` file.
74
+ raise_error : bool, default True
75
+ Whether to propagate parse errors or return ``None``.
76
+ use_simpandas : bool | None, default None
77
+ Passed through to the format-specific reader.
78
+ correct_units : bool, default True
79
+ Passed through to the format-specific reader.
80
+
81
+ Returns
82
+ -------
83
+ Log | None
84
+ """
85
+ if use_simpandas is not None and use_simpandas is True and not _params_.simpandas_:
86
+ raise ModuleNotFoundError("SimPandas is not installed, please install it or set parameter `use_simpandas` to False.")
87
+ use_simpandas = _params_.simpandas_ if use_simpandas is None else bool(use_simpandas)
88
+ if not os.path.isfile(path):
89
+ if raise_error:
90
+ raise FileNotFoundError("The provided path can't be found:\n" + str(path))
91
+ else:
92
+ logging.warning("The provided path can't be found:\n" + str(path))
93
+ if path.split('.')[-1].lower() == 'las':
94
+ return las2frame(path, use_simpandas=use_simpandas, raise_error=raise_error, correct_units=correct_units)
95
+ elif path.split('.')[-1].lower() == 'dlis':
96
+ return dlis2frame(path, use_simpandas=use_simpandas, raise_error=raise_error, correct_units=correct_units)
97
+ elif path.split('.')[-1].lower() in ['lis', 'lti']:
98
+ return lis2frame(path, use_simpandas=use_simpandas, raise_error=raise_error, correct_units=correct_units)
99
+ elif not raise_error:
100
+ # logging.warning("Not a valid log file: " + str(path))
101
+ return None
102
+ else:
103
+ raise ValueError("`path` should be a '.las' or '.dlis' file")
104
+
105
+
106
+ def read(path: str, recursive=True, raise_error=False, squeeze=True, use_simpandas=None, correct_units=True):
107
+ """Read a well-log file (LAS, DLIS, LIS) into a units-aware :class:`Log` container.
108
+
109
+ Accepts individual file paths or a list of paths/globs. If multiple files are
110
+ read, they are returned grouped in a :class:`Pack` container.
111
+
112
+ Parameters
113
+ ----------
114
+ path : str | list[str]
115
+ Path to the ``.las``, ``.dlis``, or ``.lis`` file(s). Glob
116
+ expressions (``**/*.*``) and bare directory paths are supported.
117
+ recursive : bool, default True
118
+ Whether ``**`` glob patterns recurse into subdirectories.
119
+ squeeze : bool, default True
120
+ Return a single :class:`Log` directly when the result would be a
121
+ one-entry :class:`Pack`.
122
+ use_simpandas : bool | None, default None
123
+ Use :class:`simpandas.SimDataFrame` for curve storage when
124
+ ``True``; plain :class:`pandas.DataFrame` when ``False``;
125
+ auto-detect when ``None`` (default).
126
+ raise_error : bool, default False
127
+ Raise exceptions for unreadable files. When ``False`` (default)
128
+ unreadable files are silently skipped.
129
+ correct_units : bool, default True
130
+ Normalise unit strings using the built-in correction dictionary
131
+ (e.g. ``GAPI`` → ``gAPI``).
132
+
133
+ Returns
134
+ -------
135
+ Log | Pack
136
+ A :class:`Log` if a single file was resolved; a :class:`Pack`
137
+ otherwise (unless *squeeze* collapses a one-entry Pack).
138
+
139
+ Examples
140
+ --------
141
+ >>> import log2frame
142
+ >>> log = log2frame.read('well.las')
143
+ >>> pack = log2frame.read('data/**/*.las')
144
+ >>> log = log2frame.read(['run1.las', 'run2.las'], squeeze=False)
145
+ """
146
+ if use_simpandas is not None and use_simpandas is True and not _params_.simpandas_:
147
+ raise ModuleNotFoundError("SimPandas is not installed, please install it or set parameter `use_simpandas` to False.")
148
+ use_simpandas = _params_.simpandas_ if use_simpandas is None else bool(use_simpandas)
149
+ raise_error = _params_.raise_error_ if raise_error is None else bool(raise_error)
150
+ if os.path.isfile(path):
151
+ return _read_one(path, raise_error=raise_error, use_simpandas=use_simpandas, correct_units=correct_units)
152
+ if type(path) is str:
153
+ path = [path]
154
+ if type(path) is not str and hasattr(path, '__iter__'):
155
+ result = Pack()
156
+ for each in path:
157
+ for file in glob.iglob(each, recursive=recursive):
158
+ result.append(_read_one(file, raise_error=raise_error, use_simpandas=use_simpandas, correct_units=correct_units))
159
+ if squeeze:
160
+ return result.squeeze()
161
+ else:
162
+ return result
@@ -0,0 +1,69 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Unit-name correction mapping for LAS/DLIS/LIS mnemonics.
3
+
4
+ Loads ``units_dictionary.json`` (stored alongside this file) and
5
+ exposes :func:`correct_units` to normalise non-standard unit strings to
6
+ their canonical forms (e.g. ``'GAPI'`` → ``'gAPI'``,
7
+ ``'OHMM'`` → ``'ohm.m'``).
8
+
9
+ Module-level attributes
10
+ -----------------------
11
+ units_correction_dict_ : dict
12
+ The raw ``{non_canonical: canonical}`` mapping loaded from the JSON
13
+ dictionary. An empty dict is used as a safe fallback when the file
14
+ is missing or malformed (a :class:`UserWarning` is emitted).
15
+ """
16
+
17
+ import json
18
+ from os.path import dirname
19
+ from pathlib import Path
20
+
21
+ this_path_ = Path(__file__).with_name('units_dictionary.json').absolute()
22
+ this_path_ = dirname(this_path_) + '/'
23
+
24
+ try:
25
+ with open(this_path_ + 'units_dictionary.json', 'r') as f:
26
+ units_correction_dict_ = json.load(f)
27
+ except (FileNotFoundError, json.JSONDecodeError) as e:
28
+ import warnings
29
+ warnings.warn(
30
+ f"log2frame: units-correction dictionary not found "
31
+ f"({type(e).__name__}: {e}); proceeding with unit-correction disabled.",
32
+ UserWarning,
33
+ )
34
+ units_correction_dict_ = {}
35
+
36
+
37
+ def correct_units(units):
38
+ """Normalise unit strings using the built-in correction dictionary.
39
+
40
+ Parameters
41
+ ----------
42
+ units : str | dict
43
+ A single unit string, or a ``{mnemonic: unit}`` dict whose
44
+ values are corrected. Any other type is returned unchanged.
45
+
46
+ Returns
47
+ -------
48
+ str | dict
49
+ The corrected unit string (when *units* is a ``str``) or a new
50
+ dict with corrected values (when *units* is a ``dict``). Returns
51
+ the input unchanged when *units* is neither.
52
+
53
+ Examples
54
+ --------
55
+ >>> from log2frame.dictionaries.units import correct_units
56
+ >>> correct_units('GAPI')
57
+ 'gAPI'
58
+ >>> correct_units({'GR': 'GAPI', 'RHOB': 'G/C3'})
59
+ {'GR': 'gAPI', 'RHOB': 'g/c3'}
60
+ """
61
+ if type(units) is dict:
62
+ return {k: (units_correction_dict_[u]
63
+ if u in units_correction_dict_
64
+ else u)
65
+ for k, u in units.items()}
66
+ elif type(units) is str and units in units_correction_dict_:
67
+ return units_correction_dict_[units]
68
+ else:
69
+ return units
@@ -1,8 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
- """
3
- Created on Thu Jan 19 20:07:16 2023
2
+ """DLIS reader — parse DLIS files into :class:`Log` or :class:`Pack` containers.
3
+
4
+ This module wraps ``dlisio`` to provide curve data and metadata as
5
+ unit-aware :class:`Log` objects. When a DLIS file contains multiple
6
+ logical files or frames, a :class:`Pack` grouping all frames is returned.
4
7
 
5
- @author: Martín Carlos Araya <martinaraya@gmail.com>
8
+ Public API
9
+ ----------
10
+ dlis2frame(path, ...) Read a ``.dlis`` file.
6
11
  """
7
12
 
8
13
  import logging
@@ -19,23 +24,47 @@ try:
19
24
  except ModuleNotFoundError:
20
25
  pass
21
26
 
22
- __version__ = '0.2.1'
23
- __release__ = 20260426
27
+ __version__ = '0.2.3'
28
+ __release__ = 20260503
24
29
 
25
30
  logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
26
31
 
27
32
 
28
33
  class DLISIOError(Exception):
29
- """
30
- Error raised while reading LAS file.
31
- """
34
+ """Raised when ``dlisio`` fails to parse a DLIS file."""
32
35
  def __init__(self, message='raised by dlisio.'):
33
36
  self.message = 'ERROR: reading DLIS file ' + message
34
37
 
35
38
 
36
39
  def dlis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=True):
40
+ """Read a DLIS file into a :class:`Log` container.
41
+
42
+ Parameters
43
+ ----------
44
+ path : str
45
+ Path to the ``.dlis`` file.
46
+ use_simpandas : bool | None, default None
47
+ If True, the returned :class:`Log` wraps a
48
+ :class:`simpandas.SimDataFrame` and units propagate through
49
+ operations. If False, plain :class:`pandas.DataFrame` is used.
50
+ ``None`` falls back to True when SimPandas is installed.
51
+ raise_error : bool, default True
52
+ If False, parse errors are caught and ``None`` is returned
53
+ instead of raising.
54
+ correct_units : bool, default True
55
+ Apply the LAS-mnemonic-to-canonical-unit corrections (GR's
56
+ ``GAPI`` ↔ ``gAPI`` etc.) defined in
57
+ :data:`dictionaries.units_correction_dict_`.
58
+
59
+ Returns
60
+ -------
61
+ Log | Pack | None
62
+ The parsed log; or a ``Pack`` of logs if multiple frames are present;
63
+ ``None`` if parsing failed and ``raise_error=False``.
64
+ """
37
65
  if not os.path.isfile(path):
38
66
  raise FileNotFoundError("The provided path can't be found:\n" + str(path))
67
+ original_path = path
39
68
  if type(path) is str:
40
69
  path = path.replace('\\', '/')
41
70
 
@@ -129,10 +158,10 @@ def dlis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tr
129
158
  data=data[0] if not use_simpandas else spd.SimDataFrame(data=data[0], units=data[2].to_dict(),
130
159
  name=data[3],
131
160
  meta=data[1],
132
- source=str(path)),
161
+ source=str(original_path)),
133
162
  header=data[1],
134
163
  units=data[2],
135
- source=str(path),
164
+ source=str(original_path),
136
165
  well=data[3]) for name, data in frames.items()}
137
166
  return frames[list(frames.keys())[0]]
138
167
  else:
@@ -142,11 +171,11 @@ def dlis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tr
142
171
  meta=data[1],
143
172
  source='logical file ' + str(name[0]) +
144
173
  ', frame ' + str(name[1]) +
145
- ' in: ' + path),
174
+ ' in: ' + str(original_path)),
146
175
  header=data[1],
147
176
  units=data[2],
148
177
  source='logical file ' + str(name[0]) +
149
178
  ', frame ' + str(name[1]) +
150
- ' in: ' + path,
179
+ ' in: ' + str(original_path),
151
180
  well=data[3]) for name, data in frames.items()}
152
181
  return Pack(frames)
@@ -1,8 +1,12 @@
1
1
  # -*- coding: utf-8 -*-
2
- """
3
- Created on Thu Jan 19 19:02:12 2023
2
+ """LAS reader — parse LAS 1.2, 2.0, and 3.0 files into :class:`Log` containers.
3
+
4
+ This module wraps ``lasio`` to provide curve data and metadata as
5
+ unit-aware :class:`Log` objects.
4
6
 
5
- @author: Martín Carlos Araya <martinaraya@gmail.com>
7
+ Public API
8
+ ----------
9
+ las2frame(path, ...) Read a ``.las`` file.
6
10
  """
7
11
  import logging
8
12
  import lasio
@@ -18,23 +22,55 @@ try:
18
22
  except ModuleNotFoundError:
19
23
  pass
20
24
 
21
- __version__ = '0.2.1'
22
- __release__ = 20260426
25
+ __version__ = '0.2.3'
26
+ __release__ = 20260503
23
27
 
24
28
  logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
25
29
 
26
30
 
27
31
  class LASIOError(Exception):
28
- """
29
- Error raised while reading LAS file.
30
- """
32
+ """Raised when ``lasio`` fails to parse a LAS file."""
31
33
  def __init__(self, message='raised by lasio.'):
32
34
  self.message = 'ERROR: reading LAS file ' + message
33
35
 
34
36
 
35
37
  def las2frame(path: str, use_simpandas=None, raise_error=True, correct_units=True):
38
+ """Read a LAS file (1.2 / 2.0 / 3.0) into a :class:`Log` container.
39
+
40
+ Parameters
41
+ ----------
42
+ path : str
43
+ Path to the ``.las`` file.
44
+ use_simpandas : bool | None, default None
45
+ If True, the returned :class:`Log` wraps a
46
+ :class:`simpandas.SimDataFrame` and units propagate through
47
+ operations. If False, plain :class:`pandas.DataFrame` is used.
48
+ ``None`` falls back to True when SimPandas is installed.
49
+ raise_error : bool, default True
50
+ If False, parse errors are caught and ``None`` is returned
51
+ instead of raising.
52
+ correct_units : bool, default True
53
+ Apply the LAS-mnemonic-to-canonical-unit corrections (GR's
54
+ ``GAPI`` ↔ ``gAPI`` etc.) defined in
55
+ :data:`dictionaries.units_correction_dict_`.
56
+
57
+ Returns
58
+ -------
59
+ Log | None
60
+ The parsed log; ``None`` if parsing failed and
61
+ ``raise_error=False``.
62
+
63
+ Examples
64
+ --------
65
+ >>> log = log2frame.las2frame("well.las")
66
+ >>> log.curves
67
+ ['GR', 'NPHI', 'RHOB', 'DT']
68
+ >>> log.units_dict()['GR']
69
+ 'gAPI'
70
+ """
36
71
  if not os.path.isfile(path):
37
72
  raise FileNotFoundError("The provided path can't be found:\n" + str(path))
73
+ original_path = path
38
74
  if type(path) is str:
39
75
  path = path.replace('\\', '/')
40
76
 
@@ -92,10 +128,10 @@ def las2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tru
92
128
  units=las_units,
93
129
  name=well_name,
94
130
  meta=las_header,
95
- source=path)
131
+ source=original_path)
96
132
 
97
133
  return Log(data=data,
98
134
  header=las_header,
99
135
  units=pd.Series(las_units, name='curves_units'),
100
- source=path,
136
+ source=original_path,
101
137
  well=well_name)
@@ -1,8 +1,15 @@
1
1
  # -*- coding: utf-8 -*-
2
- """
3
- Created on Thu Jan 19 20:07:16 2023
2
+ """LIS reader — parse LIS files into :class:`Log` or :class:`Pack` containers.
3
+
4
+ This module wraps ``dlisio.lis`` to provide curve data and metadata as
5
+ unit-aware :class:`Log` objects. When a LIS file contains multiple
6
+ logical files or format specs with different sample rates, each
7
+ combination is returned as a separate :class:`Log` grouped in a
8
+ :class:`Pack`.
4
9
 
5
- @author: Martín Carlos Araya <martinaraya@gmail.com>
10
+ Public API
11
+ ----------
12
+ lis2frame(path, ...) Read a ``.lis`` or ``.lti`` file.
6
13
  """
7
14
 
8
15
  import logging
@@ -18,22 +25,46 @@ try:
18
25
  except ModuleNotFoundError:
19
26
  pass
20
27
 
21
- __version__ = '0.2.1'
22
- __release__ = 20260426
28
+ __version__ = '0.2.3'
29
+ __release__ = 20260503
23
30
 
24
31
  logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
25
32
 
26
33
 
27
34
  class DLISIOError(Exception):
28
- """
29
- Error raised while reading LAS file.
30
- """
35
+ """Raised when ``dlisio`` fails to parse a LIS file."""
31
36
  def __init__(self, message='raised by dlisio.'):
32
37
  self.message = 'ERROR: reading LIS file ' + message
33
38
 
34
39
  def lis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=True):
40
+ """Read a LIS file into a :class:`Log` container.
41
+
42
+ Parameters
43
+ ----------
44
+ path : str
45
+ Path to the ``.lis`` or ``.lti`` file.
46
+ use_simpandas : bool | None, default None
47
+ If True, the returned :class:`Log` wraps a
48
+ :class:`simpandas.SimDataFrame` and units propagate through
49
+ operations. If False, plain :class:`pandas.DataFrame` is used.
50
+ ``None`` falls back to True when SimPandas is installed.
51
+ raise_error : bool, default True
52
+ If False, parse errors are caught and ``None`` is returned
53
+ instead of raising.
54
+ correct_units : bool, default True
55
+ Apply the LAS-mnemonic-to-canonical-unit corrections (GR's
56
+ ``GAPI`` ↔ ``gAPI`` etc.) defined in
57
+ :data:`dictionaries.units_correction_dict_`.
58
+
59
+ Returns
60
+ -------
61
+ Log | Pack | None
62
+ The parsed log; or a ``Pack`` of logs if multiple frames are present;
63
+ ``None`` if parsing failed and ``raise_error=False``.
64
+ """
35
65
  if not os.path.isfile(path):
36
66
  raise FileNotFoundError("The provided path can't be found:\n" + str(path))
67
+ original_path = path
37
68
  if type(path) is str:
38
69
  path = path.replace('\\', '/')
39
70
 
@@ -44,6 +75,28 @@ def lis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tru
44
75
  raise ModuleNotFoundError("SimPandas is not installed, please install it or set parameter `use_simpandas` to False.")
45
76
 
46
77
  def _get_frame(data, l_count, i, sr):
78
+ """Return the curve data for one format spec and sample rate.
79
+
80
+ When *use_simpandas* is ``True``, wraps the raw structured array
81
+ in a :class:`simpandas.SimDataFrame` with units and index
82
+ settings applied. Otherwise returns a plain
83
+ :class:`pandas.DataFrame` with the index set to the depth column.
84
+
85
+ Parameters
86
+ ----------
87
+ data : pandas.DataFrame
88
+ Raw DataFrame produced from :func:`dlisio.lis.curves`.
89
+ l_count : int
90
+ Logical-file counter key into ``frames``.
91
+ i : int
92
+ Format-spec index within the logical file.
93
+ sr : int | float
94
+ Sample-rate key within the format spec.
95
+
96
+ Returns
97
+ -------
98
+ pandas.DataFrame | simpandas.SimDataFrame
99
+ """
47
100
  if frames[l_count][i]['index_name'] in data:
48
101
  index_name = frames[l_count][i]['index_name']
49
102
  elif len([c for c in data.columns if c.startswith(frames[l_count][i]['index_name'])]) > 0:
@@ -65,12 +118,31 @@ def lis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tru
65
118
  if frames[l_count]['header']['name'] is not None
66
119
  and len(frames[l_count]['header']['name']) > 0 else None),
67
120
  meta=_make_header(l_count=l_count, i=i, sr=sr),
68
- source='logical file ' + str(l_count) + ', frame ' + str(i) + ', sample rate ' + str(sr) + ' in: ' + str(path))
121
+ source='logical file ' + str(l_count) + ', frame ' + str(i) + ', sample rate ' + str(sr) + ' in: ' + str(original_path))
69
122
  else:
70
123
  return data.set_index(index_name)
71
124
 
72
125
 
73
126
  def _make_header(l_count, i, sr):
127
+ """Build a merged header DataFrame for one logical file / frame / sample rate.
128
+
129
+ Concatenates curve units, wellsite data, and LIS file/reel header
130
+ into a single :class:`pandas.DataFrame` indexed by mnemonic.
131
+
132
+ Parameters
133
+ ----------
134
+ l_count : int
135
+ Logical-file index.
136
+ i : int
137
+ Format-spec index within the logical file.
138
+ sr : int | float
139
+ Sample-rate key.
140
+
141
+ Returns
142
+ -------
143
+ pandas.DataFrame
144
+ Combined header table; empty cells are replaced with ``''``.
145
+ """
74
146
  to_concat = []
75
147
  if 'curves_units' in frames[l_count][i] and sr in frames[l_count][i]['curves_units']:
76
148
  to_concat.append(pd.DataFrame(frames[l_count][i]['curves_units'][sr],
@@ -85,6 +157,18 @@ def lis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tru
85
157
  return pd.concat(to_concat, axis=0).fillna(value='')
86
158
 
87
159
  def _try_frame(data):
160
+ """Attempt to wrap *data* in a :class:`pandas.DataFrame`.
161
+
162
+ Parameters
163
+ ----------
164
+ data : structured numpy array or similar
165
+ Raw curve data returned by :func:`dlisio.lis.curves`.
166
+
167
+ Returns
168
+ -------
169
+ pandas.DataFrame | None
170
+ ``None`` when the conversion raises any exception.
171
+ """
88
172
  try:
89
173
  return pd.DataFrame(data)
90
174
  except:
@@ -161,7 +245,7 @@ def lis2frame(path: str, use_simpandas=None, raise_error=True, correct_units=Tru
161
245
  source='logical file ' + str(l_count) +
162
246
  ', frame ' + str(i) +
163
247
  ', sample rate ' + str(sr) +
164
- ' in: ' + str(path),
248
+ ' in: ' + str(original_path),
165
249
  well=(
166
250
  frames[l_count]['header']['service_name'] if frames[l_count]['header']['service_name'] is not None and len(
167
251
  frames[l_count]['header']['service_name']) > 0 else