From 91e325c95a8c9693d9b710c32e140d669796015d Mon Sep 17 00:00:00 2001 From: Robert Smallshire Date: Wed, 3 Dec 2014 20:38:08 +0100 Subject: [PATCH] Some progress towards complete support for writing SEG Y --- textual_reel_header_definition.py | 147 ++++++++++++++++++++++++++++++ toolkit.py | 111 +++++++++++++++++++++- util.py | 55 +++++++++++ 3 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 textual_reel_header_definition.py diff --git a/textual_reel_header_definition.py b/textual_reel_header_definition.py new file mode 100644 index 0000000..3ceccad --- /dev/null +++ b/textual_reel_header_definition.py @@ -0,0 +1,147 @@ +from revisions import SEGY_REVISION_0, SEGY_REVISION_1 + +TEMPLATE = """ +C 1 CLIENT { client } COMPANY { company } CREW NO {crew } +C 2 LINE { line } AREA { area } MAP ID { map_id } +C 3 REEL NO {reelnum} DAY-START OF REEL {d} YEAR {yr} OBSERVER {observer } +C 4 INSTRUMENT: MFG { mfg } MODEL { model } SERIAL NO { serial } +C 5 DATA TRACES/RECORD {dtpr} AUXILIARY TRACES/RECORD {atpr } CDP FOLD {cdpfold} +C 6 SAMPLE INTERVAL {intvl} SAMPLES/TRACE {spt} BITS/IN {bi} BYTES/SAMPLE {bps } +C 7 RECORDING FORMAT {rfmt} FORMAT THIS REEL {ftr } MEASUREMENT SYSTEM {measmnt} +C 8 SAMPLE CODE: FLOATING PT {f} FIXED PT {x} FIXED PT-GAIN {g} CORRELATED {cor} +C 9 GAIN TYPE: FIXED {i} BINARY {b} FLOATING POINT {c} OTHER {other } +C10 FILTERS: ALIAS {a }HZ NOTCH {n }HZ BAND {b1}-{b2 }HZ SLOPE {s}-{s2}DB/OCT +C11 SOURCE: TYPE {type } NUMBER/POINT {npt } POINT INTERVAL {point_interval } +C12 PATTERN: {source_pattern } LENGTH {lent} WIDTH {width } +C13 SWEEP: START {ss}HZ END {se}HZ LENGTH {lms}MS CHANNEL NO {q} TYPE {cd } +C14 TAPER: START LENGTH {tsl }MS END LENGTH {tel }MS TYPE {taper_type } +C15 SPREAD: OFFSET {off } MAX DISTANCE {md } GROUP INTERVAL {group_interval } +C16 GEOPHONES: PER GROUP {p} SPACING {y} FREQUENCY {r} MFG {gmfg } MODEL {gmod} +C17 PATTERN: {geophone_pattern } LENGTH {glen} WIDTH {geophone_width } +C18 TRACES SORTED BY: RECORD {u} CDP {v} OTHER { sort_other } +C19 AMPLITUDE RECOVERY: NONE {ar} SPHERICAL DIV {sd } AGC {} OTHER {ar_other } +C20 MAP PROJECTION {map_projection } ZONE ID {zid} COORDINATE UNITS {co_units } +C21 PROCESSING: { processing1 } +C22 PROCESSING: { processing2 } +C23 { unassigned1 } +C24 { unassigned2 } +C25 { unassigned3 } +C26 { unassigned4 } +C27 { unassigned5 } +C28 { unassigned6 } +C29 { unassigned7 } +C30 { unassigned8 } +C31 { unassigned9 } +C32 { unassigned10 } +C33 { unassigned11 } +C34 { unassigned12 } +C35 { unassigned13 } +C36 { unassigned14 } +C37 { unassigned15 } +C38 { unassigned16 } +C39 { unassigned17 } +C40 { end_marker } +""" + + +END_TEXTUAL_HEADER = 'END TEXTUAL HEADER' +END_EBCDIC = 'END EBCDIC' + +END_MARKERS = {SEGY_REVISION_0: END_EBCDIC, + SEGY_REVISION_1: END_TEXTUAL_HEADER} + +TEMPLATE_FIELD_NAMES = {'client': 'client', + 'company': 'company', + 'crew': 'crew_number', + 'line': 'line', + 'area': 'area', + 'map_id': 'map_id', + 'reelnum': 'reel_number', + 'd': 'day_start_of_reel', + 'yr': 'year', + 'observer': 'observer', + 'mfg': 'instrument_manufacturer', + 'model': 'instrument_model', + 'serial': 'instrument_serial', + 'dtpr': 'data_traces_per_record', + 'atpr': 'auxiliary_traces_per_record', + 'cdpfold': 'cdp_fold', + 'intvl': 'sample_interval', + 'spt': 'samples_per_trace', + 'bi': 'bits_per_inch', + 'bps': 'bytes_per_sample', + 'rfmt': 'recording_format', + 'ftr': 'format_this_reel', + 'measmnt': 'measurement_system', + 'f': 'sample_code_floating_point', + 'x': 'sample_code_fixed_point', + 'g': 'sample_code_fixed_point_gain', + 'cor': 'sample_code_correlated', + 'i': 'gain_type_fixed', + 'b': 'gain_type_binary', + 'c': 'gain_type_fixed_point_gain', + 'other': 'gain_type_other', + 'a': 'filters_alias_hz', + 'n': 'filters_notch_hz', + 'b1': 'filters_band_lower_hz', + 'b2': 'filters_band_upper_hz', + 's': 'filters_slope_lower_db_per_oct', + 's2': 'filters_slope_upper_db_per_oct', + 'type': 'source_type', + 'npt': 'source_number_per_point', + 'point_interval': 'source_point_interval', + 'source_pattern': 'source_pattern', + 'lent': 'source_length', + 'width': 'source_width', + 'ss': 'sweep_start_hz', + 'se': 'sweep_end_hz', + 'lms': 'sweep_length_ms', + 'q': 'sweep_channel_number', + 'cd': 'sweep_type', + 'tsl': 'taper_start_length_ms', + 'tel': 'taper_end_length_ms', + 'taper_type': 'taper_type', + 'off': 'spread_offset', + 'md': 'spread_max_distance', + 'group_interval': 'spread_group_interval', + 'p': 'geophones_per_group', + 'y': 'geophone_spacing', + 'r': 'geophone_frequency', + 'gmfg': 'geophone_manufacturer', + 'gmod': 'geophone_model', + 'geophone_pattern': 'geophone_pattern', + 'glen': 'geophone_length', + 'geophone_width': 'geophone_width', + 'u': 'traces_sorted_by_record', + 'v': 'traces_sorted_by_cdp', + 'sort_other': 'traces_sorted_by_other', + 'ar': 'amplitude_recovery_none', + 'sd': 'amplitude_recovery_spherical_div', + '': 'amplitude_recovery_agc', + 'ar_other': 'amplitude_recovery_other', + 'map_projection': 'map_projection', + 'zid': 'zone_id', + 'co_units': 'coordinate_units', + 'processing1': 'processing1', + 'processing2': 'processing2', + 'unassigned1': 'unassigned1', + 'unassigned2': 'unassigned2', + 'unassigned3': 'unassigned3', + 'unassigned4': 'unassigned4', + 'unassigned5': 'unassigned5', + 'unassigned6': 'unassigned6', + 'unassigned7': 'unassigned7', + 'unassigned8': 'unassigned8', + 'unassigned9': 'unassigned9', + 'unassigned10': 'unassigned10', + 'unassigned11': 'unassigned11', + 'unassigned12': 'unassigned12', + 'unassigned13': 'unassigned13', + 'unassigned14': 'unassigned14', + 'unassigned15': 'unassigned15', + 'unassigned16': 'unassigned16', + 'unassigned17': 'unassigned17', + 'end_marker': 'end_marker' + } + +INV_TEMPLATE_FIELD_NAMES = dict((v, k) for k, v in TEMPLATE_FIELD_NAMES.items()) diff --git a/toolkit.py b/toolkit.py index 9024194..341a768 100644 --- a/toolkit.py +++ b/toolkit.py @@ -1,8 +1,10 @@ from array import array -from collections import namedtuple +from collections import namedtuple, OrderedDict import itertools +from itertools import zip_longest import os import struct +import re from catalog import CatalogBuilder from datatypes import CTYPES, size_in_bytes @@ -10,10 +12,13 @@ from encoding import guess_encoding from binary_reel_header_definition import HEADER_DEF from ibm_float import ibm2ieee, ieee2ibm from revisions import canonicalize_revision +import textual_reel_header_definition from trace_header_definition import TRACE_HEADER_DEF -from util import file_length, batched, pad, round_up +from util import file_length, batched, pad, round_up, complementary_slices from portability import EMPTY_BYTE_STRING + + CARD_LENGTH = 80 CARDS_PER_HEADER = 40 @@ -476,6 +481,95 @@ def unpack_values(buf, count, item_size, fmt, endian='>'): # swapping ourselves. +def format_standard_textual_header(revision, **kwargs): + """Produce a standard SEG Y textual header. + + Args: + revision: The SEG Y revision. + + **kwargs: Named arguments corresponding to the values in the + textual_reel_header_definition.TEMPLATE_FIELD_NAMES dictionary, + which in turn correspond to the placeholders in the + textual_reel_header_definition.TEMPLATE string. Any omitted + arguments will result in placeholders being replaced by spaces. + If the end_marker argument is not supplied, an appropriate end + marker will be selected based on the SEG Y revision. For standard + end markers consider using textual_reel_header_definition.END_TEXTUAL_HEADER + or textual_reel_header_definition.END_EBCDIC. + + Returns: + A list of forty Unicode strings. + + Usage: + header = format_standard_textual_header(1, + client="Lundin", + company="Western Geco", + crew_number=123, + processing1="Sixty North AS", + sweep_start_hz=10, + sweep_end_hz=1000, + sweep_length_ms=10000, + sweep_channel_number=3, + sweep_type='spread') + + """ + + kwargs.setdefault('end_marker', textual_reel_header_definition.END_MARKERS[revision]) + + template = textual_reel_header_definition.TEMPLATE + + placeholder_slices = parse_template(template) + background_slices = complementary_slices(placeholder_slices.values(), 0, len(template)) + + chunks = [] + for bg_slice, placeholder in zip_longest(background_slices, placeholder_slices.items()): + + if bg_slice is not None: + chunks.append(template[bg_slice]) + + if placeholder is not None: + ph_name, ph_slice = placeholder + ph_arg_name = textual_reel_header_definition.TEMPLATE_FIELD_NAMES[ph_name] + ph_value = kwargs.pop(ph_arg_name, '') + ph_len = ph_slice.stop - ph_slice.start + substitute = str(ph_value)[:ph_len].ljust(ph_len, ' ') + chunks.append(substitute) + + if len(kwargs) > 0: + raise TypeError("The following keyword arguments did not correspond to template placeholders: {!r}" + .format(list(kwargs.keys()))) + + concatenation = ''.join(chunks) + lines = concatenation.splitlines(keepends=False) + + return lines[1:] # Omit the first and last lines, which are artifacts of the multiline string template + + +def parse_template(template): + """Parse a template to produce a dictionary of placeholders. + + Args: + template: The template string containing { field-name } style fixed-width fields. + + Returns: + A OrderedDict mapping field names to slices objects which can be used to index + into the template string. The order of the entries is the same as the order within + which they occur in the template. + """ + PATTERN = r'\{\s*(\w*)\s*\}' + regex = re.compile(PATTERN) + matches = regex.finditer(template) + + fields = OrderedDict() + for match in matches: + name = match.group(1) + start = match.start() + end = match.end() + fields[name] = slice(start, end) + + return fields + + def write_textual_reel_header(fh, lines, encoding): """Write the SEG Y card image header, also known as the textual header @@ -721,3 +815,16 @@ def _compile_trace_header_record(): TraceHeader = _compile_trace_header_record() +if __name__ == '__main__': + from pprint import pprint as pp + header = format_standard_textual_header(1, + client="Lundin", + company="Western Geco", + crew_number=123, + processing1="Sixty North AS", + sweep_start_hz=10, + sweep_end_hz=1000, + sweep_length_ms=10000, + sweep_channel_number=3, + sweep_type='spread') + pp(header, width=200) \ No newline at end of file diff --git a/util.py b/util.py index 8ee8359..770288e 100644 --- a/util.py +++ b/util.py @@ -42,6 +42,61 @@ def pad(iterable, padding=None, size=None): return itertools.islice(pad(iterable, padding), size) +def complementary_slices(slices, start=None, stop=None): + """Compute a complementary set of slices which alternate with given slices to form a contiguous range. + + Given, + + Start Stop + [-----) [-----) [----) + + produces, + + [--) [----) [-) [---) + + Args: + slices: An sequence of existing slices + start: An optional start index, defaults to the start of the first slice. + stop: An optional one-beyond-the-end index, defaults to the stop attribute of the last slice. + + Yields: + A complementary series of slices which alternate with the supplied slices. The number of returned + slices will always be len(slices) + 1 since both leading and trailing slices will always be returned. + Note the some of the returned slices may be 'empty' (having zero length). + """ + if start is None: + start = slices[0].start + + if stop is None: + stop = slices[-1].stop + + index = start + for s in slices: + yield slice(index, s.start) + index = s.stop + + yield slice(index, stop) + + +def roundrobin(*iterables): + """Take items from each iterable in turn until all iterables are exhausted. + + roundrobin('ABC', 'D', 'EF') --> A D E B F C + """ + # Recipe credited to George Sakkis + pending = len(iterables) + nexts = itertools.cycle(iter(it).__next__ for it in iterables) + while pending: + try: + for n in nexts: + print + yield n() + except StopIteration: + pending -= 1 + nexts = itertools.cycle(itertools.islice(nexts, pending)) + + + def contains_duplicates(sorted_iterable): """Determine in an iterable series contains duplicates.