mirror of
https://github.com/wassname/segpy.git
synced 2026-07-08 04:50:40 +08:00
Use the HeaderPacker from toolkit to read and write the trace header.
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user