1
|
|
|
import logging |
2
|
|
|
import textwrap |
3
|
|
|
|
4
|
|
|
import numpy as np |
5
|
|
|
|
6
|
|
|
from .las_items import ( |
7
|
|
|
HeaderItem, CurveItem, SectionItems, OrderedDict) |
8
|
|
|
from . import defaults |
9
|
|
|
from . import exceptions |
10
|
|
|
|
11
|
|
|
logger = logging.getLogger(__name__) |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
def write(las, file_object, version=None, wrap=None, STRT=None, |
15
|
|
|
STOP=None, STEP=None, fmt='%.5f'): |
16
|
|
|
'''Write a LAS files. |
17
|
|
|
|
18
|
|
|
Arguments: |
19
|
|
|
las (:class:`lasio.las.LASFile`) |
20
|
|
|
file_object (file-like object open for writing): output |
21
|
|
|
|
22
|
|
|
Keyword Arguments: |
23
|
|
|
version (float or None): version of written file, either 1.2 or 2. |
24
|
|
|
If this is None, ``las.version.VERS.value`` will be used. |
25
|
|
|
wrap (bool or None): whether to wrap the output data section. |
26
|
|
|
If this is None, ``las.version.WRAP.value`` will be used. |
27
|
|
|
STRT (float or None): value to use as STRT (note the data will not |
28
|
|
|
be clipped). If this is None, the data value in the first column, |
29
|
|
|
first row will be used. |
30
|
|
|
STOP (float or None): value to use as STOP (note the data will not |
31
|
|
|
be clipped). If this is None, the data value in the first column, |
32
|
|
|
last row will be used. |
33
|
|
|
STEP (float or None): value to use as STEP (note the data will not |
34
|
|
|
be resampled and/or interpolated). If this is None, the STEP will |
35
|
|
|
be estimated from the first two rows of the first column. |
36
|
|
|
fmt (str): Python string formatting operator for numeric data to be |
37
|
|
|
used. |
38
|
|
|
|
39
|
|
|
You should avoid calling this function directly - instead use the |
40
|
|
|
:meth:`lasio.las.LASFile.write` method. |
41
|
|
|
|
42
|
|
|
''' |
43
|
|
|
if wrap is None: |
44
|
|
|
wrap = las.version['WRAP'] == 'YES' |
45
|
|
|
elif wrap is True: |
46
|
|
|
las.version['WRAP'] = HeaderItem( |
47
|
|
|
'WRAP', '', 'YES', 'Multiple lines per depth step') |
48
|
|
|
elif wrap is False: |
49
|
|
|
las.version['WRAP'] = HeaderItem( |
50
|
|
|
'WRAP', '', 'NO', 'One line per depth step') |
51
|
|
|
lines = [] |
52
|
|
|
|
53
|
|
|
assert version in (1.2, 2, None) |
54
|
|
|
if version is None: |
55
|
|
|
version = las.version['VERS'].value |
56
|
|
|
if version == 1.2: |
57
|
|
|
las.version['VERS'] = HeaderItem( |
58
|
|
|
'VERS', '', 1.2, 'CWLS LOG ASCII STANDARD - VERSION 1.2') |
59
|
|
|
elif version == 2: |
60
|
|
|
las.version['VERS'] = HeaderItem( |
61
|
|
|
'VERS', '', 2.0, 'CWLS log ASCII Standard -VERSION 2.0') |
62
|
|
|
|
63
|
|
|
if STRT is None: |
64
|
|
|
STRT = las.index[0] |
65
|
|
|
if STOP is None: |
66
|
|
|
STOP = las.index[-1] |
67
|
|
|
if STEP is None: |
68
|
|
|
STEP = las.index[1] - las.index[0] # Faster than np.gradient |
69
|
|
|
las.well['STRT'].value = STRT |
70
|
|
|
las.well['STOP'].value = STOP |
71
|
|
|
las.well['STEP'].value = STEP |
72
|
|
|
|
73
|
|
|
# Check units |
74
|
|
|
if las.curves[0].unit: |
75
|
|
|
unit = las.curves[0].unit |
76
|
|
|
else: |
77
|
|
|
unit = las.well['STRT'].unit |
78
|
|
|
las.well['STRT'].unit = unit |
79
|
|
|
las.well['STOP'].unit = unit |
80
|
|
|
las.well['STEP'].unit = unit |
81
|
|
|
las.curves[0].unit = unit |
82
|
|
|
|
83
|
|
|
# Check for any changes in the pandas dataframe and if there are, |
84
|
|
|
# create new curves so they are reflected in the output LAS file. |
85
|
|
|
|
86
|
|
|
# if las.use_pandas: |
87
|
|
|
# curve_names = lambda: [ci.mnemonic for ci in las.curves] |
88
|
|
|
# for df_curve_name in list(las.df.columns.values): |
89
|
|
|
# if not df_curve_name in curve_names(): |
90
|
|
|
# las.add_curve(df_curve_name, las.df[df_curve_name]) |
91
|
|
|
|
92
|
|
|
# Write each section. |
93
|
|
|
|
94
|
|
|
# ~Version |
95
|
|
|
logger.debug('LASFile.write Version section') |
96
|
|
|
lines.append('~Version '.ljust(60, '-')) |
97
|
|
|
order_func = get_section_order_function('Version', version) |
98
|
|
|
section_widths = get_section_widths( |
99
|
|
|
'Version', las.version, version, order_func) |
100
|
|
|
for header_item in las.version.values(): |
101
|
|
|
mnemonic = header_item.original_mnemonic |
102
|
|
|
# logger.debug('LASFile.write ' + str(header_item)) |
103
|
|
|
order = order_func(mnemonic) |
104
|
|
|
# logger.debug('LASFile.write order = %s' % (order, )) |
105
|
|
|
logger.debug('LASFile.write %s order=%s section_widths=%s' % ( |
106
|
|
|
header_item, order, section_widths)) |
107
|
|
|
formatter_func = get_formatter_function(order, **section_widths) |
108
|
|
|
line = formatter_func(header_item) |
109
|
|
|
lines.append(line) |
110
|
|
|
|
111
|
|
|
# ~Well |
112
|
|
|
logger.debug('LASFile.write Well section') |
113
|
|
|
lines.append('~Well '.ljust(60, '-')) |
114
|
|
|
order_func = get_section_order_function('Well', version) |
115
|
|
|
section_widths = get_section_widths( |
116
|
|
|
'Well', las.well, version, order_func) |
117
|
|
|
# logger.debug('LASFile.write well section_widths=%s' % section_widths) |
118
|
|
|
for header_item in las.well.values(): |
119
|
|
|
mnemonic = header_item.original_mnemonic |
120
|
|
|
order = order_func(mnemonic) |
121
|
|
|
logger.debug('LASFile.write %s order=%s section_widths=%s' % ( |
122
|
|
|
header_item, order, section_widths)) |
123
|
|
|
formatter_func = get_formatter_function(order, **section_widths) |
124
|
|
|
line = formatter_func(header_item) |
125
|
|
|
lines.append(line) |
126
|
|
|
|
127
|
|
|
# ~Curves |
128
|
|
|
logger.debug('LASFile.write Curves section') |
129
|
|
|
lines.append('~Curve Information '.ljust(60, '-')) |
130
|
|
|
order_func = get_section_order_function('Curves', version) |
131
|
|
|
section_widths = get_section_widths( |
132
|
|
|
'Curves', las.curves, version, order_func) |
133
|
|
|
for header_item in las.curves: |
134
|
|
|
mnemonic = header_item.original_mnemonic |
135
|
|
|
order = order_func(mnemonic) |
136
|
|
|
formatter_func = get_formatter_function(order, **section_widths) |
137
|
|
|
line = formatter_func(header_item) |
138
|
|
|
lines.append(line) |
139
|
|
|
|
140
|
|
|
# ~Params |
141
|
|
|
lines.append('~Params '.ljust(60, '-')) |
142
|
|
|
order_func = get_section_order_function('Parameter', version) |
143
|
|
|
section_widths = get_section_widths( |
144
|
|
|
'Parameter', las.params, version, order_func) |
145
|
|
|
for header_item in las.params.values(): |
146
|
|
|
mnemonic = header_item.original_mnemonic |
147
|
|
|
order = order_func(mnemonic) |
148
|
|
|
formatter_func = get_formatter_function(order, **section_widths) |
149
|
|
|
line = formatter_func(header_item) |
150
|
|
|
lines.append(line) |
151
|
|
|
|
152
|
|
|
# ~Other |
153
|
|
|
lines.append('~Other '.ljust(60, '-')) |
154
|
|
|
lines += las.other.splitlines() |
155
|
|
|
|
156
|
|
|
lines.append('~ASCII '.ljust(60, '-')) |
157
|
|
|
|
158
|
|
|
file_object.write('\n'.join(lines)) |
159
|
|
|
file_object.write('\n') |
160
|
|
|
line_counter = len(lines) |
161
|
|
|
|
162
|
|
|
# data_arr = np.column_stack([c.data for c in las.curves]) |
163
|
|
|
data_arr = las.data |
164
|
|
|
nrows, ncols = data_arr.shape |
165
|
|
|
|
166
|
|
|
def format_data_section_line(n, fmt, l=10, spacer=' '): |
167
|
|
|
try: |
168
|
|
|
if np.isnan(n): |
169
|
|
|
return spacer + str(las.well['NULL'].value).rjust(l) |
170
|
|
|
else: |
171
|
|
|
return spacer + (fmt % n).rjust(l) |
172
|
|
|
except TypeError: |
173
|
|
|
return spacer + str(n).rjust(l) |
174
|
|
|
|
175
|
|
|
twrapper = textwrap.TextWrapper(width=79) |
176
|
|
|
|
177
|
|
|
for i in range(nrows): |
178
|
|
|
depth_slice = '' |
179
|
|
|
for j in range(ncols): |
180
|
|
|
depth_slice += format_data_section_line(data_arr[i, j], fmt) |
181
|
|
|
|
182
|
|
|
if wrap: |
183
|
|
|
lines = twrapper.wrap(depth_slice) |
184
|
|
|
logger.debug('LASFile.write Wrapped %d lines out of %s' % |
185
|
|
|
(len(lines), depth_slice)) |
186
|
|
|
else: |
187
|
|
|
lines = [depth_slice] |
188
|
|
|
|
189
|
|
|
for line in lines: |
190
|
|
|
if las.version['VERS'].value == 1.2 and len(line) > 255: |
191
|
|
|
logger.warning('[v1.2] line #{} has {} chars (>256)'.format( |
192
|
|
|
line_counter + 1, len(line))) |
193
|
|
|
file_object.write(line + '\n') |
194
|
|
|
line_counter += 1 |
195
|
|
|
|
196
|
|
|
|
197
|
|
|
def get_formatter_function(order, left_width=None, middle_width=None): |
198
|
|
|
'''Create function to format a LAS header item for output. |
199
|
|
|
|
200
|
|
|
Arguments: |
201
|
|
|
order: format of item, either 'descr:value' or 'value:descr' |
202
|
|
|
|
203
|
|
|
Keyword Arguments: |
204
|
|
|
left_width (int): number of characters to the left hand side of the |
205
|
|
|
first period |
206
|
|
|
middle_width (int): total number of characters minus 1 between the |
207
|
|
|
first period from the left and the first colon from the left. |
208
|
|
|
|
209
|
|
|
Returns: |
210
|
|
|
A function which takes a header item |
211
|
|
|
(e.g. :class:`lasio.las_items.HeaderItem`) as its single argument and |
212
|
|
|
which in turn returns a string which is the correctly formatted LAS |
213
|
|
|
header line. |
214
|
|
|
|
215
|
|
|
''' |
216
|
|
|
if left_width is None: |
217
|
|
|
left_width = 10 |
218
|
|
|
if middle_width is None: |
219
|
|
|
middle_width = 40 |
220
|
|
|
mnemonic_func = lambda mnemonic: mnemonic.ljust(left_width) |
221
|
|
|
middle_func = lambda unit, right_hand_item: ( |
222
|
|
|
unit |
223
|
|
|
+ ' ' * (middle_width - len(str(unit)) - len(right_hand_item)) |
224
|
|
|
+ right_hand_item |
225
|
|
|
) |
226
|
|
|
if order == 'descr:value': |
227
|
|
|
return lambda item: '%s.%s : %s' % ( |
228
|
|
|
mnemonic_func(item.original_mnemonic), |
229
|
|
|
middle_func(str(item.unit), str(item.descr)), |
230
|
|
|
item.value |
231
|
|
|
) |
232
|
|
|
elif order == 'value:descr': |
233
|
|
|
return lambda item: '%s.%s : %s' % ( |
234
|
|
|
mnemonic_func(item.original_mnemonic), |
235
|
|
|
middle_func(str(item.unit), str(item.value)), |
236
|
|
|
item.descr |
237
|
|
|
) |
238
|
|
|
|
239
|
|
|
|
240
|
|
|
def get_section_order_function(section, version, |
241
|
|
|
order_definitions=defaults.ORDER_DEFINITIONS): |
242
|
|
|
'''Get a function that returns the order per the mnemonic and section. |
243
|
|
|
|
244
|
|
|
Arguments: |
245
|
|
|
section (str): either 'well', 'params', 'curves', 'version' |
246
|
|
|
version (float): either 1.2 and 2.0 |
247
|
|
|
|
248
|
|
|
Keyword Arguments: |
249
|
|
|
order_definitions (dict): see source of defaults.py for more information |
250
|
|
|
|
251
|
|
|
Returns: |
252
|
|
|
A function which takes a mnemonic (str) as its only argument, and |
253
|
|
|
in turn returns the order 'value:descr' or 'descr:value'. |
254
|
|
|
|
255
|
|
|
''' |
256
|
|
|
section_orders = order_definitions[version][section] |
257
|
|
|
default_order = section_orders[0] |
258
|
|
|
orders = {} |
259
|
|
|
for order, mnemonics in section_orders[1:]: |
260
|
|
|
for mnemonic in mnemonics: |
261
|
|
|
orders[mnemonic] = order |
262
|
|
|
return lambda mnemonic: orders.get(mnemonic, default_order) |
263
|
|
|
|
264
|
|
|
|
265
|
|
|
def get_section_widths(section_name, items, version, order_func): |
266
|
|
|
'''Find minimum section widths fitting the content in *items*. |
267
|
|
|
|
268
|
|
|
Arguments: |
269
|
|
|
section_name (str): either 'version', 'well', 'curves', or 'params' |
270
|
|
|
items (SectionItems): section items |
271
|
|
|
version (float): either 1.2 or 2.0 |
272
|
|
|
order_func (func): see :func:`lasio.writer.get_section_order_function` |
273
|
|
|
|
274
|
|
|
''' |
275
|
|
|
section_widths = { |
276
|
|
|
'left_width': None, |
277
|
|
|
'middle_width': None |
278
|
|
|
} |
279
|
|
|
if len(items) > 0: |
280
|
|
|
section_widths['left_width'] = max( |
281
|
|
|
[len(i.original_mnemonic) for i in items]) |
282
|
|
|
middle_widths = [] |
283
|
|
|
for i in items: |
284
|
|
|
order = order_func(i.mnemonic) |
285
|
|
|
rhs_element = order.split(':')[0] |
286
|
|
|
logger.debug( |
287
|
|
|
'get_section_widths %s\n\torder=%s rhs_element=%s' % ( |
288
|
|
|
i, order, rhs_element)) |
289
|
|
|
middle_widths.append( |
290
|
|
|
len(str(i.unit)) + 1 + len(str(i[rhs_element]))) |
291
|
|
|
section_widths['middle_width'] = max(middle_widths) |
292
|
|
|
return section_widths |
293
|
|
|
|