From b6fda775c296abb97ba3fb6717775e52ec3a21e8 Mon Sep 17 00:00:00 2001 From: Robert Smallshire Date: Fri, 27 Feb 2015 16:19:40 +0100 Subject: [PATCH] Progress on the infrastructure for header definitions and instances. --- segpy/binary_headers.py | 41 ++++ segpy/field.py | 305 +++++++++++++------------- segpy/trace_header.py | 462 ++++++++++++++++++++++------------------ segpy/util.py | 12 +- 4 files changed, 458 insertions(+), 362 deletions(-) create mode 100644 segpy/binary_headers.py diff --git a/segpy/binary_headers.py b/segpy/binary_headers.py new file mode 100644 index 0000000..28e8312 --- /dev/null +++ b/segpy/binary_headers.py @@ -0,0 +1,41 @@ +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/field.py b/segpy/field.py index 7b2be05..f0c2ab0 100644 --- a/segpy/field.py +++ b/segpy/field.py @@ -1,180 +1,161 @@ from weakref import WeakKeyDictionary -from segpy.docstring import docstring_property -from segpy.util import underscores_to_camelcase +from segpy.util import underscores_to_camelcase, first_sentence, lower_first, UNSET -# class FieldItems(object): -# -# def __init__(self, field): -# self._field = field -# self.__doc__ = field.__doc__ -# -# @property -# def name(self): -# if self._field._name is not None: -# return self._field._name -# raise AttributeError("Field is unnamed") -# -# @property -# def ftype(self): -# return self._field._field_type -# -# @property -# def value(self): -# return self._value if hasattr(self, '_value') else self._field._default -# -# @value.setter -# def value(self, value): -# self._value = self._field._field_type(value) -# -# @property -# def default(self): -# return self._field._default -# -# @property -# def offset(self): -# return self._start_offset if hasattr(self, '_start_offset') else self._field._offset -# -# @docstring_property -# def __doc__(self): -# return self._field._documentation +def field_mangler(descriptor): + """A factory function for creating a metaclass which detects descriptors which subclass a certain archetype. + + Args: + descriptor: The type (class) of a descriptor used to identity descriptor instances which should be renamed. + """ + + class NamedDescriptorMangler(type): + """A metaclass for assigning descriptor names. + """ + def __new__(mcs, class_name, bases, class_dict): + for name, attr in class_dict.items(): + + # This shenanigans is necessary so we can have all the following work is a useful way + # 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, descriptor): + if attr._name is None: + attr._name = name + + # We rename the *class* and set its docstring so help() works usefully + # when called with a class containing such fields. + attr_class = attr.__class__ + if issubclass(attr_class, descriptor) and attr_class is not descriptor: + attr_class.__name__ = underscores_to_camelcase(name) + attr_class.__doc__ = attr.assemble_docstring() + + return super(NamedDescriptorMangler, mcs).__new__(mcs, class_name, bases, class_dict) + + return NamedDescriptorMangler -# class Field(object): -# """A field of a binary format, specifying type, file offset, value and default. -# -# Attributes: -# name: The name of the field -# ftype: The type of the field. -# value: The value of the field. -# offset: The byte offset of the start of the field. -# default: The default value of the field. -# __doc__: The documentation of the field. -# """ -# -# def __init__(self, field_type, offset, default, documentation): -# self._name = None # This field can be set by the NamedDescriptorResolverMetaClass -# self._field_type = field_type -# self._offset = offset -# self._default = default -# self._data = WeakKeyDictionary() -# self._documentation = documentation -# -# def __get__(self, instance, owner): -# if instance is None: -# return self -# return self._data.setdefault(instance, FieldItems(self)) -# -# def __set__(self, instance, value): -# raise AttributeError("Can't set field attribute. Did you intend to set field.value instead?") -# -# def __delete__(self, instance): -# raise AttributeError("Can't delete field attribute. Did you intend to set field.value instead?") +class NamedField(object): + """Instances of NamedField can be detected by the NamedDescriptorResolver metaclass.""" + def __init__(self, docstrings=None): + # These fields can be set by the NamedDescriptorResolver metaclass + self._name = None + self._docstrings = docstrings -# def field(field_type, offset, default, documentation): -# -# class SpecificField(Field): -# pass -# -# SpecificField.__doc__ = documentation -# -# """Declare a field descriptor. -# -# Args: -# field_type: The type of the field. -# offset: The offset in bytes of this field in a binary stream. -# default: The default value of this field. -# documentation: Description of the field. -# -# Returns: -# A Field descriptor. -# """ -# -# return SpecificField(field_type, offset, default, documentation) + def assemble_docstring(self): + if self._name is None: + raise RuntimeError("Field name is not set.") + return first_sentence(self._docstrings.get(self._name, '')) +# ---------------------------------------------------------------------------------------------------------------------- -class MetaField(object): - - def __init__(self, field_type, offset, default, documentation): - self._field_type = field_type - self._offset = offset - self._default = default - self._documentation = documentation - - @property - def field_type(self): - return self._field_type - - @property - def offset(self): - return self._offset - - @property - def default(self): - return self._default - - @property - def documentation(self): - return self._documentation - - -def field(field_type, offset, default, documentation): - return MetaField(field_type, offset, default, documentation) - - -# class NamedDescriptorResolver(type): -# """A metaclass for assigning descriptor names. -# """ -# def __new__(cls, class_name, bases, class_dict): -# for name, attr in class_dict.items(): -# attr_class = attr.__class__ -# if issubclass(attr_class, Field) and attr_class is not Field: -# attr_class.__name__ = underscores_to_camelcase(name) -# return type.__new__(cls, class_name, bases, class_dict) - - -class FormatFieldItems(object): +class BinaryField(object): + """Aggregates the name, type and offset of a field within a binary format. + """ def __init__(self, field): self._field = field - self.__doc__ = field.__doc__ + + @property + def name(self): + """The field name (read-only).""" + if self._field._name is not None: + return self._field._name + raise AttributeError("Field is unnamed") @property def ftype(self): + """The type of the field described using the classes in segpy.types""" return self._field._field_type @property def offset(self): + """The one-based (IMPORTANT!) offset in bytes of the field from the beginning of the structure""" return self._field._offset - @docstring_property - def __doc__(self): - return self._field._documentation - -class FormatField(object): +class BinaryFieldDescriptor(NamedField): """A field of a binary format, specifying type, file offset, value and default. Attributes: - name: The name of the field ftype: The type of the field. offset: The byte offset of the start of the field. - __doc__: The documentation of the field. """ - def __init__(self, field_type, offset, documentation): - self._name = None # This field can be set by the NamedDescriptorResolver - self._field_type = field_type + def __init__(self, ftype, offset, docstrings): + super(BinaryFieldDescriptor, self).__init__(docstrings) + self._ftype = ftype self._offset = offset self._data = WeakKeyDictionary() - self._documentation = documentation def __get__(self, instance, owner): if instance is None: return self - return self._data.setdefault(instance, FormatFieldItems(self)) + return self._data.setdefault(instance, BinaryField(self)) + + def __set__(self, instance, value): + raise AttributeError("Can't set binary field attribute. Did you intend to set field.ftype or field.offset instead?") + + def __delete__(self, instance): + raise AttributeError("Can't delete field attribute. Did you intend to set field.ftype or field.offset instead?") + + def assemble_docstring(self): + if self._name is None: + raise RuntimeError("Field name is not set.") + return "A BinaryField containing the type and byte offset for the {}".format( + first_sentence(lower_first(self._docstrings.get(self._name, '')))) + + +#----------------------------------------------------------------------------------------------------------------------- + + +class ValueField(object): + """Aggregates the name, type and offset of a field within a binary format. + """ + + def __init__(self, field): + self._field = field + self._value = UNSET + + @property + def name(self): + """The field name (read-only).""" + if self._field._name is not None: + return self._field._name + raise AttributeError("Field is unnamed") + + @property + def default(self): + return self._field._default + + @property + def value(self): + """The type of the field described using the classes in segpy.types""" + return self._value if self._value is not UNSET else self.default + + @value.setter + def value(self, v): + self._value = v + + +class ValueFieldDescriptor(NamedField): + """A field of a binary format, specifying type, file offset, value and default. + + Attributes: + value: The value of the field. + """ + + def __init__(self, default, docstrings): + super(ValueFieldDescriptor, self).__init__(docstrings) + self._default = default + self._data = WeakKeyDictionary() + + def __get__(self, instance, owner): + if instance is None: + return self + return self._data.setdefault(instance, ValueField(self)) def __set__(self, instance, value): raise AttributeError("Can't set field attribute. Did you intend to set field.value instead?") @@ -182,21 +163,35 @@ class FormatField(object): def __delete__(self, instance): raise AttributeError("Can't delete field attribute. Did you intend to set field.value instead?") + def assemble_docstring(self): + if self._name is None: + raise RuntimeError("Field name is not set.") + return "A ValueField containing the value and default for the {}".format( + first_sentence(lower_first(self._docstrings.get(self._name, '')))) + +# ---------------------------------------------------------------------------------------------------------------------- -def binary_format(header_definition): +def field(descriptor, docstrings, **kwargs): + """ + Args: + descriptor: The type (class) of the descriptor be be created. - class BinaryFormat(type): - """A metaclass for specifying the field type.""" + docstrings: A dictionary of strings where the docstring for the descriptor can be looked up using the + descriptor name. For this to work correctly, the class hosting the descriptors should use a metaclass + created with field_manger(descriptor). e.g. class Foo(metaclass=field_mangler(descriptor)) - def __new__(cls, class_name, bases, class_dict): - for name in class_dict: - print("Found field : {}".format(name)) - attr = class_dict[name] - if isinstance(attr, MetaField): - print("Found meta field : {}".format(attr)) - meta_field = class_dict[name] - class_dict[name] = FormatField(field_type=meta_field.ftype, - offset=meta_field.offset, - documentation=meta_field.doc) - return type.__new__(cls, class_name, bases, class_dict) + **kwargs: Any other arguments will be forwarded to the descriptor constructor. + + Returns: + An instance of the descriptor class. + """ + + # Create a class specifically for this field. This class will later get + # renamed when the NamedDescriptorMangler metaclass does its job, to + # a class name based on the field name. + + class SpecificField(descriptor): + pass + + return SpecificField(docstrings=docstrings, **kwargs) \ No newline at end of file diff --git a/segpy/trace_header.py b/segpy/trace_header.py index 69ce9e7..db0ecc1 100644 --- a/segpy/trace_header.py +++ b/segpy/trace_header.py @@ -1,226 +1,276 @@ -from collections import OrderedDict -from segpy.field import field, BinaryFormat, binary_format -from segpy.types import Int16, Int32 -from segpy.util import pairwise +from segpy.binary_headers import BinaryFormat, Header +from segpy.field import field, BinaryFieldDescriptor, ValueFieldDescriptor +from segpy.types import Int32 -class TraceHeader(object, - #metaclass=NamedDescriptorResolver - ): +# Field descriptions use by both the TraceHeaderFormat and TraceHeader classes, which extract their documentation +# from this dictionary. The first sentence of each docstring (up to the first stop) must be usable as a brief +# description and make sense in the sentence "This field is the " when the first character is lower-cased. +TRACE_HEADER_DOCS = { + 'line_sequence_num': "Trace sequence number within line. Numbers continue to increase if the same line continues " + "across multiple SEG Y files. Highly recommended for all types of data.", - line_sequence_num = field( - Int32, offset=1, default=0, documentation= - "Trace sequence number within line — Numbers continue to increase if the same line " - "continues across multiple SEG Y files. Highly recommended for all types of data.") + 'file_sequence_num': "Trace sequence number within SEG Y file. Each file starts with trace sequence one.", - file_sequence_num = field( - Int32, offset=5, default=0, documentation= - "Trace sequence number within SEG Y file — Each file starts with trace sequence one.") + 'field_record_num': "Original field record number. Highly recommended for all types of data.", - field_record_num = field( - Int32, offset=9, default=0, documentation= - "Original field record number. Highly recommended for all types of data.") - - trace_num = field( - Int32, offset=13, default=0, documentation= - "Trace number within the original field record. Highly recommended for all types of data.") - - energy_source_point_num = field( - Int32, offset=17, default=0, documentation= - "Energy source point number — Used when more than one record occurs at the same " - "effective surface location. It is recommended that the new entry defined in Trace " - "Header bytes 197-202 be used for shotpoint number.") - - ensemble_num = field( - Int32, offset=21, default=0, documentation= - "Ensemble number (i.e. CDP , CMP , CRP , etc)") - - ensemble_trace_num = field( - Int32, offset=25, default=0, documentation= - "Trace number within the ensemble — Each ensemble starts with trace number one.") - - trace_identification_code = field( - Int16, offset=29, default=0, documentation= - "Trace identification code") - - num_vertically_summed_traces = field( - Int16, offset=31, default=1, documentation= - "Number of vertically summed traces yielding this trace. (1 is one trace, 2 is two summed traces, etc.)") - - num_horizontally_stacked_traces = field( - Int16, offset=33, default=1, documentation= - "Number of horizontally stacked traces yielding this trace. (1 is one trace, 2 is two stacked traces, etc.)") - - data_use = field( - Int16, offset=35, default=1, documentation= - "Data use: 1 = Production, 2 = Test") - - source_receiver_offset = field( - Int32, offset=37, default=0, documentation= - "Distance from center of the source point to the center of the receiver group (negative if opposite to " - "direction in which line is shot).") - - receiver_group_elevation = field( - Int32, offset=41, default=0, documentation= - "Receiver group elevation (all elevations above the Vertical datum are positive and below are negative). The " - "elevation_scalar applies to this value.") - - surface_elevation_at_source = field( - Int32, offset=45, default=0, documentation= - "Surface elevation at source. The elevation_scalar applies to this value.") - - source_depth_below_surface = field( - Int32, offset=49, default=0, documentation= - "Source depth below surface (a positive number). The elevation_scalar applies to this value.") - - datum_elevation_at_receiver_group = field( - Int32, offset=53, default=0, documentation= - "Source depth below surface (a positive number). The elevation_scalar applies to this value.") - - datum_elevation_at_source = field( - Int32, offset=57, default=0, documentation= - "Datum elevation at source. The elevation_scalar applies to this value.") - - water_depth_at_source = field( - Int32, offset=61, default=0, documentation= - "Water depth at source. The elevation_scalar applies to this value.") - - water_depth_at_group = field( - Int32, offset=65, default=0, documentation= - "Water depth at group. The elevation_scalar applies to this value." - ) - - elevation_scalar = field( - Int16, offset=69, default=1, documentation= - "Scalar to be applied to the elevations and depths specified in: receiver_group_elevation, " - "surface_elevation_at_source, source_depth_below_surface, datum_elevation_at_receiver_group, " - "datum_elevation_at_source, water_depth_at_source and water_depth_at_group, to give the real value. " - "Scalar = 1, +10, +100, +1000, or +10,000. If positive, scalar is used as a multiplier; if negative, scalar is " - "used as a divisor." - ) - - xy_scalar = field( - Int16, offset=71, default=1, documentation= - "Scalar to be applied to all coordinates specified in source_x, source_y, group_x, group_y, cdp_x and cdp_y to " - "give the real value. Scalar = 1, +10, +100, +1000, or +10,000. If positive, scalar is used as a multiplier; " - "if negative, scalar is used as divisor." - ) - - source_x = field( - Int32, offset=73, default=0, documentation= - "Source coordinate - X. The xy_scalar applies to this value. The coordinate reference system should be " - "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " - "decimal degrees or DMS, the X values represent longitude. A positive value designates east of Greenwich " - "Meridian and a negative value designates west." - ) - - source_y = field( - Int32, offset=77, default=0, documentation= - "Source coordinate - Y. The xy_scalar applies to this value. The coordinate reference system should be " - "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " - "decimal degrees or DMS, the Y values represent latitude. A positive value designates north of the equator and " - "a negative value designates south." - ) - - group_x = field( - Int32, offset=73, default=0, documentation= - "Group coordinate - X. The xy_scalar applies to this value. The coordinate reference system should be " - "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " - "decimal degrees or DMS, the X values represent longitude. A positive value designates east of Greenwich " - "Meridian and a negative value designates west." - ) - - group_y = field( - Int32, offset=77, default=0, documentation= - "Source coordinate - Y. The xy_scalar applies to this value. The coordinate reference system should be " - "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " - "decimal degrees or DMS, the Y values represent latitude. A positive value designates north of the equator and " - "a negative value designates south." - ) - - coordinate_units = field( - Int16, offset=89, default=0, documentation= - "Coordinate units: 1 = Length (meters or feet), 2 = Seconds of arc, 3 = Decimal degrees, 4 = Degrees, minutes, " - "seconds (DMS). Note: To encode ±DDDMMSS bytes this value equals ±DDD*104 + MM*102 + SS with xy_scalar set to " - "1; To encode ±DDDMMSS.ss this value equals ±DDD*106 + MM*104 + SS*102 with xy_scalar set to -100." - ) - - weathering_velocity = field( - Int16, offset=91, default=0, documentation= - "Weathering velocity. (ft/s or m/s as specified in Binary File Header bytes 3255- 3256)" # TODO - ) - - subweathering_velocity = field( - Int16, offset=93, default=0, documentation= - "Subweathering velocity. (ft/s or m/s as specified in Binary File Header bytes 3255-3256)" # TODO - ) - - uphole_time_at_source = field( - Int16, offset=95, default=0, documentation= - "Uphole time at source in milliseconds. The time_scalar applies to this value." - ) - - uphole_time_at_group = field( - Int16, offset=97, default=0, documentation= - "Uphole time at group in milliseconds. The time_scalar applies to this value." - ) - - source_static_correction = field( - Int16, offset=99, default=0, documentation= - "Source static correction in milliseconds. The time_scalar applies to this value." - ) - - group_static_correction = field( - Int16, offset=101, default=0, documentation= - "Group static correction in milliseconds. The time_scalar applies to this value." - ) + 'trace_num': "Trace number within the original field record. Highly recommended for all types of data." +} -class TraceHeaderFormat(metaclass=binary_format(traceheader)): - - pass +def trace_header_format_field(ftype, offset): + return field(descriptor=BinaryFieldDescriptor, docstrings=TRACE_HEADER_DOCS, ftype=ftype, offset=offset) +class TraceHeaderFormat(BinaryFormat): -def size_of(t): - return t.SIZE + line_sequence_num = trace_header_format_field(Int32, offset=1) + file_sequence_num = trace_header_format_field(Int32, offset=5) + field_record_num = trace_header_format_field(Int32, offset=9) + trace_num = trace_header_format_field(Int32, offset=13) -def compile_struct(record, num_bytes): - """Compile a struct description from a record. +def trace_header_field(default): + return field(descriptor=ValueFieldDescriptor, docstrings=TRACE_HEADER_DOCS, default=default) - Args: - record: A record containing Field descriptors from which ftype and offset attributes can be obtained. - num_bytes: The total number of bytes to be retrieved. - """ - descriptors = (descriptor for descriptor in record.__class__.__dict__.values() if isinstance(descriptor, Field)) +class TraceHeader(Header): - # Sort those descriptors where offset is not None - sorted_descriptors = sorted(descriptor for descriptor in descriptors if descriptor.offset is not None) + line_sequence_num = trace_header_field(default=0) + file_sequence_num = trace_header_field(default=0) + field_record_num = trace_header_field(default=0) + trace_num = trace_header_field(default=0) - # Check for overlaps - for a, b in pairwise(sorted_descriptors): - if a.offset + size_of(a.ftype) > b.offset: - raise ValueError("Record fields {!r} at offset {} and {!r} at offset {} are distinct but overlap." - .format(a.name, a.offset, b.name, b.offset)) - # Invert the mapping to give offset -> [fields] - # Check that the types are the same - offset_to_field = OrderedDict() - for descriptor in descriptors: - if descriptor.offset not in offset_to_field: - offset_to_field[descriptor.offset] = [] - if len(offset_to_field) > 0: - if offset_to_field[0].ftype is not descriptor.ftype: - raise ValueError("Coincident fields {!r} and {!r} at offset {} have different types {!r} and {!r}" - .format(offset_to_field[0], descriptor, descriptor.offset, offset_to_field[0].ftype, - descriptor.ftype )) - offset_to_field[descriptor.offset].append(descriptor) +# class TraceHeader(object): +# +# def __init__(self, trace_header_format=None): +# """Initialize a TraceHeader. +# +# Args: +# trace_header_format: An optional TraceHeaderFormat instance against which +# field values will be validated. If not provided, fields will not be validated. +# """ +# +# line_sequence_num = field(default=0, documentation= +# "Trace sequence number within line — Numbers continue to increase if the same line " +# "continues across multiple SEG Y files. Highly recommended for all types of data.") - range_to_field = OrderedDict() - for fields in offset_to_field.values(): - field = fields[0] - range_to_field[range(field.offset, field.offset+size_of(field.ftype))] = fields +# +# +# +# class TraceHeader(object, +# #metaclass=NamedDescriptorResolver +# ): +# +# line_sequence_num = field( +# Int32, offset=1, default=0, documentation= +# "Trace sequence number within line — Numbers continue to increase if the same line " +# "continues across multiple SEG Y files. Highly recommended for all types of data.") +# +# file_sequence_num = field( +# Int32, offset=5, default=0, documentation= +# "Trace sequence number within SEG Y file — Each file starts with trace sequence one.") +# +# field_record_num = field( +# Int32, offset=9, default=0, documentation= +# "Original field record number. Highly recommended for all types of data.") +# +# trace_num = field( +# Int32, offset=13, default=0, documentation= +# "Trace number within the original field record. Highly recommended for all types of data.") +# +# energy_source_point_num = field( +# Int32, offset=17, default=0, documentation= +# "Energy source point number — Used when more than one record occurs at the same " +# "effective surface location. It is recommended that the new entry defined in Trace " +# "Header bytes 197-202 be used for shotpoint number.") +# +# ensemble_num = field( +# Int32, offset=21, default=0, documentation= +# "Ensemble number (i.e. CDP , CMP , CRP , etc)") +# +# ensemble_trace_num = field( +# Int32, offset=25, default=0, documentation= +# "Trace number within the ensemble — Each ensemble starts with trace number one.") +# +# trace_identification_code = field( +# Int16, offset=29, default=0, documentation= +# "Trace identification code") +# +# num_vertically_summed_traces = field( +# Int16, offset=31, default=1, documentation= +# "Number of vertically summed traces yielding this trace. (1 is one trace, 2 is two summed traces, etc.)") +# +# num_horizontally_stacked_traces = field( +# Int16, offset=33, default=1, documentation= +# "Number of horizontally stacked traces yielding this trace. (1 is one trace, 2 is two stacked traces, etc.)") +# +# data_use = field( +# Int16, offset=35, default=1, documentation= +# "Data use: 1 = Production, 2 = Test") +# +# source_receiver_offset = field( +# Int32, offset=37, default=0, documentation= +# "Distance from center of the source point to the center of the receiver group (negative if opposite to " +# "direction in which line is shot).") +# +# receiver_group_elevation = field( +# Int32, offset=41, default=0, documentation= +# "Receiver group elevation (all elevations above the Vertical datum are positive and below are negative). The " +# "elevation_scalar applies to this value.") +# +# surface_elevation_at_source = field( +# Int32, offset=45, default=0, documentation= +# "Surface elevation at source. The elevation_scalar applies to this value.") +# +# source_depth_below_surface = field( +# Int32, offset=49, default=0, documentation= +# "Source depth below surface (a positive number). The elevation_scalar applies to this value.") +# +# datum_elevation_at_receiver_group = field( +# Int32, offset=53, default=0, documentation= +# "Source depth below surface (a positive number). The elevation_scalar applies to this value.") +# +# datum_elevation_at_source = field( +# Int32, offset=57, default=0, documentation= +# "Datum elevation at source. The elevation_scalar applies to this value.") +# +# water_depth_at_source = field( +# Int32, offset=61, default=0, documentation= +# "Water depth at source. The elevation_scalar applies to this value.") +# +# water_depth_at_group = field( +# Int32, offset=65, default=0, documentation= +# "Water depth at group. The elevation_scalar applies to this value." +# ) +# +# elevation_scalar = field( +# Int16, offset=69, default=1, documentation= +# "Scalar to be applied to the elevations and depths specified in: receiver_group_elevation, " +# "surface_elevation_at_source, source_depth_below_surface, datum_elevation_at_receiver_group, " +# "datum_elevation_at_source, water_depth_at_source and water_depth_at_group, to give the real value. " +# "Scalar = 1, +10, +100, +1000, or +10,000. If positive, scalar is used as a multiplier; if negative, scalar is " +# "used as a divisor." +# ) +# +# xy_scalar = field( +# Int16, offset=71, default=1, documentation= +# "Scalar to be applied to all coordinates specified in source_x, source_y, group_x, group_y, cdp_x and cdp_y to " +# "give the real value. Scalar = 1, +10, +100, +1000, or +10,000. If positive, scalar is used as a multiplier; " +# "if negative, scalar is used as divisor." +# ) +# +# source_x = field( +# Int32, offset=73, default=0, documentation= +# "Source coordinate - X. The xy_scalar applies to this value. The coordinate reference system should be " +# "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " +# "decimal degrees or DMS, the X values represent longitude. A positive value designates east of Greenwich " +# "Meridian and a negative value designates west." +# ) +# +# source_y = field( +# Int32, offset=77, default=0, documentation= +# "Source coordinate - Y. The xy_scalar applies to this value. The coordinate reference system should be " +# "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " +# "decimal degrees or DMS, the Y values represent latitude. A positive value designates north of the equator and " +# "a negative value designates south." +# ) +# +# group_x = field( +# Int32, offset=73, default=0, documentation= +# "Group coordinate - X. The xy_scalar applies to this value. The coordinate reference system should be " +# "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " +# "decimal degrees or DMS, the X values represent longitude. A positive value designates east of Greenwich " +# "Meridian and a negative value designates west." +# ) +# +# group_y = field( +# Int32, offset=77, default=0, documentation= +# "Source coordinate - Y. The xy_scalar applies to this value. The coordinate reference system should be " +# "identified through an extended header Location Data stanza. If the coordinate units are in seconds of arc, " +# "decimal degrees or DMS, the Y values represent latitude. A positive value designates north of the equator and " +# "a negative value designates south." +# ) +# +# coordinate_units = field( +# Int16, offset=89, default=0, documentation= +# "Coordinate units: 1 = Length (meters or feet), 2 = Seconds of arc, 3 = Decimal degrees, 4 = Degrees, minutes, " +# "seconds (DMS). Note: To encode ±DDDMMSS bytes this value equals ±DDD*104 + MM*102 + SS with xy_scalar set to " +# "1; To encode ±DDDMMSS.ss this value equals ±DDD*106 + MM*104 + SS*102 with xy_scalar set to -100." +# ) +# +# weathering_velocity = field( +# Int16, offset=91, default=0, documentation= +# "Weathering velocity. (ft/s or m/s as specified in Binary File Header bytes 3255- 3256)" # TODO +# ) +# +# subweathering_velocity = field( +# Int16, offset=93, default=0, documentation= +# "Subweathering velocity. (ft/s or m/s as specified in Binary File Header bytes 3255-3256)" # TODO +# ) +# +# uphole_time_at_source = field( +# Int16, offset=95, default=0, documentation= +# "Uphole time at source in milliseconds. The time_scalar applies to this value." +# ) +# +# uphole_time_at_group = field( +# Int16, offset=97, default=0, documentation= +# "Uphole time at group in milliseconds. The time_scalar applies to this value." +# ) +# +# source_static_correction = field( +# Int16, offset=99, default=0, documentation= +# "Source static correction in milliseconds. The time_scalar applies to this value." +# ) +# +# group_static_correction = field( +# Int16, offset=101, default=0, documentation= +# "Group static correction in milliseconds. The time_scalar applies to this value." +# ) +# +# +# +# def size_of(t): +# return t.SIZE +# +# +# def compile_struct(record, num_bytes): +# """Compile a struct description from a record. +# +# Args: +# record: A record containing Field descriptors from which ftype and offset attributes can be obtained. +# +# num_bytes: The total number of bytes to be retrieved. +# """ +# descriptors = (descriptor for descriptor in record.__class__.__dict__.values() if isinstance(descriptor, Field)) +# +# # Sort those descriptors where offset is not None +# sorted_descriptors = sorted(descriptor for descriptor in descriptors if descriptor.offset is not None) +# +# # Check for overlaps +# for a, b in pairwise(sorted_descriptors): +# if a.offset + size_of(a.ftype) > b.offset: +# raise ValueError("Record fields {!r} at offset {} and {!r} at offset {} are distinct but overlap." +# .format(a.name, a.offset, b.name, b.offset)) +# +# # Invert the mapping to give offset -> [fields] +# # Check that the types are the same +# offset_to_field = OrderedDict() +# for descriptor in descriptors: +# if descriptor.offset not in offset_to_field: +# offset_to_field[descriptor.offset] = [] +# if len(offset_to_field) > 0: +# if offset_to_field[0].ftype is not descriptor.ftype: +# raise ValueError("Coincident fields {!r} and {!r} at offset {} have different types {!r} and {!r}" +# .format(offset_to_field[0], descriptor, descriptor.offset, offset_to_field[0].ftype, +# descriptor.ftype )) +# offset_to_field[descriptor.offset].append(descriptor) +# +# range_to_field = OrderedDict() +# for fields in offset_to_field.values(): +# field = fields[0] +# range_to_field[range(field.offset, field.offset+size_of(field.ftype))] = fields diff --git a/segpy/util.py b/segpy/util.py index 0b16647..20d00e2 100644 --- a/segpy/util.py +++ b/segpy/util.py @@ -230,4 +230,14 @@ def round_up(integer, multiple): def underscores_to_camelcase(s): """Convert text_in_this_style to TextInThisStyle.""" - return ''.join(w.capitalize() for w in s.split('_')) \ No newline at end of file + return ''.join(w.capitalize() for w in s.split('_')) + + +def first_sentence(s): + sentence, stop, _ = s.partition('.') + return sentence + stop + + +def lower_first(s): + """Lower case the first character of a string.""" + return s[:1].lower() + s[1:] \ No newline at end of file