diff --git a/segpy/binary_headers.py b/segpy/binary_headers.py deleted file mode 100644 index 28e8312..0000000 --- a/segpy/binary_headers.py +++ /dev/null @@ -1,41 +0,0 @@ -from segpy.field import field_mangler, BinaryFieldDescriptor, ValueFieldDescriptor - - -class BinaryFormat(object, - metaclass=field_mangler(descriptor=BinaryFieldDescriptor)): - """Base class for headers format definitions comprised of BinaryFields.""" - - def __init__(self, **field_types_and_offsets): - """Override default types and offsets. - - Args: - **field_types_and_offsets: Keywords arguments, with names corresponding to the - binary_fields may be provided where each name is associated with a 2-tuple - containing a field_type and an offset in bytes. - """ - for name, (field_type, offset) in field_types_and_offsets: - if not hasattr(self, name): - raise TypeError("Unknown attribute {!r}".format(name)) - fld = getattr(self, name) - fld.ftype = field_type - fld.offset = offset - # TODO: Consider validating for overlaps - - -class Header(object, - metaclass=field_mangler(descriptor=ValueFieldDescriptor)): - """Base class for headers comprised of ValueFields.""" - - def __init__(self, **field_values): - """Set field values. - - Args: - **field_values: Keywords arguments, with names corresponding to the - trace_header_fields may be provided where each name is associated with a - value of the correct type. - """ - for name, value in field_values: - if not hasattr(self, name): - raise TypeError("Unknown attribute {!r}".format(name)) - fld = getattr(self, name) - fld.value = value \ No newline at end of file diff --git a/segpy/header.py b/segpy/header.py index 6fdfad7..7eb9e18 100644 --- a/segpy/header.py +++ b/segpy/header.py @@ -1,7 +1,35 @@ from collections import OrderedDict from weakref import WeakKeyDictionary +from itertools import chain + from segpy.docstring import docstring_property -from segpy.util import is_magic_name, underscores_to_camelcase, first_sentence, ensure_contains, conjoin +from segpy.util import underscores_to_camelcase, first_sentence, super_class + + +class Header: + """An abstract base class for header format definitions.""" + + def __init__(self, **kwargs): + # TODO: This has terrible performance - better to handle validation optimistically by overriding __getattr__() + for keyword, arg in kwargs.items(): + if keyword not in self.ordered_field_names(): + raise TypeError("{!r} is not a recognised field name for {!r}".format(keyword, self.__class__.__name__)) + setattr(self, keyword, arg) + + _ordered_field_names = tuple() + + @classmethod + def ordered_field_names(cls): + """The ordered list of field names. + + This is a metamethod which should be called on cls. + + Returns: + An tuple containing the field names in order. + """ + if cls is Header: + return cls._ordered_field_names + return super_class(cls).ordered_field_names() + cls._ordered_field_names class FormatMeta(type): @@ -17,10 +45,15 @@ class FormatMeta(type): # TODO: This is a good point to validate that the fields are in order and that the # TODO: format specification is valid. We shouldn't even build the class otherwise. + # TODO: Also validate existence of LENGTH_IN_BYTES - # TODO: In the case of inheritance, this collection may already exist - namespace['_ordered_field_names'] = tuple(name for name in namespace.keys() - if not is_magic_name(name)) + namespace['_ordered_field_names'] = tuple(name for name, attr in namespace.items() + if isinstance(attr, HeaderFieldDescriptor)) + + transitive_bases = set(chain.from_iterable(type(base).mro(base) for base in bases)) + + if Header not in transitive_bases: + bases = (Header,) + bases for attr_name, attr in namespace.items(): @@ -28,7 +61,7 @@ class FormatMeta(type): # help(class), help(instance), help(class.property) and help(instance.property) # Set the _name attribute of the field instance if it hasn't already been set - if isinstance(attr, NamedField): + if isinstance(attr, HeaderFieldDescriptor): if attr._name is None: attr._name = attr_name @@ -64,7 +97,7 @@ class NamedField: @property def offset(self): - "The offset it bytes from the beginning of the header." + "The offset in bytes from the beginning of the header." return self._offset @property @@ -81,12 +114,13 @@ class NamedField: def __doc__(self): return first_sentence(self._documentation) - # TODO: Uncomment this to get HELP - # def __repr__(self): - # return first_sentence(self._documentation) - def __repr__(self): - return "{}()".format(self.__class__.__name__) + return "{}(name={!r}, value_type={!r}, offset={!r}, default={!r})".format( + self.__class__.__name__, + self.name, + self.value_type.__name__, + self.offset, + self.default) def field(value_type, offset, default, documentation): @@ -109,95 +143,58 @@ def field(value_type, offset, default, documentation): # renamed when the NamedDescriptorMangler metaclass does its job, to # a class name based on the field name. - class SpecificField(NamedField): + class SpecificField(HeaderFieldDescriptor): pass return SpecificField(value_type, offset, default, documentation) -class ValueField: +class HeaderFieldDescriptor: - def __init__(self, name, value_type, default, documentation): - self._name = name - self._value_type = value_type - self._default = default - self._documentation = documentation + def __init__(self, value_type, offset, default, documentation): + self._named_field = NamedField(value_type, offset, default, documentation) self._instance_data = WeakKeyDictionary() + @property + def _name(self): + return self._named_field.name + + @_name.setter + def _name(self, value): + self._named_field._name = value + def __get__(self, instance, owner): - if owner is None: - return self + """Retrieve the format or instance data. + + When called on the class we return a NamedField instance containing the format data. For example: + + line_seq_num_default = TraceHeaderRev1.line_sequence_num.default + line_seq_num_offset = TraceHeaderRev1.line_sequence_num.offset + + When called on an instance we return the field value. + + line_seq_num = my_trace_header.line_sequence_num + """ + #print("HeaderFieldDescriptor.__get__({!r}, {!r}, {!r})".format(self, instance, owner)) + if instance is None: + return self._named_field if instance not in self._instance_data: - return self._default + return self._named_field.default return self._instance_data[instance] def __set__(self, instance, value): + """Set the field value.""" try: - self._instance_data[instance] = self._value_type(value) + self._instance_data[instance] = self._named_field._value_type(value) except ValueError as e: raise ValueError("Assigned value {!r} for {} attribute must be convertible to {}: {}" - .format(value, self._name, self._value_type.__name__, e)) from e + .format(value, self._name, self._named_field._value_type.__name__, e)) from e def __delete__(self, instance): raise AttributeError("Can't delete {} attribute".format(self._name)) @docstring_property(__doc__) def __doc__(self): - return self._documentation + return self._named_field._documentation # TODO: Get documentation of these descriptors working correctly - - -class BuildFromFormat(type): - """A metaclass for building a data transfer object from a format definition.""" - - def __new__(mcs, name, bases, namespace, format_class): - """Create a new DTO class from a format class.""" - if format_class.__class__ is not FormatMeta: - raise TypeError("Format class {} specified for class {} does not use the FormatMeta metaclass" - .format(format_class.__name__, name)) - - bases = ensure_contains(bases, HeaderBase) - - namespace['_format'] = format_class - - for field_name in format_class._ordered_field_names: - format_field = getattr(format_class, field_name) - namespace[field_name] = ValueField( - name=field_name, - value_type=format_field.value_type, - default=format_field.default, - documentation=format_field.documentation) - - return super().__new__(mcs, name, bases, namespace) - - def __init__(mcs, name, bases, namespace, format_class): - super().__init__(mcs, name, bases) - pass - - -class HeaderBase: - - _ordered_field_names = tuple() - - def __init__(self, **kwargs): - for keyword, value in kwargs.items(): - setattr(self, keyword, value) - - def __repr__(self): - field_names = self._format._ordered_field_names - return '{}({})'.format(self.__class__.__name__, - ', '.join('{}={}'.format(name, getattr(self, name)) for name in field_names)) - - @classmethod - def ordered_field_names(cls): - mro = cls.mro() - print("mro =", mro) - super_class = mro[1] - print("super_class =", super_class) - if not isinstance(super_class, HeaderBase): - super_field_names = tuple() - else: - super_field_names = super_class.ordered_field_names() - print("super_field_names =", super_field_names) - return conjoin(super_field_names, cls._format._ordered_field_names) \ No newline at end of file diff --git a/segpy/packer.py b/segpy/packer.py index e8dcc06..0b4be2e 100644 --- a/segpy/packer.py +++ b/segpy/packer.py @@ -1,6 +1,7 @@ from collections import OrderedDict from struct import Struct from segpy.datatypes import SEG_Y_TYPE_TO_CTYPE +from segpy.portability import izip_longest from segpy.util import pairwise, intervals_partially_overlap, complementary_intervals @@ -8,19 +9,24 @@ def size_of(t): return t.SIZE -def compile_struct(header_format_class, endian='>'): +def compile_struct(header_format_class, length_in_bytes=None, endian='>'): """Compile a struct description from a record. Args: header_format_class: A header_format class. + length_in_bytes: Optional length in bytes for the header. If the supplied header described a format shorter + than this value the returned format will be padded with placeholders for bytes to be discarded. If the + value is less than the minimum required for the format described by header_format_class an error will be + raised. + endian: '>' for big-endian data (the standard and default), '<' for little-endian (non-standard). Returns: A two-tuple containing in the zeroth element a format string which can be used with the struct.unpack function, - and in the second element containing a list-of-lists for field names. Each item in the out list corresponds to - an element of the tuple of data values returned by struct.unpack(); each name associated with that index is a + and in the second element containing a list-of-lists for field names. Each item in the outer list corresponds + to an element of the tuple of data values returned by struct.unpack(); each name associated with that index is a field to which the unpacked value should be assigned. format, allocations = compile_struct(TraceHeaderFormat) @@ -31,16 +37,34 @@ def compile_struct(header_format_class, endian='>'): field_names_to_values[field_name] = value header = Header(**field_names_to_values) + Raises: + ValueError: If header_format_class defines no fields. + ValueError: If header_format_class contains fields which overlap but are not exactly coincident. + ValueError: If header_format_class contains coincident fields of different types. + ValueError: If header_format_class described a format longer than length_in_bytes. + """ - fields = [getattr(header_format_class, name) for name in header_format_class._ordered_field_names] + fields = [getattr(header_format_class, name) for name in header_format_class.ordered_field_names()] sorted_fields = sorted(fields, key=lambda f: f.offset) - for a, b in pairwise(sorted_fields): - if intervals_partially_overlap(range(a.offset, a.offset + size_of(a.value_type)), - range(b.offset, b.offset + size_of(b.value_type))): - raise ValueError("Record fields {!r} at offset {} and {!r} at offset {} are distinct but overlap." - .format(a.name, a.offset, b.name, b.offset)) + if len(sorted_fields) < 1: + raise ValueError("Header format class {!r} defines no fields".format(header_format_class.__name__)) + + if len(sorted_fields) > 1: + for a, b in pairwise(sorted_fields): + if intervals_partially_overlap(range(a.offset, a.offset + size_of(a.value_type)), + range(b.offset, b.offset + size_of(b.value_type))): + raise ValueError("Record fields {!r} at offset {} and {!r} at offset {} are distinct but overlap." + .format(a.name, a.offset, b.name, b.offset)) + + last_field = sorted_fields[-1] + defined_length = last_field.offset + size_of(last_field.value_type) + specified_length = defined_length if (length_in_bytes is None) else length_in_bytes + padding_length = specified_length - defined_length + if padding_length < 0: + raise ValueError("Header length {!r} bytes defined by {!r} is less than specified length in bytes {!r}" + .format(defined_length, header_format_class.__name__, specified_length)) offset_to_fields = OrderedDict() for field in sorted_fields: @@ -60,15 +84,18 @@ def compile_struct(header_format_class, endian='>'): field_spans = [range(offset, offset + size_of(fields[0].value_type)) for offset, fields in offset_to_fields.items()] - gap_spans = complementary_intervals(field_spans) + gap_intervals = complementary_intervals(field_spans, start=1, stop=specified_length + 1) # One-based indexes # Create a format string usable with the struct module - format_chunks = [] + format_chunks = [endian] representative_fields = (fields[0] for fields in offset_to_fields.values()) - for field, gap_span in zip(representative_fields, gap_spans): - format_chunks.append(SEG_Y_TYPE_TO_CTYPE[field.value_type.SEG_Y_TYPE]) - format_chunks.append('x' * len(gap_span)) - cformat = endian + ''.join(format_chunks) + for gap_interval, field in izip_longest(gap_intervals, representative_fields, fillvalue=None): + gap_length = len(gap_interval) + if gap_length > 0: + format_chunks.append('x' * gap_length) + if field is not None: + format_chunks.append(SEG_Y_TYPE_TO_CTYPE[field.value_type.SEG_Y_TYPE]) + cformat = ''.join(format_chunks) # Create a list of mapping item index to field names. # [0] -> ['field_1', 'field_2'] @@ -80,16 +107,24 @@ def compile_struct(header_format_class, endian='>'): class HeaderPacker: + """Packing and unpacking header instances.""" def __init__(self, header_format_class, endian='>'): self._header_format_class = header_format_class - self._format, self._field_name_allocations = compile_struct(header_format_class, endian) + self._format, self._field_name_allocations = compile_struct( + header_format_class, + header_format_class.LENGTH_IN_BYTES, + endian) self._struct = Struct(self._format) + @property + def header_format_class(self): + return self._header_format_class + def pack(self, header): """Pack a header into a buffer. """ - if header._format is not self._header_format_class: + if not isinstance(header, self._header_format_class): raise TypeError("{}({}) cannot pack header of type {}.".format( self.__class__.__name__, self._header_format_class.__name__, @@ -98,7 +133,7 @@ class HeaderPacker: values = [getattr(header, names[0]) for names in self._field_name_allocations] return self._struct.pack(*values) - def unpack(self, buffer, header_class): + def unpack(self, buffer): """Unpack a header into a header object. Overwrites any existing header field values with new values @@ -107,22 +142,24 @@ class HeaderPacker: Returns: The header object. """ - if header_class._format is not self._header_format_class: - raise TypeError("{}({}) cannot unpack header of type {}.".format( - self.__class__.__name__, - self._header_format_class.__name__, - header_class.__name__ - )) - values = self._struct.unpack(buffer) kwargs = {name: value for names, value in zip(self._field_name_allocations, values) - for name in names } + for name in names} - return header_class(**kwargs) + return self._header_format_class(**kwargs) def __repr__(self): return "{}({})".format( self.__class__.__name__, - self._header_format_class.__name__) \ No newline at end of file + self._header_format_class.__name__) + + +def main(): + from segpy.trace_header import TraceHeaderRev0 + compile_struct(TraceHeaderRev0, 240) + pass + +if __name__ == '__main__': + main() diff --git a/segpy/reader.py b/segpy/reader.py index 034e65d..99774fe 100644 --- a/segpy/reader.py +++ b/segpy/reader.py @@ -3,7 +3,7 @@ from segpy.encoding import ASCII from segpy.packer import HeaderPacker from segpy.portability import seekable -from segpy.trace_header import TraceHeaderFormatRev1 +from segpy.trace_header import TraceHeaderRev1 from segpy.util import file_length, filename_from_handle from segpy.datatypes import DATA_SAMPLE_FORMAT_TO_SEG_Y_TYPE, SEG_Y_TYPE_DESCRIPTION, SEG_Y_TYPE_TO_CTYPE, size_in_bytes from segpy.toolkit import (extract_revision, @@ -19,7 +19,7 @@ from segpy.toolkit import (extract_revision, guess_textual_header_encoding) -def create_reader(fh, encoding=None, endian='>', progress=None): +def create_reader(fh, encoding=None, trace_header_format=TraceHeaderRev1, endian='>', progress=None): """Create a SegYReader (or one of its subclasses) based on performing a scan of SEG Y data. @@ -39,6 +39,8 @@ def create_reader(fh, encoding=None, endian='>', progress=None): None (the default) a heuristic will be used to guess the header encoding. + trace_header_format: The class defining the layout of the trace header. + endian: '>' for big-endian data (the standard and default), '<' for little-endian (non-standard) @@ -103,18 +105,19 @@ def create_reader(fh, encoding=None, endian='>', progress=None): revision = extract_revision(binary_reel_header) bps = bytes_per_sample(binary_reel_header, revision) - trace_offset_catalog, trace_length_catalog, cdp_catalog, line_catalog = catalog_traces(fh, bps, endian, progress) + trace_offset_catalog, trace_length_catalog, cdp_catalog, line_catalog = catalog_traces(fh, bps, trace_header_format, + endian, progress) if cdp_catalog is not None and line_catalog is None: return SegYReader2D(fh, textual_reel_header, binary_reel_header, extended_textual_header, trace_offset_catalog, - trace_length_catalog, cdp_catalog, encoding, endian) + trace_length_catalog, cdp_catalog, trace_header_format, encoding, endian) if cdp_catalog is None and line_catalog is not None: return SegYReader3D(fh, textual_reel_header, binary_reel_header, extended_textual_header, trace_offset_catalog, - trace_length_catalog, line_catalog, encoding, endian) + trace_length_catalog, line_catalog, trace_header_format, encoding, endian) return SegYReader(fh, textual_reel_header, binary_reel_header, extended_textual_header, trace_offset_catalog, - trace_length_catalog, encoding, endian) + trace_length_catalog, trace_header_format, encoding, endian) class SegYReader(object): @@ -131,6 +134,7 @@ class SegYReader(object): extended_textual_headers, trace_offset_catalog, trace_length_catalog, + trace_header_format, encoding, endian='>'): """Initialize a SegYReader around a file-like-object. @@ -156,6 +160,8 @@ class SegYReader(object): trace_length_catalog: A mapping from zero-based trace_samples index to the number of samples in that trace_samples. + trace_header_format: The class defining the layout of the trace header. + encoding: Either ASCII or EBCDIC. endian: '>' for big-endian data (the standard and default), '<' for @@ -170,7 +176,7 @@ class SegYReader(object): self._binary_reel_header = binary_reel_header self._extended_textual_headers = extended_textual_headers - self._trace_header_packer = HeaderPacker(TraceHeaderFormatRev1) # Deal with hardwiring + self._trace_header_packer = HeaderPacker(trace_header_format, endian) self._trace_offset_catalog = trace_offset_catalog self._trace_length_catalog = trace_length_catalog @@ -373,6 +379,7 @@ class SegYReader3D(SegYReader): trace_offset_catalog, trace_length_catalog, line_catalog, + trace_header_format, encoding, endian='>'): """Initialize a SegYReader3D around a file-like-object. @@ -396,13 +403,16 @@ class SegYReader3D(SegYReader): line_catalog: A mapping from (xline, inline) tuples to trace_indexes. + trace_header_format: The class defining the layout of the trace header. + encoding: Either ASCII or EBCDIC. endian: '>' for big-endian data (the standard and default), '<' for little-endian (non-standard) """ super(SegYReader3D, self).__init__(fh, textual_reel_header, binary_reel_header, extended_textual_headers, - trace_offset_catalog, trace_length_catalog, encoding, endian) + trace_offset_catalog, trace_length_catalog, trace_header_format, + encoding, endian) self._line_catalog = line_catalog self._num_inlines = None self._num_xlines = None @@ -514,6 +524,7 @@ class SegYReader2D(SegYReader): trace_offset_catalog, trace_length_catalog, cdp_catalog, + trace_header_format, encoding, endian='>'): """Initialize a SegYReader2D around a file-like-object. @@ -536,13 +547,16 @@ class SegYReader2D(SegYReader): cdp_catalog: A mapping from CDP numbers to trace_indexes. + trace_header_format: The class defining the layout of the trace header. + encoding: Either ASCII or EBCDIC. endian: '>' for big-endian data (the standard and default), '<' for little-endian (non-standard) """ super(SegYReader2D, self).__init__(fh, textual_reel_header, binary_reel_header, extended_textual_headers, - trace_offset_catalog, trace_length_catalog, encoding, endian) + trace_offset_catalog, trace_length_catalog, trace_header_format, + encoding, endian) self._cdp_catalog = cdp_catalog def _dimensionality(self): @@ -653,11 +667,6 @@ def main(argv=None): print(segy_reader.extended_textual_header) print("=== END EXTENDED TEXTUAL_HEADER ===") - for trace_index in segy_reader.trace_indexes(): - trace_header = segy_reader.trace_header(trace_index) - print("Inline {}, Crossline {}, Shotpoint {}".format(trace_header.Inline3D, trace_header.Crossline3D, - trace_header.ShotPoint)) - if __name__ == '__main__': main() diff --git a/segpy/toolkit.py b/segpy/toolkit.py index da3c761..e1de337 100644 --- a/segpy/toolkit.py +++ b/segpy/toolkit.py @@ -18,7 +18,7 @@ from segpy.binary_reel_header_definition import HEADER_DEF from segpy.ibm_float import IBMFloat from segpy.packer import HeaderPacker from segpy.revisions import canonicalize_revision -from segpy.trace_header import TraceHeaderFormatRev1, TraceHeaderRev1 +from segpy.trace_header import TraceHeaderRev1 from segpy.util import file_length, batched, pad, complementary_intervals, NATIVE_ENDIANNESS from segpy.portability import EMPTY_BYTE_STRING, izip_longest @@ -288,7 +288,7 @@ _READ_PROPORTION = 0.75 # The proportion of time spent in catalog_traces # reading the file. Determined empirically. -def catalog_traces(fh, bps, endian='>', progress=None): +def catalog_traces(fh, bps, trace_header_format=TraceHeaderRev1, endian='>', progress=None): """Build catalogs to facilitate random access to trace_samples data. Note: @@ -315,6 +315,9 @@ def catalog_traces(fh, bps, endian='>', progress=None): bps: The number of bytes per sample, such as obtained by a call to bytes_per_sample() + trace_header_format: The class defining the trace header format. + Defaults to TraceHeaderRev1. + endian: '>' for big-endian data (the standard and default), '<' for little-endian (non-standard) @@ -336,8 +339,7 @@ def catalog_traces(fh, bps, endian='>', progress=None): if not callable(progress_callback): raise TypeError("catalog_traces(): progress callback must be callable") - #trace_header_format = compile_trace_header_format(endian) - trace_header_packer = HeaderPacker(TraceHeaderFormatRev1) + trace_header_packer = HeaderPacker(trace_header_format, endian) length = file_length(fh) @@ -355,8 +357,7 @@ def catalog_traces(fh, bps, endian='>', progress=None): data = fh.read(TRACE_HEADER_NUM_BYTES) if len(data) < TRACE_HEADER_NUM_BYTES: break - #trace_header = TraceHeader._make(trace_header_format.unpack(data)) - trace_header = trace_header_packer.unpack(data, TraceHeaderRev1) + trace_header = trace_header_packer.unpack(data) num_samples = trace_header.num_samples trace_length_catalog_builder.add(trace_number, num_samples) @@ -418,7 +419,7 @@ def read_trace_header(fh, trace_header_packer, pos=None): data = fh.read(TRACE_HEADER_NUM_BYTES) # trace_header = TraceHeader._make( # trace_header_format.unpack(data)) - trace_header = trace_header_packer.unpack(data, TraceHeaderRev1) # TODO: Remove hardwired TraceHeaderRev1 + trace_header = trace_header_packer.unpack(data) return trace_header @@ -745,7 +746,7 @@ def write_extended_textual_headers(fh, pages, encoding): fh.write(concatenated_page) -def write_trace_header(fh, trace_header, trace_header_format, pos=None): +def write_trace_header(fh, trace_header, trace_header_packer, pos=None): """Write a TraceHeader to file. Args: @@ -753,8 +754,8 @@ def write_trace_header(fh, trace_header, trace_header_format, pos=None): trace_header: A TraceHeader object. - trace_header_format: A Struct object, such as obtained from a - call to compile_trace_header_format() + trace_header_packer: A Packer object configured for the trace + header format. pos: An optional file offset in bytes from the beginning of the file. Defaults to the current file position. @@ -762,7 +763,7 @@ def write_trace_header(fh, trace_header, trace_header_format, pos=None): if pos is not None: fh.seek(pos, os.SEEK_SET) - buf = trace_header_format.pack(trace_header) + buf = trace_header_packer.pack(trace_header) fh.write(buf) diff --git a/segpy/trace_header.py b/segpy/trace_header.py index 356b4f0..4af2336 100644 --- a/segpy/trace_header.py +++ b/segpy/trace_header.py @@ -1,8 +1,10 @@ -from segpy.header import FormatMeta, field, BuildFromFormat +from segpy.header import FormatMeta, field from segpy.types import Int32, Int16 -class TraceHeaderFormatRev0(metaclass=FormatMeta): +class TraceHeaderRev0(metaclass=FormatMeta): + + LENGTH_IN_BYTES = 240 line_sequence_num = field( Int32, offset=1, default=0, documentation= @@ -386,7 +388,7 @@ class TraceHeaderFormatRev0(metaclass=FormatMeta): ) -class TraceHeaderFormatRev1(TraceHeaderFormatRev0, metaclass=FormatMeta): +class TraceHeaderRev1(TraceHeaderRev0, metaclass=FormatMeta): cdp_x = field( Int32, offset=181, default=0, documentation= @@ -556,14 +558,3 @@ class TraceHeaderFormatRev1(TraceHeaderFormatRev0, metaclass=FormatMeta): "5 = Newton (N), " "6 = Kilograms (kg)" ) - - -class TraceHeaderRev0(metaclass=BuildFromFormat, format_class=TraceHeaderFormatRev0): - pass - - -class TraceHeaderRev1(metaclass=BuildFromFormat, format_class=TraceHeaderFormatRev1): - pass - - - diff --git a/segpy/util.py b/segpy/util.py index 9984d11..776aea2 100644 --- a/segpy/util.py +++ b/segpy/util.py @@ -1,8 +1,10 @@ -import itertools + import time import os import sys +from itertools import (islice, cycle, tee, chain, repeat) + from segpy.portability import izip NATIVE_ENDIANNESS = '<' if sys.byteorder == 'little' else '>' @@ -17,10 +19,13 @@ def pairwise(iterable): Returns: An iterator over 2-tuples. + + Raises: + StopIteration: If the iterable contains fewer than two items. """ - a, b = itertools.tee(iterable) - next(b, None) - return izip(a, b) + a, b = tee(iterable) + next(b) + yield from izip(a, b) def batched(iterable, batch_size, padding=UNSET): @@ -61,8 +66,8 @@ def batched(iterable, batch_size, padding=UNSET): def pad(iterable, padding=None, size=None): if size is None: - return itertools.chain(iterable, itertools.repeat(padding)) - return itertools.islice(pad(iterable, padding), size) + return chain(iterable, repeat(padding)) + return islice(pad(iterable, padding), size) def complementary_intervals(intervals, start=None, stop=None): @@ -106,6 +111,24 @@ def complementary_intervals(intervals, start=None, stop=None): yield interval_type(index, stop) +def intervals_are_contiguous(intervals): + """Determine whether a series of intervals are contiguous. + + Args: + intervals: An iterable series of intervals where each interval is either + a range or slice object. + + Returns: + True if the intervals are in order, contiguous and non-overlapping, + otherwise False. + """ + + for a, b in pairwise(intervals): + if a.stop != b.start: + return False + return True + + def intervals_partially_overlap(interval_a, interval_b): """Determine whether two intervals partially overlap. @@ -130,7 +153,6 @@ def intervals_partially_overlap(interval_a, interval_b): return second_interval.start < first_interval.stop - def roundrobin(*iterables): """Take items from each iterable in turn until all iterables are exhausted. @@ -138,14 +160,14 @@ def roundrobin(*iterables): """ # Recipe credited to George Sakkis pending = len(iterables) - nexts = itertools.cycle(iter(it).__next__ for it in iterables) + nexts = cycle(iter(it).__next__ for it in iterables) while pending: try: for n in nexts: yield n() except StopIteration: pending -= 1 - nexts = itertools.cycle(itertools.islice(nexts, pending)) + nexts = cycle(islice(nexts, pending)) def contains_duplicates(sorted_iterable): @@ -184,6 +206,7 @@ def measure_stride(iterable): return None return stride + def minmax(iterable): """Return the minimum and maximum of an iterable series. @@ -283,4 +306,18 @@ def conjoin(collection, item): def is_magic_name(name): - return len(name) > 4 and name.startswith('__') and name.endswith('__') \ No newline at end of file + return len(name) > 4 and name.startswith('__') and name.endswith('__') + + +def super_class(cls): + """Return the next class in the MRO of cls.""" + mro = cls.mro() + assert len(mro) > 0 + if len(mro) == 1: + assert mro[0] is object + return object + return mro[1] + + +def flatten(sequence_of_sequences): + return chain.from_iterable(sequence_of_sequences) diff --git a/segpy/writer.py b/segpy/writer.py index db97d69..80a6698 100644 --- a/segpy/writer.py +++ b/segpy/writer.py @@ -1,12 +1,16 @@ from segpy.encoding import ASCII, is_supported_encoding, UnsupportedEncodingError -from segpy.toolkit import (write_textual_reel_header, write_binary_reel_header, compile_trace_header_format, - write_trace_header, write_trace_samples, format_extended_textual_header, +from segpy.packer import HeaderPacker +from segpy.trace_header import TraceHeaderRev1 +from segpy.toolkit import (write_textual_reel_header, write_binary_reel_header, + write_trace_header, write_trace_samples, write_extended_textual_headers) + def write_segy(fh, seg_y_data, encoding=None, + trace_header_format=TraceHeaderRev1, endian='>', progress=None): """ @@ -27,6 +31,8 @@ def write_segy(fh, One such legitimate object would be a SegYReader instance. + trace_header_format: The class which defines the layout of the trace header. Defaults to TraceHeaderRev1. + encoding: Optional encoding for text data. Typically 'cp037' for EBCDIC or 'ascii' for ASCII. If omitted, the seg_y_data object will be queries for an encoding property. @@ -48,11 +54,8 @@ def write_segy(fh, write_binary_reel_header(fh, seg_y_data.binary_reel_header, endian) write_extended_textual_headers(fh, seg_y_data.extended_textual_header, encoding) - trace_header_format = compile_trace_header_format(endian) + trace_header_packer = HeaderPacker(trace_header_format, endian) for trace_index in seg_y_data.trace_indexes(): - write_trace_header(fh, seg_y_data.trace_header(trace_index), trace_header_format) + write_trace_header(fh, seg_y_data.trace_header(trace_index), trace_header_packer) write_trace_samples(fh, seg_y_data.trace_samples(trace_index), seg_y_data.data_sample_format, endian=endian) - - - diff --git a/test/strategies.py b/test/strategies.py index 1b66ba9..211ea79 100644 --- a/test/strategies.py +++ b/test/strategies.py @@ -1,5 +1,7 @@ +from itertools import accumulate, starmap from hypothesis import strategy from hypothesis.specifiers import integers_in_range +from segpy.util import batched PRINTABLE_ASCII_RANGE = (32, 127) @@ -16,6 +18,24 @@ def multiline_ascii_encodable_text(min_num_lines, max_num_lines): and characters which are encodable as printable 7-bit ASCII characters. """ - return strategy(integers_in_range(min_num_lines, max_num_lines)) \ + return strategy(integers_in_range(min_num_lines, max_num_lines)) \ .flatmap(lambda n: ([integers_in_range(*PRINTABLE_ASCII_RANGE)],) * n) \ .map(lambda xs: '\n'.join(bytes(x).decode('ascii') for x in xs)) + + +def spaced_ranges(min_num_ranges, max_num_ranges, min_interval, max_interval): + """A Hypothesis strategy to produce separated, non-overlapping ranges. + + Args: + min_num_ranges: The minimum number of ranges to produce. TODO: Correct? + max_num_ranges: The maximum number of ranges to produce. + min_interval: The minimum interval used for the lengths of the alternating ranges and spaces. + max_interval: The maximum interval used for the lengths of the alternating ranges and spaces. + """ + return strategy(integers_in_range(min_num_ranges, max_num_ranges)) \ + .map(lambda n: 2*n) \ + .flatmap(lambda n: (integers_in_range(min_interval, max_interval),) * n) \ + .map(list).map(lambda lst: list(accumulate(lst))) \ + .map(lambda lst: list(batched(lst, 2))) \ + .map(lambda pairs: list(starmap(range, pairs))) + diff --git a/test/test_util.py b/test/test_util.py index 47d19af..ad2e397 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,8 +1,9 @@ import unittest -from hypothesis import given, assume +from hypothesis import given, assume, example from hypothesis.specifiers import integers_in_range -from segpy.util import batched +from segpy.util import batched, complementary_intervals, flatten, intervals_are_contiguous, roundrobin +from test.strategies import spaced_ranges class TestBatched(unittest.TestCase): @@ -46,5 +47,34 @@ class TestBatched(unittest.TestCase): batches = list(batched([0, 0], 3, 42)) self.assertEqual(batches[-1], [0, 0, 42]) + +class TestComplementaryIntervals(unittest.TestCase): + + @given(spaced_ranges(min_num_ranges=1, max_num_ranges=10, + min_interval=0, max_interval=10)) + def test_contiguous(self, intervals): + complements = complementary_intervals(intervals) + interleaved = list(roundrobin(complements, intervals)) + self.assertTrue(intervals_are_contiguous(interleaved)) + + @given(spaced_ranges(min_num_ranges=1, max_num_ranges=10, + min_interval=0, max_interval=10), + integers_in_range(0, 10)) + def test_contiguous_with_offset_start(self, intervals, start_offset): + first_interval_start = intervals[0].start + start_index = first_interval_start - start_offset + complements = list(complementary_intervals(intervals, start=start_index)) + self.assertEqual(complements[0], range(start_index, first_interval_start)) + + @given(spaced_ranges(min_num_ranges=1, max_num_ranges=10, + min_interval=0, max_interval=10), + integers_in_range(0, 10)) + @example(intervals=[range(0, 0)], end_offset=1) + def test_contiguous_with_offset_end(self, intervals, end_offset): + last_interval_end = intervals[-1].stop + end_index = last_interval_end + end_offset + complements = list(complementary_intervals(intervals, stop=end_index)) + self.assertEqual(complements[-1], range(last_interval_end, end_index)) + if __name__ == '__main__': unittest.main() \ No newline at end of file