This cookbook example contains a module that implements a reader for a LAS (Log ASCII Standard) well log file (LAS 2.0). See the Canadian Well Logging Society page about this format for more information.
1 """LAS File Reader
2
3 The main class defined here is LASReader, a class that reads a LAS file
4 and makes the data available as a Python object.
5 """
6
7 # Copyright (c) 2011, Warren Weckesser
8 #
9 # Permission to use, copy, modify, and/or distribute this software for any
10 # purpose with or without fee is hereby granted, provided that the above
11 # copyright notice and this permission notice appear in all copies.
12 #
13 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
14 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
16 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
17 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
18 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
19 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20
21
22 import re
23 import keyword
24
25 import numpy as np
26
27
28 def isidentifier(s):
29 if s in keyword.kwlist:
30 return False
31 return re.match(r'^[a-z_][a-z0-9_]*$', s, re.I) is not None
32
33
34 def _convert_to_value(s):
35 try:
36 value = int(s)
37 except ValueError:
38 try:
39 value = float(s)
40 except ValueError:
41 value = s
42 return value
43
44
45 class LASError(Exception):
46 pass
47
48
49 class LASItem(object):
50 """This class is just a namespace, holding the attributes 'name',
51 'units', 'data', 'value', and 'descr'. 'value' is the numerical
52 value of 'data', if it has a numerical value (specifically, if
53 int() or float() don't raise an exception when given the value
54 of the 'data' attribute).
55
56 A class method, from_line(cls, line), is provided to parse
57 a line from a LAS file and create a LASItem instance.
58 """
59 def __init__(self, name, units='', data='', descr=''):
60 self.name = name
61 self.units = units
62 self.data = data
63 self.value = _convert_to_value(data)
64 self.descr = descr
65
66 def __str__(self):
67 s = ("name='%s', units='%s', data='%s', descr='%s'" %
68 (self.name, self.units, self.data, self.descr))
69 return s
70
71 def __repr__(self):
72 s = str(self)
73 return "LASItem(%s)" % s
74
75 @classmethod
76 def from_line(cls, line):
77 first, descr = line.rsplit(':', 1)
78 descr = descr.strip()
79 name, mid = first.split('.', 1)
80 name = name.strip()
81 if mid.startswith(' '):
82 # No units
83 units = ''
84 data = mid
85 else:
86 units_data = mid.split(None, 1)
87 if len(units_data) == 1:
88 units = units_data[0]
89 data = ''
90 else:
91 units, data = units_data
92 return LASItem(name=name, units=units, data=data.strip(),
93 descr=descr.strip())
94
95
96 def _read_wrapped_row(f, n):
97 """Read a "row" of data from the Ascii section of a "wrapped" LAS file.
98
99 `f` must be a file object opened for reading.
100 `n` is the number of fields in the row.
101
102 Returns the list of floats read from the file.
103 """
104 depth = float(f.readline().strip())
105 values = [depth]
106 while len(values) < n:
107 new_values = [float(s) for s in f.readline().split()]
108 values.extend(new_values)
109 return values
110
111
112 def _read_wrapped_data(f, dt):
113 data = []
114 ncols = len(dt.names)
115 while True:
116 try:
117 row = _read_wrapped_row(f, ncols)
118 except Exception:
119 break
120 data.append(tuple(row))
121 data = np.array(data, dtype=dt)
122 return data
123
124
125 class LASSection(object):
126 """Represents a "section" of a LAS file.
127
128 A section is basically a collection of items, where each item has the
129 attributes 'name', 'units', 'data' and 'descr'.
130
131 Any item in the section whose name is a valid Python identifier is
132 also attached to the object as an attribute. For example, if `s` is a
133 LASSection instance, and the corresponding section in the LAS file
134 contained this line:
135
136 FD .K/M3 999.9999 : Fluid Density
137
138 then the item may be referred to as `s.FD` (in addition to the longer
139 `s.items['FD']`).
140
141 Attributes
142 ----------
143 items : dict
144 The keys are the item names, and the values are LASItem instances.
145 names : list
146 List of item names, in the order they were read from the LAS file.
147
148 """
149 def __init__(self):
150 # Note: In Python 2.7, 'items' could be an OrderedDict, and
151 # then 'names' would not be necessary--one could use items.keys().
152 self.items = dict()
153 self.names = []
154
155 def add_item(self, item):
156 self.items[item.name] = item
157 self.names.append(item.name)
158 if isidentifier(item.name) and not hasattr(self, item.name):
159 setattr(self, item.name, item)
160
161 def display(self):
162 for name in self.names:
163 item = self.items[name]
164 namestr = name
165 if item.units != '':
166 namestr = namestr + (" (%s)" % item.units)
167 print "%-16s %-30s [%s]" % (namestr, "'" + item.data + "'",
168 item.descr)
169
170
171 class LASReader(object):
172 """The LASReader class holds data from a LAS file.
173
174 This reader only handles LAS 2.0 files (as far as I know).
175
176 Constructor
177 -----------
178 LASReader(f, null_subs=None)
179
180 f : file object or string
181 If f is a file object, it must be opened for reading.
182 If f is a string, it must be the filename of a LAS file.
183 In that case, the file will be opened and read.
184
185 Attributes for LAS Sections
186 ---------------------------
187 version : LASSection instance
188 This LASSection holds the items from the '~V' section.
189
190 well : LASSection instance
191 This LASSection holds the items from the '~W' section.
192
193 curves : LASection instance
194 This LASSection holds the items from the '~C' section.
195
196 parameters : LASSection instance
197 This LASSection holds the items from the '~P' section.
198
199 other : str
200 Holds the contents of the '~O' section as a single string.
201
202 data : numpy 1D structured array
203 The numerical data from the '~A' section. The data type
204 of the array is constructed from the items in the '~C'
205 section.
206
207 Other attributes
208 ----------------
209 data2d : numpy 2D array of floats
210 The numerical data from the '~A' section, as a 2D array.
211 This is a view of the same data as in the `data` attribute.
212
213 wrap : bool
214 True if the LAS file was wrapped. (More specifically, this
215 attribute is True if the data field of the item with the
216 name 'WRAP' in the '~V' section has the value 'YES'.)
217
218 vers : str
219 The LAS version. (More specifically, the value of the data
220 field of the item with the name 'VERS' in the '~V' section).
221
222 null : float or None
223 The numerical value of the 'NULL' item in the '~W' section.
224 The value will be None if the 'NULL' item was missing.
225
226 null_subs : float or None
227 The value given in the constructor, to be used as the
228 replacement value of each occurrence of `null_value` in
229 the log data. The value will be None (and no substitution
230 will be done) if the `null_subs` argument is not given to
231 the constructor.
232
233 start : float, or None
234 Numerical value of the 'STRT' item from the '~W' section.
235 The value will be None if 'STRT' was not given in the file.
236
237 start_units : str
238 Units of the 'STRT' item from the '~W' section.
239 The value will be None if 'STRT' was not given in the file.
240
241 stop : float
242 Numerical value of the 'STOP' item from the '~W' section.
243 The value will be None if 'STOP' was not given in the file.
244
245 stop_units : str
246 Units of the 'STOP' item from the '~W' section.
247 The value will be None if 'STOP' was not given in the file.
248
249 step : float
250 Numerical value of the 'STEP' item from the '~W' section.
251 The value will be None if 'STEP' was not given in the file.
252
253 step_units : str
254 Units of the 'STEP' item from the '~W' section.
255 The value will be None if 'STEP' was not given in the file.
256
257 """
258
259 def __init__(self, f, null_subs=None):
260 """f can be a filename (str) or a file object.
261
262 If 'null_subs' is not None, its value replaces any values in the data
263 that matches the NULL value specified in the Version section of the LAS
264 file.
265 """
266 self.null = None
267 self.null_subs = null_subs
268 self.start = None
269 self.start_units = None
270 self.stop = None
271 self.stop_units = None
272 self.step = None
273 self.step_units = None
274
275 self.version = LASSection()
276 self.well = LASSection()
277 self.curves = LASSection()
278 self.parameters = LASSection()
279 self.other = ''
280 self.data = None
281
282 self._read_las(f)
283
284 self.data2d = self.data.view(float).reshape(-1, len(self.curves.items))
285 if null_subs is not None:
286 self.data2d[self.data2d == self.null] = null_subs
287
288 def _read_las(self, f):
289 """Read a LAS file.
290
291 Returns a dictionary with keys 'V', 'W', 'C', 'P', 'O' and 'A',
292 corresponding to the sections of a LAS file. The values associated
293 with keys 'V', 'W', 'C' and 'P' will be lists of Item instances. The
294 value associated with the 'O' key is a list of strings. The value
295 associated with the 'A' key is a numpy structured array containing the
296 log data. The field names of the array are the mnemonics from the
297 Curve section of the file.
298 """
299 opened_here = False
300 if isinstance(f, basestring):
301 opened_here = True
302 f = open(f, 'r')
303
304 self.wrap = False
305
306 line = f.readline()
307 current_section = None
308 current_section_label = ''
309 while not line.startswith('~A'):
310 if not line.startswith('#'):
311 if line.startswith('~'):
312 if len(line) < 2:
313 raise LASError("Missing section character after '~'.")
314 current_section_label = line[1:2]
315 other = False
316 if current_section_label == 'V':
317 current_section = self.version
318 elif current_section_label == 'W':
319 current_section = self.well
320 elif current_section_label == 'C':
321 current_section = self.curves
322 elif current_section_label == 'P':
323 current_section = self.parameters
324 elif current_section_label == 'O':
325 current_section = self.other
326 other = True
327 else:
328 raise LASError("Unknown section '%s'" % line)
329 elif current_section is None:
330 raise LASError("Missing first section.")
331 else:
332 if other:
333 # The 'Other' section is just lines of text, so we
334 # assemble them into a single string.
335 self.other += line
336 current_section = self.other
337 else:
338 # Parse the line into a LASItem and add it to the
339 # current section.
340 m = LASItem.from_line(line)
341 current_section.add_item(m)
342 # Check for the required items whose values we'll
343 # store as attributes of the LASReader instance.
344 if current_section == self.version:
345 if m.name == 'WRAP':
346 if m.data.strip() == 'YES':
347 self.wrap = True
348 if m.name == 'VERS':
349 self.vers = m.data.strip()
350 if current_section == self.well:
351 if m.name == 'NULL':
352 self.null = float(m.data)
353 elif m.name == 'STRT':
354 self.start = float(m.data)
355 self.start_units = m.units
356 elif m.name == 'STOP':
357 self.stop = float(m.data)
358 self.stop_units = m.units
359 elif m.name == 'STEP':
360 self.step = float(m.data)
361 self.step_units = m.units
362 line = f.readline()
363
364 # Finished reading the header--all that is left is the numerical
365 # data that follows the '~A' line. We'll construct a structured
366 # data type, and, if the data is not wrapped, use numpy.loadtext
367 # to read the data into an array. For wrapped rows, we use the
368 # function _read_wrapped() defined elsewhere in this module.
369 # The data type is determined by the items from the '~Curves' section.
370 dt = np.dtype([(name, float) for name in self.curves.names])
371 if self.wrap:
372 a = _read_wrapped_data(f, dt)
373 else:
374 a = np.loadtxt(f, dtype=dt)
375 self.data = a
376
377 if opened_here:
378 f.close()
379
380
381 if __name__ == "__main__":
382 import sys
383
384 las = LASReader(sys.argv[1], null_subs=np.nan)
385 print "wrap? ", las.wrap
386 print "vers? ", las.vers
387 print "null =", las.null
388 print "start =", las.start
389 print "stop =", las.stop
390 print "step =", las.step
391 print "Version ---"
392 las.version.display()
393 print "Well ---"
394 las.well.display()
395 print "Curves ---"
396 las.curves.display()
397 print "Parameters ---"
398 las.parameters.display()
399 print "Other ---"
400 print las.other
401 print "Data ---"
402 print las.data2d
Source code: las.py
Here's an example of the use of this module:
>>> import numpy as np >>> from las import LASReader >>> sample3 = LASReader('sample3.las', null_subs=np.nan) >>> print sample3.null -999.25 >>> print sample3.start, sample3.stop, sample3.step 910.0 909.5 -0.125 >>> print sample3.well.PROV.data, sample3.well.UWI.data ALBERTA 100123401234W500 >>> from matplotlib.pyplot import plot, show >>> plot(sample3.data['DEPT'], sample3.data['PHIE']) [<matplotlib.lines.Line2D object at 0x4c2ae90>] >>> show()
It creates the following plot:
The sample LAS file is here: