Use the HeaderPacker from toolkit to read and write the trace header.

This commit is contained in:
Robert Smallshire
2015-05-05 13:02:07 +02:00
parent 1311d2f84f
commit e8cdaaf198
10 changed files with 291 additions and 207 deletions
-41
View File
@@ -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
+76 -79
View File
@@ -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)
+65 -28
View File
@@ -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__)
self._header_format_class.__name__)
def main():
from segpy.trace_header import TraceHeaderRev0
compile_struct(TraceHeaderRev0, 240)
pass
if __name__ == '__main__':
main()
+23 -14
View File
@@ -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()
+12 -11
View File
@@ -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)
+5 -14
View File
@@ -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
+47 -10
View File
@@ -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('__')
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)
+10 -7
View File
@@ -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)
+21 -1
View File
@@ -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)))
+32 -2
View File
@@ -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()