├── .gitignore ├── LICENSE ├── MANIFEST ├── MANIFEST.in ├── README.rst ├── documentation ├── conf.py └── volume.rst ├── examples ├── basic_horizon_usage.py ├── basic_volume_usage.py └── data │ ├── Colormaps │ └── blue-orange-tweaked.colormap │ ├── Horizons │ ├── channels.hzn │ └── seafloor.hzn │ ├── Volumes │ └── example.vol │ └── swFaults │ ├── empty.swf │ ├── example_normal.swf │ └── example_ss.swf ├── geoprobe ├── _2dHeader.py ├── __init__.py ├── _volHeader.py ├── colormap.py ├── common.py ├── data2d.py ├── ezfault.py ├── horizon.py ├── swfault.py ├── tsurf.py ├── utilities.py └── volume.py ├── setup.py └── tests ├── test_swfault.py ├── test_utilities.py └── test_volume.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *~ 4 | *.egg-info 5 | .pytest_cache 6 | dist/ 7 | build/ 8 | documentation/.build/ 9 | geoprobe/sync.sh 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2010 Free Software Foundation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | LICENSE 2 | README 3 | setup.py 4 | geoprobe/_2dHeader.py 5 | geoprobe/__init__.py 6 | geoprobe/_volHeader.py 7 | geoprobe/common.py 8 | geoprobe/data2d.py 9 | geoprobe/ezfault.py 10 | geoprobe/horizon.py 11 | geoprobe/utilities.py 12 | geoprobe/volume.py 13 | geoprobe/swfault.py 14 | geoprobe/colormap.py 15 | geoprobe/tsurf.py 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README 2 | include LICENSE 3 | 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-geoprobe 2 | =============== 3 | python-geoprobe is a python module to read and write geoprobe horizons, 4 | volumes, and faults. 5 | 6 | Notes 7 | ----- 8 | 9 | This implementation is based on reverse-engineering the file formats, and as 10 | such, is certainly not complete. However, things seem to work. 11 | 12 | Examples 13 | -------- 14 | 15 | ``python-geoprobe`` supports basic reading and writing of geoprobe-formatted 16 | volumes, horizons, fault sticks, 2D seismic, colormaps, and TSurfs. 17 | 18 | As an example of reading and display a slice from a volume:: 19 | 20 | import matplotlib.pyplot as plt 21 | import geoprobe 22 | 23 | vol = geoprobe.volume('examples/data/Volumes/example.vol') 24 | 25 | # By default, this will not load the data into RAM. Instead, 26 | # ``vol.data`` will be a memmapped numpy array 27 | print vol.data 28 | 29 | # Now let's load everythign into RAM. Note that ``vol.load()`` returns the 30 | # array, but ``vol.data`` will be the same in-memory array after this, as well 31 | vol.load() 32 | 33 | # Indexing the volume works in "model" (inline/crossline/z) coordinates. 34 | # We could also use ``vol.XSlice(2300)``, but indexing gives more flexibility. 35 | data = vol[2300, :, :].T 36 | 37 | # Display stretched. Note that for no vertical exaggeration, we'd use 38 | # ``aspect=1/vol.dyW`` 39 | fig, ax = plt.subplots() 40 | ax.imshow(data, cmap='gray_r', aspect='auto', 41 | extent=[vol.ymin, vol.ymax, vol.zmax, vol.zmin]) 42 | ax.set(title='Inline 2300', xlabel='Crossline', ylabel='Depth (m)') 43 | 44 | plt.show() 45 | 46 | 47 | .. image:: http://joferkington.github.io/python-geoprobe/images/vol_example.png 48 | :alt: An inline from the 3D seismic volume. 49 | :align: center 50 | 51 | We can also read/write geoprobe-formatted binary horizons (ascii horizons 52 | currently not supported):: 53 | 54 | import matplotlib.pyplot as plt 55 | import geoprobe 56 | 57 | hor = geoprobe.horizon('examples/data/Horizons/channels.hzn') 58 | 59 | # Some basic information about the horizon, to show useful attributes 60 | print ('The horizon has a total of %i points, %i of which are' 61 | ' auto-tracked' % (hor.data.size, hor.surface.size)) 62 | print 'The horizon has %i manually picked lines' % len(hor.lines) 63 | print 'The inline coordinates range from', hor.xmin, 'to', hor.xmax 64 | print 'The crossline coordinates range from', hor.ymin, 'to', hor.ymax 65 | print 'The depth/time coordinates range from', hor.zmin, 'to', hor.zmax 66 | 67 | # Display the horizon: 68 | fig, ax = plt.subplots() 69 | 70 | # hor.grid is a 2D array of the Z-values stored in the horizon 71 | im = ax.imshow(hor.grid, cmap='gist_earth_r', origin='lower', 72 | extent=(hor.xmin, hor.xmax, hor.ymin, hor.ymax)) 73 | 74 | # Plot the manual picks 75 | # Here, "line" is a numpy structured array with fields 'x', 'y', 'z', etc. 76 | # "line_info" is a 4-tuple of (xdir, ydir, zdir, ID) (and is unused here) 77 | for line_info, line in hor.lines: 78 | ax.plot(line['x'], line['y'], color='gray') 79 | 80 | # Other information... 81 | cb = fig.colorbar(im, orientation='horizontal') 82 | cb.set_label('Depth in meters below sea level') 83 | ax.set(title='An example horizon file', xlabel='Inline', ylabel='Crossline') 84 | ax.axis('tight') 85 | 86 | plt.show() 87 | 88 | .. image:: http://joferkington.github.io/python-geoprobe/images/hor_example.png 89 | :alt: A 3D horizon and manual picks. 90 | :align: center 91 | 92 | Author 93 | ------ 94 | 95 | Joe Kington 96 | 97 | Dependencies 98 | ------------ 99 | 100 | Requires python >= 2.5 and numpy. Some functions (e.g. horizon.toGeotiff) 101 | require gdal and its python bindings. The plotting functions in utilities (e.g. 102 | utilities.wiggles) and some swfault functionality requires matplotlib. 103 | 104 | Installation 105 | ------------ 106 | 107 | Installation should be as simple as "python setup.py install" 108 | -------------------------------------------------------------------------------- /documentation/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-geoprobe documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Oct 21 21:54:15 2009. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # The contents of this file are pickled, so don't put values in the namespace 9 | # that aren't pickleable (module imports are okay, they're removed automatically). 10 | # 11 | # Note that not all possible configuration values are present in this 12 | # autogenerated file. 13 | # 14 | # All configuration values have a default; values that are commented out 15 | # serve to show the default. 16 | 17 | import sys, os 18 | 19 | # If your extensions are in another directory, add it here. If the directory 20 | # is relative to the documentation root, use os.path.abspath to make it 21 | # absolute, like shown here. 22 | #sys.path.append(os.path.abspath('.')) 23 | 24 | # General configuration 25 | # --------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['.templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'python-geoprobe' 45 | copyright = u'2009, Joe Kington' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.1' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.1' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of documents that shouldn't be included in the build. 67 | #unused_docs = [] 68 | 69 | # List of directories, relative to source directory, that shouldn't be searched 70 | # for source files. 71 | exclude_trees = ['.build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | 91 | # Options for HTML output 92 | # ----------------------- 93 | 94 | # The style sheet to use for HTML and HTML Help pages. A file of that name 95 | # must exist either in Sphinx' static/ path, or in one of the custom paths 96 | # given in html_static_path. 97 | #html_style = 'custom.css' 98 | 99 | # The name for this set of Sphinx documents. If None, it defaults to 100 | # " v documentation". 101 | #html_title = None 102 | 103 | # A shorter title for the navigation bar. Default is the same as html_title. 104 | #html_short_title = None 105 | 106 | # The name of an image file (relative to this directory) to place at the top 107 | # of the sidebar. 108 | #html_logo = None 109 | 110 | # The name of an image file (within the static path) to use as favicon of the 111 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 112 | # pixels large. 113 | #html_favicon = None 114 | 115 | # Add any paths that contain custom static files (such as style sheets) here, 116 | # relative to this directory. They are copied after the builtin static files, 117 | # so a file named "default.css" will overwrite the builtin "default.css". 118 | #html_static_path = ['.static'] 119 | 120 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 121 | # using the given strftime format. 122 | #html_last_updated_fmt = '%b %d, %Y' 123 | 124 | # If true, SmartyPants will be used to convert quotes and dashes to 125 | # typographically correct entities. 126 | #html_use_smartypants = True 127 | 128 | # Custom sidebar templates, maps document names to template names. 129 | #html_sidebars = {} 130 | 131 | # Additional templates that should be rendered to pages, maps page names to 132 | # template names. 133 | #html_additional_pages = {} 134 | 135 | # If false, no module index is generated. 136 | #html_use_modindex = True 137 | 138 | # If false, no index is generated. 139 | #html_use_index = True 140 | 141 | # If true, the index is split into individual pages for each letter. 142 | #html_split_index = False 143 | 144 | # If true, the reST sources are included in the HTML build as _sources/. 145 | #html_copy_source = True 146 | 147 | # If true, an OpenSearch description file will be output, and all pages will 148 | # contain a tag referring to it. The value of this option must be the 149 | # base URL from which the finished HTML is served. 150 | #html_use_opensearch = '' 151 | 152 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 153 | html_file_suffix = '.html' 154 | 155 | # Output file base name for HTML help builder. 156 | htmlhelp_basename = 'python-geoprobedoc' 157 | 158 | 159 | # Options for LaTeX output 160 | # ------------------------ 161 | 162 | # The paper size ('letter' or 'a4'). 163 | #latex_paper_size = 'letter' 164 | 165 | # The font size ('10pt', '11pt' or '12pt'). 166 | #latex_font_size = '10pt' 167 | 168 | # Grouping the document tree into LaTeX files. List of tuples 169 | # (source start file, target name, title, author, document class [howto/manual]). 170 | latex_documents = [ 171 | ('index', 'python-geoprobe.tex', ur'python-geoprobe Documentation', 172 | ur'Joe Kington', 'manual'), 173 | ] 174 | 175 | # The name of an image file (relative to this directory) to place at the top of 176 | # the title page. 177 | #latex_logo = None 178 | 179 | # For "manual" documents, if this is true, then toplevel headings are parts, 180 | # not chapters. 181 | #latex_use_parts = False 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #latex_preamble = '' 185 | 186 | # Documents to append as an appendix to all manuals. 187 | #latex_appendices = [] 188 | 189 | # If false, no module index is generated. 190 | #latex_use_modindex = True 191 | -------------------------------------------------------------------------------- /documentation/volume.rst: -------------------------------------------------------------------------------- 1 | Welcome to my project's documentation! 2 | ====================================== 3 | 4 | :mod:`volume` -- Reference 5 | ============================================= 6 | Geoprobe volume files contain a 3D array of values discretized and stored as 7 | unsigned 8-bit integers. Reading from and writing to Geoprobe volumes is supported 8 | through the volume class. 9 | 10 | .. autoclass:: geoprobe.volume 11 | .. automethod:: geoprobe.volume.__init__ 12 | 13 | The raw uint8 array values can be accessed through the volume.data attribute. This 14 | is a 3D numpy array (or memory-mapped-file array) with shape (nx,ny,nz). The 15 | attributes nx, ny, and nz are readonly. To change them, reshape or otherwise 16 | operate on volume.data. 17 | 18 | .. autoattribute:: geoprobe.volume.nx 19 | .. autoattribute:: geoprobe.volume.ny 20 | .. autoattribute:: geoprobe.volume.nz 21 | .. autoattribute:: geoprobe.volume.data 22 | 23 | If the volume object is created from an existing volume file, volume.data will be 24 | a memory-mapped-file array. Otherwise, this will contain whatever array you set 25 | when creating the object (or assign to volume.data afterwards). When setting this 26 | attribute, the new array will be recast as a uint8 array. (i.e. the values will 27 | always be between 0-255, and overflow/wrap-around will occur if the new array has 28 | values outside of this range) 29 | 30 | To load volume.data from a memory-mapped file entirely into memory (for faster 31 | access) use volume.load 32 | 33 | .. automethod:: geoprobe.volume.load 34 | 35 | The original values stored in the volume can be recovered with the volume.d0 and 36 | volume.dv properties. For example: 37 | >>>rescaled = volume.data * volume.dv + volume + d0 38 | 39 | .. attribute:: geoprobe.volume.dv 40 | 41 | Voxel value scaling factor 42 | 43 | .. attribute:: geoprobe.volume.d0 44 | 45 | Voxel value calibration factor 46 | 47 | There are three coordinate systems stored in a geoprobe volume file. 48 | 1) The array indicies. These range from 0 to nx-1, 0 to ny-1, 0 to nz-1. 49 | 2) The model coordinates (e.g. inline and crossline). These range from volume.xmin 50 | to volume.xmax, volume.ymin to volume.ymax, and volume.zmin to volume.zmax. 51 | 3) The world coordinates. These are typically a map projection of some sort (e.g. 52 | utm). However, the only restriction is that they must be related to the model 53 | coordinates via an affine transformation. 54 | 55 | .. automethod:: geoprobe.volume.index2model 56 | .. automethod:: geoprobe.volume.model2index 57 | .. automethod:: geoprobe.volume.model2world 58 | .. automethod:: geoprobe.volume.world2model 59 | .. autoattribute:: geoprobe.volume.worldcoords 60 | .. autoattribute:: geoprobe.volume.modelcoords 61 | .. autoattribute:: geoprobe.volume.transform 62 | .. autoattribute:: geoprobe.volume.invtransform 63 | .. autoattribute:: geoprobe.volume.modelCoords 64 | .. autoattribute:: geoprobe.volume.worldCoords 65 | 66 | The x, y, and z spacing and starting values are defined in the following attributes. 67 | 68 | .. attribute:: geoprobe.volume.x0 69 | 70 | x axis calibration factor. (model x-coordinate = dx * i + x0, where i is the x-axis index) 71 | Defaults to 0.0 72 | 73 | .. attribute:: geoprobe.volume.y0 74 | 75 | y axis calibration factor. (model y-coordinate = dy * i + y0, where i is the y-axis index) 76 | Defaults to 0.0 77 | 78 | .. attribute:: geoprobe.volume.z0 79 | 80 | z axis calibration factor. (model z-coordinate = dz * i + z0, where i is the z-axis index) 81 | Defaults to 0.0 82 | 83 | .. attribute:: geoprobe.volume.dx 84 | 85 | x axis scaling factor. (model x-coordinate = dx * i + x0, where i is the x-axis index) 86 | :attribute: `geoprobe.volume.dx` may be negative, but cannot be 0. Defaults to 1.0 87 | 88 | .. attribute:: geoprobe.volume.dy 89 | 90 | y axis scaling factor. (model y-coordinate = dy * i + y0, where i is the y-axis index) 91 | :attribute: `geoprobe.volume.dy` may be negative, but cannot be 0. Defaults to 1.0 92 | 93 | .. attribute:: geoprobe.volume.dz 94 | 95 | z axis scaling factor. (model z-coordinate = dz * i + z0, where i is the z-axis index) 96 | :attribute: `geoprobe.volume.dz` may be negative, but cannot be 0. Defaults to 1.0 97 | 98 | 99 | The xmin, xmax, ymin, ymax, zmin, and zmax attributes define the model coords. Setting 100 | these will change x0, y0, or z0 (respectively). 101 | 102 | .. autoattribute:: geoprobe.volume.xmin 103 | .. autoattribute:: geoprobe.volume.xmax 104 | .. autoattribute:: geoprobe.volume.ymin 105 | .. autoattribute:: geoprobe.volume.ymax 106 | .. autoattribute:: geoprobe.volume.zmin 107 | .. autoattribute:: geoprobe.volume.zmax 108 | 109 | Changing the sign of dx, dy, or dz changes the order in which the array is stored on 110 | disk and will change volume.min and volume.max. However, volume.data 111 | is always accessed such that volume.data[0,0,0] corresponds to xmin, ymin, zmin and 112 | volume.data[nx,ny,nz] corresponds to xmax, ymax, zmax. 113 | 114 | The world x and y grid spacing can be obtained from the volume.dxW and volume.dyW attributes. 115 | 116 | .. autoattribute:: geoprobe.volume.dxW 117 | .. autoattribute:: geoprobe.volume.dyW 118 | 119 | There are also convience methods to quickly extract an axis-aligned slice at a given model 120 | coordinate. 121 | 122 | .. automethod:: geoprobe.volume.XSlice 123 | .. automethod:: geoprobe.volume.YSlice 124 | .. automethod:: geoprobe.volume.ZSlice 125 | -------------------------------------------------------------------------------- /examples/basic_horizon_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | A quick example of viewing data stored in a geoprobe horizon file 3 | """ 4 | from __future__ import print_function 5 | import os 6 | 7 | import matplotlib.pyplot as plt 8 | 9 | import geoprobe 10 | 11 | def main(): 12 | # Path to the example data dir relative to the location of this script. 13 | # This is just so that the script can be called from a different directory 14 | datadir = os.path.join(os.path.dirname(__file__), 'data') 15 | 16 | # Read an existing geoprobe horizon 17 | hor = geoprobe.horizon(os.path.join(datadir, 'Horizons', 'channels.hzn')) 18 | 19 | print_info(hor) 20 | plot(hor) 21 | 22 | def print_info(hor): 23 | """Print some basic information about "hor", a geoprobe.horizon instance""" 24 | print('The horizon has a total of %i points, %i of which are' 25 | ' auto-tracked' % (hor.data.size, hor.surface.size)) 26 | print('The horizon has %i manually picked lines' % len(hor.lines)) 27 | print('The inline coordinates range from', hor.xmin, 'to', hor.xmax) 28 | print('The crossline coordinates range from', hor.ymin, 'to', hor.ymax) 29 | print('The depth/time coordinates range from', hor.zmin, 'to', hor.zmax) 30 | 31 | def plot(hor): 32 | """Plot the "filled" z-values and "manual picks" in the geoprobe.horizon 33 | instance""" 34 | #-- Plot the "filled" values ---------------------------------------------- 35 | fig, ax = plt.subplots() 36 | 37 | # hor.grid is a 2D array of the Z-values stored in the horizon 38 | im = ax.imshow(hor.grid, cmap=plt.cm.jet_r, 39 | extent=(hor.xmin, hor.xmax, hor.ymax, hor.ymin)) 40 | 41 | # Here, "line" is a numpy structured array with fields 'x', 'y', 'z', etc. 42 | # "line_info" is a 4-tuple of (xdir, ydir, zdir, ID) (and is unused here) 43 | for line_info, line in hor.lines: 44 | ax.plot(line['x'], line['y'], 'g-') 45 | 46 | #-- Labels, titles, etc --------------------------------------------------- 47 | cb = fig.colorbar(im, orientation='horizontal') 48 | cb.set_label('Depth in meters below sea level') 49 | 50 | ax.axis('image') 51 | ax.set(title='An example horizon file', xlabel='Inline', ylabel='Crossline') 52 | 53 | plt.show() 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /examples/basic_volume_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | A quick example of viewing data stored in a geoprobe volume file. 3 | """ 4 | from __future__ import print_function 5 | import os 6 | 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | 10 | import geoprobe 11 | 12 | def main(): 13 | # Path to the example data dir relative to the location of this script. 14 | # This is just so that the script can be called from a different directory 15 | datadir = os.path.join(os.path.dirname(__file__), 'data') 16 | 17 | # Read an existing geoprobe volume 18 | vol = geoprobe.volume(os.path.join(datadir, 'Volumes', 'example.vol')) 19 | 20 | # Print some info 21 | print_info(vol) 22 | 23 | # Example plots 24 | plot(vol) 25 | 26 | def plot(vol): 27 | """Plot the first inline and first crossline in "vol", a geoprobe.volume 28 | instance.""" 29 | # Plot the first inline in the volume 30 | fig, ax = plt.subplots() 31 | ax.imshow(vol.XSlice(vol.xmin)) 32 | # Note: instead of vol.XSlice, we could have used vol.data[0,:,:].T 33 | ax.set(title='Inline %i' % vol.xmin) 34 | 35 | # Plot the first crossline in the volume 36 | fig, ax = plt.subplots() 37 | ax.imshow(vol.YSlice(vol.ymin)) 38 | # Note: instead of vol.YSlice, we could have used vol.data[:,0,:].T 39 | ax.set(title='Crossline %i' % vol.ymin) 40 | 41 | plt.show() 42 | 43 | def print_info(vol): 44 | """Print some basic information about "vol", a geoprobe.volume instance.""" 45 | # Print out some basic information 46 | print('The volume has dimensions of (nx, ny, nz):', vol.nx, vol.ny, vol.nz) 47 | print('The inline coordinates range from', vol.xmin, 'to', vol.xmax) 48 | print('The inline spacing is:', vol.dxW, 'world units') 49 | print('The crossline coordinates range from', vol.ymin, 'to', vol.ymax) 50 | print('The crossline spacing is:', vol.dyW, 'world units') 51 | print('The depth/time coordinates range from', vol.zmin, 'to', vol.zmax) 52 | 53 | # Determine the locations of the corners 54 | print('The world coordinates of the corners of the volume are:') 55 | print(' Lower-left:', vol.model2world(vol.xmin, vol.ymin)) 56 | print(' Upper-left:', vol.model2world(vol.xmin, vol.ymax)) 57 | print(' Upper-right:', vol.model2world(vol.xmax, vol.ymax)) 58 | print(' Lower-right:', vol.model2world(vol.xmax, vol.ymin)) 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /examples/data/Colormaps/blue-orange-tweaked.colormap: -------------------------------------------------------------------------------- 1 | #GeoProbe ColorMapRamp V1.0 ascii 2 | 1 3 | 7 4 | 0.0316412 0 0 1 1 5 | 0.196751 0.256637 0.243243 0.245614 1 6 | 0.505069 0.765487 0.738739 0.72807 1 7 | 0.812274 0.530973 0.27027 0 1 8 | 0.927798 1 0.333333 0 1 9 | 0.99278 1 1 0 1 10 | 0.996504 0 1 1 1 11 | 256 12 | 0 0 1 1 1 13 | 0 0 1 1 0 14 | 0 0 1 1 0 15 | 0 0 1 1 0 16 | 0 0 1 1 0 17 | 0 0 1 1 0 18 | 0 0 1 1 0 19 | 0 0 1 1 0 20 | 0 0 1 1 1 21 | 0.00567785 0.00538152 0.98331 1 0 22 | 0.0117733 0.0111589 0.965392 1 0 23 | 0.0178688 0.0169362 0.947475 1 0 24 | 0.0239642 0.0227135 0.929557 1 0 25 | 0.0300597 0.0284909 0.911639 1 0 26 | 0.0361552 0.0342682 0.893722 1 0 27 | 0.0422506 0.0400455 0.875804 1 0 28 | 0.0483461 0.0458229 0.857886 1 0 29 | 0.0544415 0.0516002 0.839969 1 0 30 | 0.060537 0.0573775 0.822051 1 0 31 | 0.0666325 0.0631549 0.804133 1 0 32 | 0.0727279 0.0689322 0.786216 1 0 33 | 0.0788234 0.0747095 0.768298 1 0 34 | 0.0849188 0.0804869 0.750381 1 0 35 | 0.0910143 0.0862642 0.732463 1 0 36 | 0.0971098 0.0920416 0.714545 1 0 37 | 0.103205 0.0978189 0.696628 1 0 38 | 0.109301 0.103596 0.67871 1 0 39 | 0.115396 0.109374 0.660792 1 0 40 | 0.121492 0.115151 0.642875 1 0 41 | 0.127587 0.120928 0.624957 1 0 42 | 0.133683 0.126706 0.607039 1 0 43 | 0.139778 0.132483 0.589122 1 0 44 | 0.145873 0.13826 0.571204 1 0 45 | 0.151969 0.144038 0.553286 1 0 46 | 0.158064 0.149815 0.535369 1 0 47 | 0.16416 0.155592 0.517451 1 0 48 | 0.170255 0.16137 0.499534 1 0 49 | 0.176351 0.167147 0.481616 1 0 50 | 0.182446 0.172924 0.463698 1 0 51 | 0.188542 0.178702 0.445781 1 0 52 | 0.194637 0.184479 0.427863 1 0 53 | 0.200733 0.190256 0.409945 1 0 54 | 0.206828 0.196034 0.392028 1 0 55 | 0.212924 0.201811 0.37411 1 0 56 | 0.219019 0.207588 0.356192 1 0 57 | 0.225114 0.213366 0.338275 1 0 58 | 0.23121 0.219143 0.320357 1 0 59 | 0.237305 0.22492 0.302439 1 0 60 | 0.243401 0.230698 0.284522 1 0 61 | 0.249496 0.236475 0.266604 1 0 62 | 0.256637 0.243243 0.245614 1 1 63 | 0.261999 0.248465 0.250698 1 0 64 | 0.268472 0.254767 0.256835 1 0 65 | 0.274944 0.261069 0.262971 1 0 66 | 0.281416 0.267372 0.269108 1 0 67 | 0.287888 0.273674 0.275244 1 0 68 | 0.29436 0.279976 0.281381 1 0 69 | 0.300832 0.286279 0.287517 1 0 70 | 0.307305 0.292581 0.293653 1 0 71 | 0.313777 0.298883 0.29979 1 0 72 | 0.320249 0.305186 0.305926 1 0 73 | 0.326721 0.311488 0.312063 1 0 74 | 0.333193 0.31779 0.318199 1 0 75 | 0.339665 0.324092 0.324336 1 0 76 | 0.346138 0.330395 0.330472 1 0 77 | 0.35261 0.336697 0.336609 1 0 78 | 0.359082 0.342999 0.342745 1 0 79 | 0.365554 0.349302 0.348882 1 0 80 | 0.372026 0.355604 0.355018 1 0 81 | 0.378498 0.361906 0.361155 1 0 82 | 0.384971 0.368209 0.367291 1 0 83 | 0.391443 0.374511 0.373427 1 0 84 | 0.397915 0.380813 0.379564 1 0 85 | 0.404387 0.387116 0.3857 1 0 86 | 0.410859 0.393418 0.391837 1 0 87 | 0.417332 0.39972 0.397973 1 0 88 | 0.423804 0.406023 0.40411 1 0 89 | 0.430276 0.412325 0.410246 1 0 90 | 0.436748 0.418627 0.416383 1 0 91 | 0.44322 0.42493 0.422519 1 0 92 | 0.449692 0.431232 0.428656 1 0 93 | 0.456165 0.437534 0.434792 1 0 94 | 0.462637 0.443837 0.440929 1 0 95 | 0.469109 0.450139 0.447065 1 0 96 | 0.475581 0.456441 0.453202 1 0 97 | 0.482053 0.462744 0.459338 1 0 98 | 0.488525 0.469046 0.465474 1 0 99 | 0.494998 0.475348 0.471611 1 0 100 | 0.50147 0.481651 0.477747 1 0 101 | 0.507942 0.487953 0.483884 1 0 102 | 0.514414 0.494255 0.49002 1 0 103 | 0.520886 0.500557 0.496157 1 0 104 | 0.527358 0.50686 0.502293 1 0 105 | 0.533831 0.513162 0.50843 1 0 106 | 0.540303 0.519464 0.514566 1 0 107 | 0.546775 0.525767 0.520703 1 0 108 | 0.553247 0.532069 0.526839 1 0 109 | 0.559719 0.538372 0.532976 1 0 110 | 0.566192 0.544674 0.539112 1 0 111 | 0.572664 0.550976 0.545249 1 0 112 | 0.579136 0.557278 0.551385 1 0 113 | 0.585608 0.563581 0.557521 1 0 114 | 0.59208 0.569883 0.563658 1 0 115 | 0.598552 0.576185 0.569794 1 0 116 | 0.605025 0.582488 0.575931 1 0 117 | 0.611497 0.58879 0.582067 1 0 118 | 0.617969 0.595092 0.588204 1 0 119 | 0.624441 0.601395 0.59434 1 0 120 | 0.630913 0.607697 0.600477 1 0 121 | 0.637385 0.613999 0.606613 1 0 122 | 0.643858 0.620302 0.61275 1 0 123 | 0.65033 0.626604 0.618886 1 0 124 | 0.656802 0.632906 0.625023 1 0 125 | 0.663274 0.639209 0.631159 1 0 126 | 0.669746 0.645511 0.637295 1 0 127 | 0.676219 0.651813 0.643432 1 0 128 | 0.682691 0.658116 0.649568 1 0 129 | 0.689163 0.664418 0.655705 1 0 130 | 0.695635 0.67072 0.661841 1 0 131 | 0.702107 0.677023 0.667978 1 0 132 | 0.708579 0.683325 0.674114 1 0 133 | 0.715052 0.689627 0.680251 1 0 134 | 0.721524 0.69593 0.686387 1 0 135 | 0.727996 0.702232 0.692524 1 0 136 | 0.734468 0.708534 0.69866 1 0 137 | 0.74094 0.714836 0.704797 1 0 138 | 0.747412 0.721139 0.710933 1 0 139 | 0.753885 0.727441 0.71707 1 0 140 | 0.760357 0.733743 0.723206 1 0 141 | 0.765487 0.738739 0.72807 1 1 142 | 0.761873 0.731519 0.716849 1 0 143 | 0.758879 0.725539 0.707555 1 0 144 | 0.755885 0.719559 0.698261 1 0 145 | 0.752892 0.713578 0.688967 1 0 146 | 0.749898 0.707598 0.679673 1 0 147 | 0.746904 0.701618 0.670379 1 0 148 | 0.743911 0.695638 0.661085 1 0 149 | 0.740917 0.689658 0.651791 1 0 150 | 0.737924 0.683678 0.642497 1 0 151 | 0.73493 0.677698 0.633203 1 0 152 | 0.731936 0.671717 0.623909 1 0 153 | 0.728943 0.665737 0.614614 1 0 154 | 0.725949 0.659757 0.60532 1 0 155 | 0.722955 0.653777 0.596026 1 0 156 | 0.719962 0.647797 0.586732 1 0 157 | 0.716968 0.641817 0.577438 1 0 158 | 0.713974 0.635836 0.568144 1 0 159 | 0.710981 0.629856 0.55885 1 0 160 | 0.707987 0.623876 0.549556 1 0 161 | 0.704993 0.617896 0.540262 1 0 162 | 0.702 0.611916 0.530968 1 0 163 | 0.699006 0.605936 0.521674 1 0 164 | 0.696012 0.599956 0.51238 1 0 165 | 0.693019 0.593975 0.503086 1 0 166 | 0.690025 0.587995 0.493792 1 0 167 | 0.687032 0.582015 0.484498 1 0 168 | 0.684038 0.576035 0.475204 1 0 169 | 0.681044 0.570055 0.46591 1 0 170 | 0.678051 0.564075 0.456616 1 0 171 | 0.675057 0.558095 0.447322 1 0 172 | 0.672063 0.552114 0.438028 1 0 173 | 0.66907 0.546134 0.428734 1 0 174 | 0.666076 0.540154 0.41944 1 0 175 | 0.663082 0.534174 0.410146 1 0 176 | 0.660089 0.528194 0.400852 1 0 177 | 0.657095 0.522214 0.391558 1 0 178 | 0.654101 0.516233 0.382264 1 0 179 | 0.651108 0.510253 0.372969 1 0 180 | 0.648114 0.504273 0.363675 1 0 181 | 0.645121 0.498293 0.354381 1 0 182 | 0.642127 0.492313 0.345087 1 0 183 | 0.639133 0.486333 0.335793 1 0 184 | 0.63614 0.480353 0.326499 1 0 185 | 0.633146 0.474372 0.317205 1 0 186 | 0.630152 0.468392 0.307911 1 0 187 | 0.627159 0.462412 0.298617 1 0 188 | 0.624165 0.456432 0.289323 1 0 189 | 0.621171 0.450452 0.280029 1 0 190 | 0.618178 0.444472 0.270735 1 0 191 | 0.615184 0.438491 0.261441 1 0 192 | 0.61219 0.432511 0.252147 1 0 193 | 0.609197 0.426531 0.242853 1 0 194 | 0.606203 0.420551 0.233559 1 0 195 | 0.60321 0.414571 0.224265 1 0 196 | 0.600216 0.408591 0.214971 1 0 197 | 0.597222 0.402611 0.205677 1 0 198 | 0.594229 0.39663 0.196383 1 0 199 | 0.591235 0.39065 0.187089 1 0 200 | 0.588241 0.38467 0.177795 1 0 201 | 0.585248 0.37869 0.168501 1 0 202 | 0.582254 0.37271 0.159207 1 0 203 | 0.57926 0.36673 0.149913 1 0 204 | 0.576267 0.360749 0.140618 1 0 205 | 0.573273 0.354769 0.131324 1 0 206 | 0.570279 0.348789 0.12203 1 0 207 | 0.567286 0.342809 0.112736 1 0 208 | 0.564292 0.336829 0.103442 1 0 209 | 0.561298 0.330849 0.0941482 1 0 210 | 0.558305 0.324869 0.0848541 1 0 211 | 0.555311 0.318888 0.0755601 1 0 212 | 0.552318 0.312908 0.0662661 1 0 213 | 0.549324 0.306928 0.056972 1 0 214 | 0.54633 0.300948 0.047678 1 0 215 | 0.543337 0.294968 0.038384 1 0 216 | 0.540343 0.288988 0.0290899 1 0 217 | 0.537349 0.283007 0.0197958 1 0 218 | 0.534356 0.277027 0.0105018 1 0 219 | 0.530973 0.27027 0 1 1 220 | 0.544826 0.272133 0 1 0 221 | 0.560747 0.274273 0 1 0 222 | 0.576669 0.276414 0 1 0 223 | 0.59259 0.278555 0 1 0 224 | 0.608512 0.280695 0 1 0 225 | 0.624434 0.282836 0 1 0 226 | 0.640355 0.284977 0 1 0 227 | 0.656277 0.287118 0 1 0 228 | 0.672199 0.289258 0 1 0 229 | 0.68812 0.291399 0 1 0 230 | 0.704042 0.29354 0 1 0 231 | 0.719963 0.295681 0 1 0 232 | 0.735885 0.297821 0 1 0 233 | 0.751807 0.299962 0 1 0 234 | 0.767728 0.302103 0 1 0 235 | 0.78365 0.304244 0 1 0 236 | 0.799572 0.306384 0 1 0 237 | 0.815493 0.308525 0 1 0 238 | 0.831415 0.310666 0 1 0 239 | 0.847337 0.312807 0 1 0 240 | 0.863258 0.314947 0 1 0 241 | 0.87918 0.317088 0 1 0 242 | 0.895101 0.319229 0 1 0 243 | 0.911023 0.32137 0 1 0 244 | 0.926945 0.32351 0 1 0 245 | 0.942866 0.325651 0 1 0 246 | 0.958788 0.327792 0 1 0 247 | 0.97471 0.329933 0 1 0 248 | 0.990631 0.332073 0 1 0 249 | 1 0.333333 0 1 1 250 | 1 0.390124 0 1 0 251 | 1 0.430356 0 1 0 252 | 1 0.470588 0 1 0 253 | 1 0.510821 0 1 0 254 | 1 0.551053 0 1 0 255 | 1 0.591286 0 1 0 256 | 1 0.631518 0 1 0 257 | 1 0.67175 0 1 0 258 | 1 0.711983 0 1 0 259 | 1 0.752215 0 1 0 260 | 1 0.792448 0 1 0 261 | 1 0.83268 0 1 0 262 | 1 0.872913 0 1 0 263 | 1 0.913145 0 1 0 264 | 1 0.953377 0 1 0 265 | 1 1 0 1 1 266 | 0 1 1 1 1 267 | 0 1 1 1 1 268 | -------------------------------------------------------------------------------- /examples/data/Horizons/channels.hzn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joferkington/python-geoprobe/535474f09a34f0cbbbfc6ff25219655b00bd410d/examples/data/Horizons/channels.hzn -------------------------------------------------------------------------------- /examples/data/Horizons/seafloor.hzn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joferkington/python-geoprobe/535474f09a34f0cbbbfc6ff25219655b00bd410d/examples/data/Horizons/seafloor.hzn -------------------------------------------------------------------------------- /examples/data/Volumes/example.vol: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joferkington/python-geoprobe/535474f09a34f0cbbbfc6ff25219655b00bd410d/examples/data/Volumes/example.vol -------------------------------------------------------------------------------- /examples/data/swFaults/empty.swf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jdk_piggyback_normal10 6 | 0.42249992 7 | 0 8 | 0 9 | 1 10 | 1 11 | 12 | 0 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/data/swFaults/example_normal.swf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jdk_example_normal 6 | 1 7 | 0 8 | 0 9 | 1 10 | 3 11 | 12 | 6 13 | 14 | 2147483647 15 | 16 | 2 17 | 18 | 2220.7773 19 | 5021 20 | 3052.5894 21 | 22 | 23 | 2211.2029 24 | 5021.0068 25 | 3341.2896 26 | 27 | 28 | 29 | 30 | 2147483647 31 | 32 | 2 33 | 34 | 2209.5742 35 | 5051.71 36 | 3130 37 | 38 | 39 | 2232.7366 40 | 4982.6602 41 | 3130 42 | 43 | 44 | 45 | 46 | 2147483647 47 | 48 | 2 49 | 50 | 2225.334 51 | 5012 52 | 3022.8291 53 | 54 | 55 | 2214.6421 56 | 5012 57 | 3351.5691 58 | 59 | 60 | 61 | 62 | 2147483647 63 | 64 | 2 65 | 66 | 2229.6238 67 | 5001 68 | 3038.5769 69 | 70 | 71 | 2218.1804 72 | 5000.9932 73 | 3379.2207 74 | 75 | 76 | 77 | 78 | 2147483647 79 | 80 | 2 81 | 82 | 2216.4241 83 | 5040 84 | 3019.1362 85 | 86 | 87 | 2207.9998 88 | 5040.0054 89 | 3300.3125 90 | 91 | 92 | 93 | 94 | 2147483647 95 | 96 | 2 97 | 98 | 2217.4424 99 | 5034 100 | 3067.4927 101 | 102 | 103 | 2208.0847 104 | 5034.0044 105 | 3310.8799 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/data/swFaults/example_ss.swf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jdk_example_ss 6 | 1 7 | 0 8 | 0 9 | 1 10 | 3 11 | 12 | 9 13 | 14 | 2147483647 15 | 16 | 2 17 | 18 | 2233.0977 19 | 5100 20 | 2941.7305 21 | 22 | 23 | 2242.2117 24 | 5098.5874 25 | 3551.3704 26 | 27 | 28 | 29 | 30 | 2147483647 31 | 32 | 2 33 | 34 | 2234.4243 35 | 5098 36 | 2943.8987 37 | 38 | 39 | 2245.1365 40 | 5096.0669 41 | 3546.7393 42 | 43 | 44 | 45 | 46 | 2147483647 47 | 48 | 2 49 | 50 | 2206.0505 51 | 5148.0859 52 | 2970 53 | 54 | 55 | 2261.5342 56 | 5051.5361 57 | 2970 58 | 59 | 60 | 61 | 62 | 2147483647 63 | 64 | 2 65 | 66 | 2282.2693 67 | 5016.0171 68 | 3040 69 | 70 | 71 | 2206.2961 72 | 5151.5879 73 | 3040 74 | 75 | 76 | 77 | 78 | 2147483647 79 | 80 | 2 81 | 82 | 2211.3533 83 | 5136 84 | 2896.1499 85 | 86 | 87 | 2228.1375 88 | 5135.9956 89 | 3600.7178 90 | 91 | 92 | 93 | 94 | 2147483647 95 | 96 | 2 97 | 98 | 2222.9097 99 | 5116 100 | 2920.4241 101 | 102 | 103 | 2238.7026 104 | 5116.0044 105 | 3628.8777 106 | 107 | 108 | 109 | 110 | 2147483647 111 | 112 | 2 113 | 114 | 2212.8093 115 | 5150.0469 116 | 3230 117 | 118 | 119 | 2280.0391 120 | 5020.1357 121 | 3230 122 | 123 | 124 | 125 | 126 | 2147483647 127 | 128 | 2 129 | 130 | 2257.709 131 | 5058 132 | 2972.3701 133 | 134 | 135 | 2264.5251 136 | 5058.0005 137 | 3431.5869 138 | 139 | 140 | 141 | 142 | 2147483647 143 | 144 | 2 145 | 146 | 2245.7273 147 | 5078 148 | 2951.7925 149 | 150 | 151 | 2253.5569 152 | 5077.9932 153 | 3480.1577 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /geoprobe/_2dHeader.py: -------------------------------------------------------------------------------- 1 | """Provides a dictonary containing the name, offset, type, and default value 2 | for each known value in a GeoProbe 2DData header""" 3 | 4 | __license__ = "MIT License " 5 | __copyright__ = "2009, Free Software Foundation" 6 | __author__ = "Joe Kington " 7 | 8 | """Reversed engineered Jan 18, 2010 by Joe Kington. GeoProbe 2DData (.2dd) files consist of 9 | a 8472 Byte header followed by "numtraces" traces stored as uint8's each with a 12 byte header. 10 | 11 | Trace format: 12 | A GeoProbe 2DData file stores amplitude values as a series of "traces" in the z-direction 13 | (rather than a regular grid). Each trace has a 12 Byte header (format: >3f) consisting of: 14 | 1) The model X-coordinate for the trace, 2) The model Y-coordinate for the trace, and 15 | 3) The trace number (stored as a float for some weird reason?) 16 | This is followed by "numsamples" uint8's containing the amplitude values 17 | """ 18 | 19 | 20 | #--Header Definition-------------------------------------------------------------- 21 | # default:None indicates that the vaule must be obtained from the input data 22 | headerLength = 8472 #In bytes 23 | headerDef = { 24 | 'version': {'offset':0, 'type':'32s', 'default':'#GeoProbe 2DData V1.0 binary\n', 'doc':'The version string stored at the beginning of the file'}, 25 | '_numtraces': {'offset':32, 'type':'>I', 'default':None , 'doc':'Number of x-values. This is _numTraces to allow the property data2d.numTraces, which returns data2d.grid.shape[0]'}, 26 | '_numsamples': {'offset':36, 'type':'>I', 'default':None , 'doc':'Number of z-values. (see above)'}, 27 | 'name': {'offset':40, 'type':'128s', 'default':'Unknown', 'doc':'Name of the line'}, 28 | 'starttime': {'offset':168, 'type':'>f', 'default':None , 'doc':'Start time of the traces'}, 29 | 'endtime': {'offset':172, 'type':'>f', 'default':None , 'doc':'End time of the traces'}, 30 | 'samplerate': {'offset':176, 'type':'>f', 'default':1.0 , 'doc':'Sample rate of the traces'}, 31 | 'x0': {'offset':180, 'type':'>f', 'default':0.0 , 'doc':'X-axis calibration factor (e.g. x = i*dx + x0, where i is the index value)'}, 32 | 'y0': {'offset':184, 'type':'>f', 'default':0.0 , 'doc':'Y-axis calibration factor'}, 33 | 'z0': {'offset':188, 'type':'>f', 'default':0.0 , 'doc':'Z-axis calibration factor'}, 34 | 'v0': {'offset':192, 'type':'>f', 'default':0.0 , 'doc':'Voxel value calibration factor'}, 35 | 'v0user': {'offset':196, 'type':'>f', 'default':0.0 , 'doc':'User-specified voxel value calibration factor (No idea what that means!)'}, 36 | 'dx': {'offset':200, 'type':'>f', 'default':1.0 , 'doc':'X-axis scaling factor'}, 37 | 'dy': {'offset':204, 'type':'>f', 'default':1.0 , 'doc':'Y-axis scaling factor'}, 38 | 'dz': {'offset':208, 'type':'>f', 'default':-1.0 , 'doc':'Z-axis scaling factor'}, 39 | 'dv': {'offset':212, 'type':'>f', 'default':1.0 , 'doc':'Voxel value scaling factor'}, 40 | 'dvuser': {'offset':216, 'type':'>f', 'default':1.0 , 'doc':'User-specified voxel value scaling factor (No idea what that means!)'}, 41 | 'xunit': {'offset':220, 'type':'16s', 'default':'Unknown' , 'doc':'Physical units for the X-axis'}, 42 | 'yunit': {'offset':236, 'type':'16s', 'default':'Unknown' , 'doc':'Physical units for the Y-axis'}, 43 | 'zunit': {'offset':252, 'type':'16s', 'default':'Unknown' , 'doc':'Physical units for the Z-axis'}, 44 | 'vunit': {'offset':268, 'type':'16s', 'default':'Unknown' , 'doc':'Physical units for voxels'}, 45 | 'xdescrip': {'offset':284, 'type':'16s', 'default':'Unknown' , 'doc':'X-axis description'}, 46 | 'ydescrip': {'offset':300, 'type':'16s', 'default':'Unknown' , 'doc':'Y-axis description'}, 47 | 'zdescrip': {'offset':316, 'type':'16s', 'default':'Unknown' , 'doc':'Z-axis description'}, 48 | 'vdescrip': {'offset':332, 'type':'16s', 'default':'Unknown' , 'doc':'Voxel description'}, 49 | 'smoothingfactor': {'offset':348, 'type':'>f', 'default':0.0 , 'doc':'No idea what this really is...'}, 50 | 'georef': {'offset':352, 'type':'>12d', 'default':[0,0,1,0,1,1,0,1,1,0,0,1] , 'doc':'3 sets of points for georeferencing. Order: worldX1, worldX2, worldX3, worldY1, worldY2, worldY3, modelY1, modelY2, modelY3, modelX1, modelX2, modelX3'}, 51 | 'createdbyuser': {'offset':448, 'type':'32s', 'default':'Unknown' , 'doc':'File originally created by username given'}, 52 | 'creationdate': {'offset':480, 'type':'>I', 'default':0 , 'doc':'Unix-style timestamp of the original creation date'}, 53 | 'lastmodifieduser': {'offset':484, 'type':'32s', 'default':'Unknown' , 'doc':'File last modified by username given'}, 54 | 'lastmodifieddate': {'offset':516, 'type':'>I', 'default':0 , 'doc':'Unix-style timestamp of last modified on date'}, 55 | 'histogram': {'offset':520, 'type':'>255Q', 'default':255*(0,) , 'doc':'Histogram of amplitude values, bins are 0-255 (or -127 to +127) '}, 56 | 'comments': {'offset':7700, 'type':'772s', 'default':'Created by python-geoprobe', 'doc':'Arbitrary comments.'} 57 | } 58 | # Almost definitely padding. No need to store this 59 | #'_unknown': {'offset':2560, 'type':'5140B', 'default':5140*(0,) }, # Appears to be padding... 60 | -------------------------------------------------------------------------------- /geoprobe/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A python module to read and write geoprobe format volumes, horizons, 3 | 2d data, and faults 4 | 5 | Reads and (mostly) writes seismic data from and to files written by 6 | Landmark's (a subsidiary of Halliburton) Geoprobe software. This 7 | implementation is based on reverse-engineering the file formats, and as 8 | such, is certainly not complete. However, things seem to work. 9 | 10 | As a basic example: 11 | 12 | >>> from geoprobe import volume 13 | >>> vol = volume('/path/to/geoprobe/volume/file.vol') 14 | >>> print(vol.xmin, vol.ymin) # Model coordinate min and max 15 | >>> test_xslice = vol.data[vol.nx/2,:,:] # a memmapped numpy array 16 | """ 17 | __author__ = 'Joe Kington ' 18 | __license__ = 'MIT License' 19 | __version__ = '0.4.0' 20 | 21 | 22 | from .horizon import horizon 23 | from .volume import volume, Volume 24 | from .ezfault import ezfault 25 | from .data2d import data2d 26 | from . import utilities 27 | from .colormap import colormap 28 | from .swfault import swfault 29 | from .tsurf import tsurf 30 | 31 | from .volume import isValidVolume 32 | 33 | __all__ = ['horizon', 'volume', 'Volume', 'ezfault', 'data2d', 'utilities', 34 | 'isValidVolume', 'colormap', 'swfault', 'tsurf'] 35 | -------------------------------------------------------------------------------- /geoprobe/_volHeader.py: -------------------------------------------------------------------------------- 1 | """Provides a dictonary containing the name, offset, type, and default value 2 | for each known value in a GeoProbe volume header""" 3 | 4 | __license__ = "MIT License " 5 | __copyright__ = "2009, Free Software Foundation" 6 | __author__ = "Joe Kington " 7 | 8 | """ 9 | Reversed engineered sometime in early 2009 by Joe Kington to make my 10 | dissertation a bit easier. Geoprobe volumes consist of a 3072 byte header, 11 | with various essential metadata. 12 | 13 | Voxels are stored as 1 byte values (i.e. 0-255 or -127 to +127) in fortran 14 | order (x cycles fastest, then y, then z. In other words, the volume is stored 15 | as a collection of z slices). The bottom slice is typically written first 16 | (thus the default dz of -1). 17 | 18 | There's definitely some stuff I don't understand (thus the "_unknown*" 19 | variables). However, this seems to work for both reading and writing. No 20 | guarentees that it will handle every geoprobe volume you'll ever come across, 21 | though! 22 | """ 23 | 24 | 25 | #--Header Definition-------------------------------------------------------------- 26 | # default:None indicates that the vaule must be obtained from the input data 27 | headerLength = 3072 #In bytes 28 | headerDef = { 29 | 'magicNum': {'offset':0, 'type':'>i', 'default':43970, 30 | 'doc':'Number indicating a geoprobe volume'}, 31 | 32 | '_unknown1': {'offset':4, 'type':'>4i', 'default':[1,0,0,0], 33 | 'doc':'Unknown, important?'}, 34 | 35 | 'path': {'offset':20, 'type':'300s', 'default':300*" ", 36 | 'doc':'Path of original geoprobe volume'}, 37 | 38 | '_unknown2': {'offset':320, 'type':'>4i', 'default':[8,0,0,8], 39 | 'doc':'Unknown, important? (8bit? but why two of them?)'}, 40 | 41 | '_nx': {'offset':336, 'type':'>i', 'default':None, 42 | 'doc':'Number of x-values. this is _nx to allow the' \ 43 | ' property volume.nx, which returns vol.data.shape[0]'}, 44 | 45 | '_ny': {'offset':340, 'type':'>i', 'default':None, 46 | 'doc':'Number of y-values. (see above)'}, 47 | 48 | '_nz': {'offset':344, 'type':'>i', 'default':None, 49 | 'doc':'Number of z-values. (see above)'}, 50 | 51 | 'v0': {'offset':352, 'type':'>f', 'default':0, 52 | 'doc':'Voxel value calibration factor (Physical voxel' \ 53 | ' values = V*dv + v0, where V is the raw uint8' \ 54 | ' value in volume.data)'}, 55 | 56 | 'x0': {'offset':356, 'type':'>f', 'default':0, 57 | 'doc':'X-axis calibration factor (e.g. x = i*dx + x0)'}, 58 | 59 | 'y0': {'offset':360, 'type':'>f', 'default':0, 60 | 'doc':'Y-axis calibration factor (e.g. y = j*dy + y0)'}, 61 | 62 | 'z0': {'offset':364, 'type':'>f', 'default':0, 63 | 'doc':'Z-axis calibration factor (e.g. z = k*dz + z0)'}, 64 | 65 | 'dv': {'offset':368, 'type':'>f', 'default':1, 66 | 'doc':'Voxel value scaling factor (Physical voxel' \ 67 | ' values = V*dv + v0, where V is the raw uint8' \ 68 | ' value in volume.data)'}, 69 | 70 | 'dx': {'offset':372, 'type':'>f', 'default':1, 71 | 'doc':'X-axis scaling factor (e.g. x = i*dx + x0)'}, 72 | 73 | 'dy': {'offset':376, 'type':'>f', 'default':1, 74 | 'doc':'Y-axis scaling factor (e.g. y = j*dy + y0)'}, 75 | 76 | 'dz': {'offset':380, 'type':'>f', 'default':1, 77 | 'doc':'Z-axis scaling factor (e.g. z = k*dz + z0)'}, 78 | 79 | 'vunit': {'offset':384, 'type':'16s', 'default':'unknown', 80 | 'doc':'Physical units for voxels'}, 81 | 82 | 'xunit': {'offset':400, 'type':'16s', 'default':'unknown', 83 | 'doc':'Physical units for the x-axis'}, 84 | 85 | 'yunit': {'offset':416, 'type':'16s', 'default':'unknown', 86 | 'doc':'Physical units for the y-axis'}, 87 | 88 | 'zunit': {'offset':432, 'type':'16s', 'default':'unknown', 89 | 'doc':'Physical units for the z-axis'}, 90 | 91 | 'vdescrip': {'offset':448, 'type':'16s', 'default':'unknown', 92 | 'doc':'Voxel description'}, 93 | 94 | 'xdescrip': {'offset':464, 'type':'16s', 'default':'unknown', 95 | 'doc':'X-axis description'}, 96 | 97 | 'ydescrip': {'offset':480, 'type':'16s', 'default':'unknown', 98 | 'doc':'Y-axis description'}, 99 | 100 | 'zdescrip': {'offset':496, 'type':'16s', 'default':'unknown', 101 | 'doc':'Z-axis description'}, 102 | 103 | '_unknown3': {'offset':2584, 'type':'8s', 'default':'\xaa\xff\xff#\xaa\x00\x00#', 104 | 'doc':'Probably important! no idea what it is, though...'}, 105 | 106 | 'georef': {'offset':2592, 'type':'>12d', 'default':[0,0,1,0,1,1,0,1,1,0,0,1], 107 | 'doc':'3 sets of points for georeferencing. order:' \ 108 | ' worldx1, worldx2, worldx3, worldy1, worldy2,' \ 109 | ' worldy3, modely1, modely2, modely3, modelx1,' \ 110 | ' modelx2, modelx3'}, 111 | 112 | 'originalnx': {'offset':2744, 'type':'>i', 'default':None, 113 | 'doc':'Original dimensions of the x-axis'}, 114 | 115 | 'originalny': {'offset':2748, 'type':'>i', 'default':None, 116 | 'doc':'Original dimensions of the y-axis'}, 117 | 118 | 'originalnz': {'offset':2752, 'type':'>i', 'default':None, 119 | 'doc':'Original dimensions of the z-axis'}, 120 | 121 | 'segmentname':{'offset':2762, 'type':'50s', 'default':50*" ", 122 | 'doc':'Pathname relative to the base geoprobe project directory'}, 123 | 124 | 'seisworksproject':{'offset':2816, 'type':'256s', 'default':'', 125 | 'doc':'Name of the associated seisworks project'} 126 | } 127 | 128 | 129 | # The following are almost definitely padding. I'm preserving them here in case the locations are ever needed... 130 | # '_padding1': {'offset':348, 'type':'>i', 'default':0 }, # Unknown, padding?? 131 | # '_padding2': {'offset':512, 'type':'>536i', 'default':536*[0] }, # Padding?? 132 | # '_padding3': {'offset':2688, 'type':'>14i', 'default':14*[0] }, # Padding?? 133 | # '_padding4': {'offset':2812, 'type':'>i', 'default':0 }, # Padding?? 134 | # '_padding5': {'offset':2756, 'type':'>6i', 'default':6*[0] }, # Padding?? 135 | -------------------------------------------------------------------------------- /geoprobe/colormap.py: -------------------------------------------------------------------------------- 1 | import six 2 | import numpy as np 3 | 4 | class colormap(object): 5 | """Reads a Geoprobe formatted colormap""" 6 | num_colors = 256 7 | def __init__(self, filename): 8 | self.filename = filename 9 | self._parse_infile() 10 | 11 | def _parse_infile(self): 12 | infile = open(self.filename, 'r') 13 | header = next(infile) 14 | if header.startswith('#'): 15 | _ = next(infile) 16 | self.num_keys = int(next(infile).strip()) 17 | keys = [] 18 | for i in range(self.num_keys): 19 | keys.append(next(infile).strip().split()) 20 | self.keys = np.array(keys, dtype=np.float) 21 | num_colors = int(next(infile).strip()) 22 | colors = [] 23 | for i in range(num_colors): 24 | colors.append(next(infile).strip().split()) 25 | self.lut = np.array(colors, dtype=np.float) 26 | dtype = {'names':['red', 'green', 'blue', 'alpha', 'keys'], 27 | 'formats':5 * [np.float]} 28 | self.lut = self.lut.view(dtype) 29 | 30 | @property 31 | def as_matplotlib(self): 32 | from matplotlib.colors import LinearSegmentedColormap 33 | cdict = dict(red=[], green=[], blue=[]) 34 | 35 | # Make sure that there is a key at 0.0 and 1.0 36 | keys = self.keys.tolist() 37 | if keys[0][0] != 0: 38 | keys = [[0.0] + keys[0][1:]] + keys 39 | if keys[-1][0] != 1.0: 40 | keys.append([1.0] + keys[-1][1:]) 41 | 42 | for stop_value, red, green, blue, alpha, in keys: 43 | for name, val in zip(['red', 'green', 'blue'], [red, green, blue]): 44 | cdict[name].append([stop_value, val, val]) 45 | return LinearSegmentedColormap(self.filename, cdict, self.num_colors) 46 | 47 | @property 48 | def as_pil(self): 49 | return list((255 * self.lut.view(np.float)[:,:3]).astype(np.int).flat) 50 | 51 | @property 52 | def as_pil_rgba(self): 53 | return list((255 * self.lut.view(np.float)[:,:4]).astype(np.int).flat) 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /geoprobe/common.py: -------------------------------------------------------------------------------- 1 | """Recipies and various code utility functions.""" 2 | 3 | from six import string_types 4 | 5 | import struct 6 | import textwrap 7 | 8 | #-- Miscellaneous ------------------------------------------------------------- 9 | def format_headerDef_docs(headerDef, initial_indent=8, subsequent_indent=12): 10 | """ 11 | Format the attributes contained in a headerDef for pretty printing 12 | (i.e. for use in docstrings) 13 | """ 14 | attribute_docs = '' 15 | initial_indent *= ' ' 16 | subsequent_indent *= ' ' 17 | 18 | for key in sorted(headerDef.keys()): 19 | value = headerDef[key] 20 | default = value['default'] 21 | if isinstance(default, string_types): 22 | default = default.strip() 23 | 24 | doc = '%s: %s (default=%s)' % (key, value['doc'], repr(default)) 25 | doc = textwrap.fill(doc, initial_indent=initial_indent, 26 | subsequent_indent=subsequent_indent) 27 | 28 | if not key.startswith('_'): 29 | attribute_docs += doc + '\n' 30 | 31 | return attribute_docs 32 | 33 | class cached_property(object): 34 | """ 35 | A decorator class that ensures that properties are only evaluated once. 36 | From: 37 | """ 38 | def __init__(self, calculate_function): 39 | self._calculate = calculate_function 40 | 41 | def __get__(self, obj, _=None): 42 | if obj is None: 43 | return self 44 | value = self._calculate(obj) 45 | setattr(obj, self._calculate.__name__, value) 46 | return value 47 | 48 | #-- Raw reading and writing --------------------------------------------------- 49 | def read_binary(infile, fmt): 50 | """ 51 | Read and unpack a binary value from the file based on string fmt (see the 52 | struct module for details). 53 | Input: 54 | infile: A file-like object to read from. 55 | fmt: A ``struct`` format string. 56 | Output: 57 | A tuple of unpacked data (or a single item if only one item is 58 | returned from ``struct.unpack``). 59 | """ 60 | size = struct.calcsize(fmt) 61 | data = infile.read(size) 62 | # Reading beyond the end of the file just returns '' 63 | if len(data) != size: 64 | raise EOFError('End of file reached') 65 | data = struct.unpack(fmt, data) 66 | 67 | for item in data: 68 | # Strip trailing zeros in strings 69 | if isinstance(item, string_types): 70 | item = item.strip('\x00') 71 | 72 | # Unpack the tuple if it only has one value 73 | if len(data) == 1: data = data[0] 74 | 75 | return data 76 | 77 | def write_binary(outfile, fmt, dat): 78 | """ 79 | Pack and write data to the file according to string fmt. 80 | Input: 81 | outfile: An open file-like object to write to. 82 | fmt: A ``struct`` format string. 83 | dat: Data to pack into binary form. 84 | """ 85 | # Try expanding input arguments (struct.pack won't take a tuple) 86 | try: 87 | dat = struct.pack(fmt, *dat) 88 | except (TypeError, struct.error): 89 | # If it's not a sequence (TypeError), or if it's a 90 | # string (struct.error), don't expand. 91 | dat = struct.pack(fmt, dat) 92 | outfile.write(dat) 93 | -------------------------------------------------------------------------------- /geoprobe/data2d.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import six 3 | 4 | from .common import format_headerDef_docs, read_binary, write_binary 5 | from ._2dHeader import headerDef as _headerDef 6 | from ._2dHeader import headerLength as _headerLength 7 | 8 | class data2d(object): 9 | __doc__ = """ 10 | Reads geoprobe 2D data files. 11 | 12 | Useful Attributes: 13 | data: array containg the seismic data as uint8's 14 | x: a list of the x-coordinates of each trace 15 | y: a list of the y-coordinates of each trace 16 | z: a list of the z-coordinates of each trace\n%s 17 | """ % format_headerDef_docs(_headerDef) 18 | # Not a "normal" docstring so that "useful attributes" is set 19 | # at initialization 20 | 21 | def __init__(self, arg, x=None, y=None, tracenumbers=None, copyFrom=None): 22 | """ 23 | Input: 24 | filename: The name of the 2D data file 25 | """ 26 | if isinstance(arg, six.string_types): 27 | filename = arg 28 | infile = open(filename, 'rb') 29 | self._read_header(infile) 30 | self._read_traces(infile) 31 | infile.close() 32 | else: 33 | if x is None or y is None: 34 | raise ValueError('When creating a new data2d object, '\ 35 | 'x and y must both be specified.') 36 | self._new_file(arg, x, y, tracenumbers, copyFrom) 37 | 38 | def _new_file(self, data, x, y, tracenumbers=None, copyFrom=None, 39 | starttime=0.0, endtime=None, samplerate=1.0): 40 | self.data, self.x, self.y = data, x, y 41 | if tracenumbers is None: 42 | tracenumbers = np.arange(1, self.numtraces+1, dtype='>f4') 43 | self.tracenumbers = tracenumbers 44 | # Set up the header Dictionary 45 | if copyFrom is not None: 46 | # Assume the string is the filename of a 2d data file 47 | if isinstance(copyFrom, six.string_types): 48 | copyFrom = data2d(copyFrom) 49 | try: 50 | self.headerValues = copyFrom.headerValues 51 | except AttributeError: 52 | raise TypeError('This does not appear to be a valid geoprobe'\ 53 | ' 2d data object') 54 | else: 55 | # Set default attributes 56 | for varname, info in six.iteritems(_headerDef): 57 | setattr(self, varname, info['default']) 58 | self._numtraces, self._numsamples = data.shape 59 | 60 | if endtime is None: 61 | endtime = starttime + samplerate * self.numsamples 62 | self.starttime = starttime 63 | self.samplerate = samplerate 64 | self.endtime = endtime 65 | 66 | def write(self, outfile): 67 | if isinstance(outfile, six.string_types): 68 | outfile = open(outfile, 'wb') 69 | self._write_header(outfile) 70 | self._write_traces(outfile) 71 | outfile.close() 72 | 73 | def _read_header(self, infile): 74 | """ 75 | Read the values stored in the file header and set each one as 76 | an attribue of the data2d object. 77 | """ 78 | for varname, info in six.iteritems(_headerDef): 79 | offset, fmt = info['offset'], info['type'] 80 | infile.seek(offset) 81 | var = read_binary(infile, fmt) 82 | setattr(self, varname, var) 83 | 84 | def _write_header(self, outfile): 85 | """Write the values in self.headerValues to "outfile".""" 86 | for varname, info in six.iteritems(_headerDef): 87 | value = getattr(self, varname, info['default']) 88 | outfile.seek(info['offset']) 89 | write_binary(outfile, info['type'], value) 90 | 91 | def _getHeaderValues(self): 92 | """ 93 | A dict of all the values stored in the file header 94 | (Each of these is also an attribute of any data2d object) 95 | """ 96 | # Return the current instance attributes that are a part of the 97 | # header definition 98 | values = {} 99 | for key in _headerDef.keys(): 100 | # If it's been deleted for some reason, return the default value 101 | default = _headerDef[key]['default'] 102 | values[key] = getattr(self, key, default) 103 | return values 104 | 105 | def _setHeaderValues(self, input_val): 106 | for key, value in six.iteritems(input_val): 107 | # Only set things in input that are normally in the header 108 | if key in _headerDef: 109 | setattr(self, key, value) 110 | 111 | headerValues = property(_getHeaderValues, _setHeaderValues) 112 | 113 | # Unused... Damnit, I need to decide what I'm doing here... 114 | def _fix_axes(self, data): 115 | """Reverses the z axis if dz is negative. This ensures that 116 | self.data[:,0] always corresponds to self.zmin.""" 117 | if self.dz < 0: 118 | data = data[:, ::-1] 119 | return data 120 | 121 | def _read_traces(self, infile): 122 | """ 123 | Read all traces (everything other than the file header) 124 | from "infile". 125 | """ 126 | dtype = [('x', '>f4'), ('y', '>f4'), ('tracenum', '>f4'), 127 | ('traces', '%i>u1'%self._numsamples)] 128 | infile.seek(_headerLength) 129 | data = np.fromfile(infile, dtype=dtype, count=self._numtraces) 130 | self.x = data['x'] 131 | self.y = data['y'] 132 | self.tracenumbers = data['tracenum'] 133 | self.data = data['traces'] 134 | 135 | def _write_traces(self, outfile): 136 | dtype = [('x', '>f4'), ('y', '>f4'), ('tracenum', '>f4'), 137 | ('traces', '%i>u1'%self._numsamples)] 138 | outfile.seek(_headerLength) 139 | data = np.empty(self.numtraces, dtype=dtype) 140 | data['x'] = self.x 141 | data['y'] = self.y 142 | data['traces'] = self.data 143 | data['tracenum'] = self.tracenumbers 144 | data.tofile(outfile, sep='') 145 | 146 | def _get_z(self): 147 | """Z-values (time/depth) for each trace.""" 148 | try: 149 | return self._z 150 | except AttributeError: 151 | self._z = np.linspace(self.zmin, self.zmax, self.numsamples) 152 | return self._z 153 | def _set_z(self, value): 154 | self._z = value 155 | z = property(_get_z, _set_z) 156 | 157 | def _bounds(self): 158 | start = self.z0 159 | # Fix this!!! "abs" is temporary!!!! 160 | stop = start + abs(self.dz) * (self.numsamples - 1) 161 | return start, stop 162 | 163 | def _get_scaled_data(self): 164 | """Trace array in its original units.""" 165 | return self.data * self.dv + self.v0 166 | def _set_scaled_data(self, value): 167 | self.v0 = value.min() 168 | self.dv = value.ptp() / 256.0 169 | self.data = (value - self.v0) / self.dv 170 | scaled_data = property(_get_scaled_data, _set_scaled_data) 171 | 172 | 173 | @property 174 | def zmin(self): 175 | return min(self._bounds()) 176 | 177 | @property 178 | def zmax(self): 179 | return max(self._bounds()) 180 | 181 | @property 182 | def numtraces(self): 183 | """ 184 | The number of traces stored in the file. 185 | Equivalent to self.data.shape[0] 186 | """ 187 | return self.data.shape[0] 188 | 189 | @property 190 | def numsamples(self): 191 | """ 192 | The number of samples in each trace stored in the file 193 | Equivalent to self.data.shape[1] 194 | """ 195 | return self.data.shape[1] 196 | -------------------------------------------------------------------------------- /geoprobe/ezfault.py: -------------------------------------------------------------------------------- 1 | """Really simplistic Landmark EzFault reader 2 | JDK 08/03/09""" 3 | # TODO: This whole thing needs documentation! 4 | # TODO: Methods to convert to grid / triangle strips 5 | # TODO: Write support... Eventually 6 | 7 | import numpy as np 8 | 9 | # Local files 10 | from . import utilities 11 | from .volume import volume 12 | 13 | class ezfault(object): 14 | """Simple geoprobe ezfault reader.""" 15 | def __init__(self, filename): 16 | """Takes a filename as input""" 17 | self._infile = file(filename, 'r') 18 | self._readHeader() 19 | 20 | 21 | def _readHeader(self): 22 | """ Blindly read the first 6 lines and set properties based on them 23 | This doesn't check order or sanity of any sort....""" 24 | # This really is a bit of a dumb and brittle parser... Doesn't parse 25 | # the {'s and }'s, just the position of the data in each line. 26 | 27 | self._infile.seek(0) 28 | 29 | #1st Line: "#GeoProbe Surface V1.0 ascii" 30 | # Identification string 31 | self.IDstring = self._infile.readline() 32 | 33 | #2nd Line: "FACE %i" 34 | # Face Direction (1=X, 2=Y, 3=Z) 35 | self.face = self._infile.readline().strip().split() 36 | self.face = int(self.face[1]) 37 | 38 | #3rd Line: "BOX {%xmin %ymin %zmin %xmax %ymax %zmax}" 39 | # Bounding Box (for some weird reason, this is in volume indicies 40 | # instead of model units!!) 41 | self.box = self._infile.readline().strip()[5:-1].split() 42 | self.box = [int(item) for item in self.box] 43 | 44 | #4th Line: "ORIENT {%xcomp %ycomp %zcomp}" 45 | # Orientation of ribs (?) 46 | self.orientation = self._infile.readline().strip()[9:-1].split() 47 | self.orientation = [float(item) for item in self.orientation] 48 | 49 | #5th Line: "COLOR %hexcode" 50 | self.color = self._infile.readline().strip()[6:] 51 | 52 | #6th Line: "TRANSPARENCY %value" 53 | self.transparency = self._infile.readline().strip()[13:] 54 | self.transparency = float(self.transparency) 55 | 56 | #Can't use self.box to set xmin, xmax, etc... 57 | # (self.box is in volume indicies instead of model units!) 58 | xmin = property(lambda self: self.points.x.min(), 59 | doc='Mininum X-coordinate (in inline/crossline)') 60 | ymin = property(lambda self: self.points.y.min(), 61 | doc='Mininum Y-coordinate (in inline/crossline)') 62 | zmin = property(lambda self: self.points.z.min(), 63 | doc='Mininum Z-coordinate (in inline/crossline)') 64 | xmax = property(lambda self: self.points.x.max(), 65 | doc='Maximum X-coordinate (in inline/crossline)') 66 | ymax = property(lambda self: self.points.y.max(), 67 | doc='Maximum Y-coordinate (in inline/crossline)') 68 | zmax = property(lambda self: self.points.z.max(), 69 | doc='Maximum Z-coordinate (in inline/crossline)') 70 | 71 | @property 72 | def ribs(self): 73 | """Reads points from a rib""" 74 | """Format looks like this: 75 | CURVE 76 | { 77 | SUBDIVISION 2 78 | VERTICES 79 | { 80 | { 2657.630371 5162.239258 2845.000000 }, 81 | { 2652.424072 5187.238281 2845.000488 }, 82 | { 2648.202637 5206.083496 2845.000488 }, 83 | { 2645.693604 5213.675781 2845.000488 }, 84 | } 85 | } 86 | """ 87 | self._readHeader() 88 | while True: 89 | # Make sure that the next line is "CURVE" 90 | line = self._infile.readline() 91 | if line == '': 92 | raise StopIteration #End of File 93 | elif line.strip() != 'CURVE': 94 | raise ValueError('Expected next line to be "CURVE",'\ 95 | ' got %s' % line.strip()) 96 | 97 | # Skip the next 4 lines, and store the 5th for processing 98 | for i in range(5): line = self._infile.readline() 99 | 100 | # Read points 101 | verts = [] 102 | while line.strip() is not '}': 103 | line = line.strip().split() 104 | verts.append( tuple([float(item) for item in line[1:4]]) ) 105 | line = self._infile.readline() 106 | # Read the other '}' 107 | line = self._infile.readline() 108 | 109 | yield verts 110 | 111 | @property 112 | def points(self): 113 | """Returns a numpy array of all points in the file""" 114 | # Have we already done this: 115 | try: 116 | return self._allPoints 117 | except AttributeError: 118 | dat = [] 119 | self._readHeader() 120 | for rib in self.ribs: 121 | dat.extend(rib) 122 | self._allPoints = np.rec.fromrecords(dat, names='x,y,z') 123 | return self._allPoints 124 | 125 | def strikeDip(self, vol=None, velocity=None): 126 | """ 127 | Returns a strike and dip of the ezfault following the Right-hand-rule. 128 | Input: 129 | vol (optional): A geoprobe volume object 130 | If specified, the x, y, and z units will be converted 131 | to world units based on the volume's header. 132 | velocity (optional): Velocity in meters/second 133 | If specified, the z units will be converted from time 134 | into depth using the velocity given. Assumes the z 135 | units are milliseconds!! 136 | Output: 137 | strike, dip 138 | """ 139 | return utilities.points2strikeDip(self.points.x, self.points.y, 140 | self.points.z, vol=vol, velocity=velocity) 141 | 142 | def interpolate(self, xi, yi): 143 | from scipy import interpolate 144 | # Need to write a norm function that calculates distance from a rib... 145 | 146 | """ 147 | def interp(x1,x2,x3, x1i, x2i): 148 | spline = interpolate.Rbf(x1, x2, x3, function='thin-plate', smooth=0) 149 | return spline(x1i,x2i) 150 | 151 | try: 152 | zi = interp(self.points.x, self.points.y, self.points.z, xi, yi) 153 | except np.linalg.linalg.LinAlgError: 154 | zi = interp(self.points.y, self.points.x, self.points.z, yi, xi) 155 | """ 156 | 157 | # Segfaults... Problems with the way scipy is compiled? 158 | tck = interpolate.bisplrep(self.points.x, self.points.y, self.points.z) 159 | zi = interpolate.bisplev(yi, xi, tck) 160 | 161 | 162 | """ 163 | spline = interpolate.Rbf(self.points.x, self.points.y, self.points.z, 164 | function='thin-plate', smooth=0) 165 | zi = spline(xi,yi) 166 | """ 167 | 168 | return zi 169 | 170 | 171 | 172 | def grid(self, dx=None, dy=None, extents=None, vol=None): 173 | 174 | if extents is None: 175 | xstart, xstop = self.xmin, self.xmax 176 | ystart, ystop = self.ymin, self.ymax 177 | else: 178 | xstart, xstop, ystart, ystop = extents 179 | 180 | if vol is not None: 181 | # Interpolate ezfault at volume indicies 182 | if type(vol) == type('String'): 183 | vol = volume(vol) 184 | dx, dy = abs(vol.dx), abs(vol.dy) 185 | 186 | # Make sure we start at a volume index 187 | xstart, xstop = [vol.index2model(vol.model2index(item)) 188 | for item in [xstart, xstop] ] 189 | ystart, ystop= [vol.index2model( 190 | vol.model2index(item, axis='y'), 191 | axis='y') 192 | for item in [ystart, ystop] ] 193 | 194 | else: 195 | if dx is None: dx = 1 196 | if dy is None: dy = dx 197 | 198 | X = np.arange(xstart, xstop, dx) 199 | Y = np.arange(ystart, ystop, dy) 200 | # Not needed w/ new way of interpolating? 201 | # X,Y = np.meshgrid(X,Y) 202 | grid = self.interpolate(X,Y) 203 | 204 | # grid = Z.reshape(X.shape) 205 | 206 | return grid 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /geoprobe/horizon.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os 3 | import six 4 | 5 | #-- Imports from local files -------------------------------------- 6 | from .volume import volume 7 | from .common import read_binary, write_binary 8 | 9 | #-- Build dtype for points ---------------------------------------- 10 | _point_format = ('>f', '>f', '>f', '>f', '>B', '>B', '>B') 11 | _point_names = ('x', 'y', 'z', 'conf', 'type', 'herid', 'tileSize') 12 | _point_dtype = list(zip(_point_names, _point_format)) 13 | 14 | class horizon(object): 15 | """ 16 | Reads and writes geoprobe horizon files 17 | 18 | horizon.x, horizon.y, and horizon.z are the x,y, and z coordinates 19 | stored in the horizon file (in model coordinates, i.e. inline/crossline). 20 | These are views into horizon.data, so any changes made to these will 21 | update horizon.data and vice versa. 22 | 23 | horizon.grid is a 2d numpy masked array of the z-values in the horizon 24 | masked in regions where there aren't any z-values. The extent of the grid 25 | is controlled by horizon.grid_extents (a tuple of xmin, xmax, ymin, ymax) 26 | which is inferred from the underlying x,y data if it is not specified. 27 | 28 | Useful attributes set at initialization: 29 | data: A structured numpy array with the fields 'x', 'y', 'z', 30 | 'conf', 'type', 'herid', and 'tileSize'. This array 31 | contains all the data stored in the horizon file. 32 | surface: A view into horizon.data that contains only the points 33 | in the file that make up a "filled" surface. (Usually points 34 | interpolated between manual picks). 35 | lines: A list of manual picks stored in the horizon file. Each item 36 | in the list is a tuple of a) a 4-tuple of line information 37 | (xdir, ydir, zdir, ID) and b) a view into horizon.data 38 | containing the points in the line. 39 | """ 40 | def __init__(self, *args, **kwargs): 41 | """ 42 | Takes either a filename or a numpy array 43 | 44 | If a single string is given as input, the string is assumed to be a 45 | filename, and a new horizon object is constructed by reading the file 46 | from disk. 47 | 48 | Otherwise, a horizon object can be created from existing data, as 49 | described below: 50 | 51 | If one input argument is given, it is assumed to be a structured 52 | numpy array and is used as the filled surface portion of the 53 | horizon. The input array must be convertable into an array 54 | with a dtype of horizon.POINT_DTYPE. 55 | If two input arguments are given, the first is used as the filled 56 | surface portion of the horizon file and the second a list of lines 57 | in the same format as horizon.lines. The arrays must be convertable 58 | into an array with dtype horizon.POINT_DTYPE 59 | If three input arguments are given, they are assumed to be lists/arrays 60 | of x, y, and z coordinates (respectively) of the points in the new 61 | horizon. 62 | Alternatively, you may specify these options using the following 63 | keyword arguments: surface, lines, or x, y, and z. 64 | 65 | For example, a horizon object can be initalized in the following ways: 66 | h = geoprobe.horizon('/path/to/file') 67 | h = geoprobe.horizon(data) 68 | h = geoprobe.horizon(surface, lines) 69 | h = geoprobe.horizon(x,y,z) 70 | h = geoprobe.horizon(x=x, y=y, z=z) 71 | h = geoprobe.horizon(lines=a_list_of_lines) 72 | h = geoprobe.horizon(surface=surface_array) 73 | h = geoprobe.horizon(surface=surface, lines=lines) 74 | """ 75 | 76 | # If __init__ is just passed a string, assume it's a filename 77 | # and make a horizon object by reading from disk 78 | if (len(args) == 1) and isinstance(args[0], six.string_types): 79 | self._readHorizon(args[0]) 80 | 81 | # Otherwise, pass the args on to _make_horizon_from_data for 82 | # parsing 83 | else: 84 | self._parse_new_horizon_input(*args, **kwargs) 85 | 86 | # For gridding: 87 | self.nodata = -9999 88 | 89 | # Need to make dx, dy, and dz properties... 90 | # How do we determine spacing without a volume? 91 | # d = np.abs(np.diff(self.x)); np.mean(d[d!=0]) (ideally, mode)? 92 | 93 | # Adding this constant so that the "correct" dtype is visible before a 94 | # horizon object is initialized 95 | POINT_DTYPE = _point_dtype 96 | 97 | def _readHorizon(self, filename): 98 | """Reads a horizon from disk""" 99 | self._file = HorizonFile(filename, 'rb') 100 | self._header = self._file.readHeader() 101 | 102 | if self._header == b"#GeoProbe Horizon V2.0 ascii\n": 103 | raise TypeError('Ascii horizons not currently supported') 104 | elif self._header != b"#GeoProbe Horizon V2.0 binary\n": 105 | raise TypeError('This does not appear to be a valid geoprobe'\ 106 | ' horizon') 107 | 108 | self.data = self._file.readAll() 109 | 110 | # Surface and line attributes 111 | self.surface = self._file.surface 112 | self.lines = self._file.lines 113 | 114 | # Oddly enough, Geoprobe (the actual Landmark application) seems to 115 | # do this a lot... 116 | # Raise the error here to avoid problems down the road! 117 | if self.data.size == 0: 118 | raise ValueError('This file does not contain any points!') 119 | 120 | def _parse_new_horizon_input(self, *args, **kwargs): 121 | """Parse input when given something other than a filename""" 122 | 123 | #-- Parse Arguments --------------------------------------------------- 124 | if len(args) == 1: 125 | # Assume argument is data (numpy array with dtype of _point_dtype) 126 | self.data = self._ensure_correct_dtype(args[0]) 127 | self.surface = self.data 128 | 129 | elif len(args) == 2: 130 | # Assume arguments are surface + lines 131 | self._init_from_surface_lines(self, surface=args[0], lines=args[1]) 132 | 133 | elif len(args) == 3: 134 | # Assume arguments are x, y, and z arrays 135 | self._init_from_xyz(*args) 136 | 137 | #-- Parse keyword arguments ------------------------------------------- 138 | elif ('x' in kwargs) and ('y' in kwargs) and ('z' in kwargs): 139 | self._init_from_xyz(kwargs['x'], kwargs['y'], kwargs['z']) 140 | 141 | elif ('surface' in kwargs) or ('lines' in kwargs): 142 | surface = kwargs.pop('surface', None) 143 | lines = kwargs.pop('lines', None) 144 | self._init_from_surface_lines(surface, lines) 145 | 146 | else: 147 | raise ValueError('Invalid arguments. You must specify one of:'\ 148 | ' x,y,&z, surface, or lines') 149 | 150 | def _ensure_correct_dtype(self, data): 151 | """Converts data into the proper dtype for points and raises a useful 152 | error message if it fails""" 153 | try: 154 | data = np.asarray(data, dtype=self.POINT_DTYPE) 155 | except TypeError: 156 | raise TypeError('The input data cannot be converted into an array' 157 | ' with dtype=%s' % repr(self.POINT_DTYPE)) 158 | return data 159 | 160 | def _init_from_xyz(self, x, y, z): 161 | """Make a new horizon object from x, y, and z arrays""" 162 | x,y,z = [np.asarray(item, dtype=np.float32) for item in [x,y,z]] 163 | if x.size == y.size == z.size: 164 | self.data = np.zeros(x.size, dtype=self.POINT_DTYPE) 165 | self.x = x 166 | self.y = y 167 | self.z = z 168 | self.surface = self.data 169 | else: 170 | raise ValueError('x, y, and z arrays must be the same length') 171 | 172 | def _init_from_surface_lines(self, surface=None, lines=None): 173 | """ 174 | Make a new horizon object from either a surface array or a list of 175 | line arrays 176 | """ 177 | if surface is not None: 178 | surface = self._ensure_correct_dtype(surface) 179 | 180 | # Calculate total number of points 181 | numpoints = surface.size if surface else 0 182 | if lines is not None: 183 | for info, line in lines: 184 | numpoints += line.size 185 | 186 | # Make self.data and make self.lines & self.surface views into self.data 187 | self.data = np.zeros(numpoints, dtype=self.POINT_DTYPE) 188 | 189 | array_list = [] 190 | if surface is not None: 191 | array_list.append((None, surface)) 192 | if lines is not None: 193 | array_list.extend(lines) 194 | 195 | i = 0 196 | self.lines = [] 197 | for info, item in array_list: 198 | self.data[i:i+item.size] = item 199 | if (surface is not None) and (i == 0): 200 | self.surface = self.data[i:i+item.size] 201 | else: 202 | self.lines.append((info, self.data[i:i+item.size])) 203 | i += item.size 204 | 205 | def write(self, filename): 206 | """ 207 | Write the horizon to a new file ("filename") 208 | """ 209 | self._file = HorizonFile(filename, 'wb') 210 | 211 | # If self.lines isn't set, default to [] 212 | try: 213 | self._file.lines = self.lines 214 | except AttributeError: 215 | self._file.lines = [] 216 | 217 | # If self.surface isn't set, default to an empty numpy array 218 | try: 219 | self._file.surface = self.surface 220 | except AttributeError: 221 | self._file.surface = np.zeros(0, dtype=self.POINT_DTYPE) 222 | 223 | self._file.writeAll() 224 | 225 | @property 226 | def numpoints(self): 227 | """The total number of points stored in the horizon 228 | (equivalent to horizon.data.size)""" 229 | return self.data.size 230 | 231 | #-- xmin, xmax, etc properties -------------------------------------------- 232 | xmin = property(lambda self: self.x.min(), doc='Mininum X-coordinate') 233 | ymin = property(lambda self: self.y.min(), doc='Mininum Y-coordinate') 234 | zmin = property(lambda self: self.z.min(), doc='Mininum Z-coordinate') 235 | xmax = property(lambda self: self.x.max(), doc='Maximum X-coordinate') 236 | ymax = property(lambda self: self.y.max(), doc='Maximum Y-coordinate') 237 | zmax = property(lambda self: self.z.max(), doc='Maximum Z-coordinate') 238 | #-------------------------------------------------------------------------- 239 | 240 | #-- x,y,z properties ------------------------------------------------------ 241 | def _get_coord(self, name): 242 | return self.data[name] 243 | def _set_coord(self, name, value): 244 | self.data[name] = value 245 | x = property(lambda self: self._get_coord('x'), 246 | lambda self, value: self._set_coord('x', value), 247 | doc='X-coordinates of all points stored in the horizon') 248 | y = property(lambda self: self._get_coord('y'), 249 | lambda self, value: self._set_coord('y', value), 250 | doc='Y-coordinates of all points stored in the horizon') 251 | z = property(lambda self: self._get_coord('z'), 252 | lambda self, value: self._set_coord('z', value), 253 | doc='Z-coordinates of all points stored in the horizon') 254 | #-------------------------------------------------------------------------- 255 | 256 | #-- Grid Extents Property ------------------------------------------------- 257 | def _get_grid_extents(self): 258 | """A tuple of (xmin, ymin, xmax, ymax) indicating the extent (in model 259 | coordinates) of self.grid. This is inferred from the extents of the 260 | horizon's data unless it is manually set, in which case the self.grid 261 | will cover the indicated area.""" 262 | try: 263 | return self._grid_extents 264 | except AttributeError: 265 | self._grid_extents = (self.xmin, self.xmax, self.ymin, self.ymax) 266 | return self._grid_extents 267 | def _set_grid_extents(self, value): 268 | xmin, xmax, ymin, ymax = value 269 | if (xmin > xmax) or (ymin > ymax): 270 | raise ValueError('Grid extents must be (xmin, xmax, ymin, ymax)') 271 | self._grid_extents = value 272 | # Delete the cache of self.grid, as it will now be invalid. 273 | try: 274 | del self._grid 275 | except AttributeError: 276 | pass 277 | grid_extents = property(_get_grid_extents, _set_grid_extents) 278 | #-------------------------------------------------------------------------- 279 | 280 | #-- Grid Property --------------------------------------------------------- 281 | def _get_grid(self): 282 | """An nx by ny numpy array (dtype=float32) of the z values contained 283 | in the horizon file""" 284 | try: 285 | return self._grid 286 | except AttributeError: 287 | x, y, z = self.x, self.y, self.z 288 | xmin, xmax, ymin, ymax = self.grid_extents 289 | ny, nx = int(ymax - ymin + 1), int(xmax - xmin + 1) 290 | grid = self.nodata * np.ones((ny, nx), dtype=np.float32) 291 | I = np.array(x - xmin, dtype=np.int) 292 | J = np.array(y - ymin, dtype=np.int) 293 | inside_extents = (I >= 0) & (I < nx) & (J >= 0) & (J < ny) 294 | I = I[inside_extents] 295 | J = J[inside_extents] 296 | grid[J,I] = z[inside_extents] 297 | grid = np.ma.masked_values(grid, self.nodata, copy=False) 298 | grid.fill_value = self.nodata 299 | self._grid = grid 300 | return self._grid 301 | def _set_grid(self, value): 302 | self._grid = np.ma.asarray(value) 303 | grid = property(_get_grid, _set_grid) 304 | #-------------------------------------------------------------------------- 305 | 306 | @property 307 | def name(self): 308 | try: 309 | _file = self._file 310 | except AttributeError: 311 | return '' 312 | basedir, basename = os.path.split(_file.name) 313 | if basename.endswith('.hzn'): 314 | return basename[:-4] 315 | else: 316 | return basename 317 | 318 | def strikeDip(self, vol=None, velocity=None): 319 | """ 320 | Returns a strike and dip of the horizon following the Right-hand-rule. 321 | Input: 322 | vol (optional): A geoprobe volume object 323 | If specified, the x, y, and z units will be converted 324 | to world units based on the volume's header. 325 | velocity (optional): Velocity in meters/second 326 | If specified, the z units will be converted from time 327 | into depth using the velocity given. Assumes the z 328 | units are milliseconds!! 329 | Output: 330 | strike, dip 331 | """ 332 | # Delayed import to avoid circular dependency 333 | from .utilities import points2strikeDip 334 | return points2strikeDip(self.x, self.y, self.z, 335 | vol=vol, velocity=velocity) 336 | 337 | def toGeotiff(self, filename, vol=None, nodata=None, zscale=None): 338 | """ 339 | Create and write a geotiff file from the geoprobe horizon object. 340 | The Z values in the output tiff will be stored as 32bit floats. 341 | Input: 342 | filename: Output filename 343 | vol (optional): A geoprobe volume object or path to a geoprobe 344 | volume file. If vol is specified, the geotiff will be 345 | georeferenced based on the data in the volume header (and will 346 | therefore be in same projection as the volume's world 347 | coordinates). Otherwise the geotiff is created using the model 348 | coordinates stored in the geoprobe horizon file. 349 | nodata (default=self.nodata (-9999)): Value to use for NoData. 350 | zscale (optional): Scaling factor to use for the Z-values. If vol 351 | is specified, and vol.dz is negative, this defaults to -1. 352 | Otherwise this defaults to 1. 353 | """ 354 | # Delayed import to avoid circular dependency 355 | from .utilities import array2geotiff 356 | 357 | if vol is not None: 358 | if type(vol) == type('string'): 359 | vol = volume(vol) 360 | Xoffset, Yoffset = vol.model2world(self.xmin, self.ymin) 361 | transform = vol 362 | else: 363 | Xoffset, Yoffset = 0,0 364 | transform = None 365 | 366 | if nodata==None: 367 | nodata = self.nodata 368 | 369 | # Zscale is not 1 by default, as I want the default to be set by vol.dz 370 | # and any specified value to override the default 371 | if zscale is None: 372 | if vol is None: zscale = 1 373 | elif vol.dz > 0: zscale = 1 374 | elif vol.dz < 0: zscale = -1 375 | 376 | data = self.grid 377 | data.fill_value = nodata 378 | data *= zscale 379 | data = data.filled() 380 | 381 | array2geotiff(data, filename, nodata=nodata, 382 | extents=(Xoffset, Yoffset), transform=transform) 383 | 384 | 385 | #-- This is currently very sloppy code... Need to clean up and document 386 | class HorizonFile(object): 387 | """Basic geoprobe horizon binary file format reader 388 | 389 | Disk layout of Geoprobe horizons 390 | Reverse engineered by JDK, Feb. 2009 391 | Format descrip: 392 | 1 ascii line w/ version (terminated with newline) 393 | There are two "sections" in every file. 394 | The first section contains x,y,z points making a "filled" surface 395 | (This is basically a sparse matrix) 396 | The second section contains lines (manual picks) 397 | Both section types have a 4 byte header (seems to be >I?) 398 | The first section (surface) always (?) has a section header 399 | value of '\x00\x00\x00\x13' (unpacks to 19) 400 | The section section (manual picks) contains the number of 401 | manually picked lines in the file (packed as >I). 402 | Subsections 403 | The first section only has one subsection, a "filled" surface 404 | Surface header: (>I) Number of points in the surface 405 | The second section contains "numlines" subsections containing 406 | manual picks (lines): 407 | Line header: (>4f) xdir,ydir,zdir,ID 408 | Point Format in all sections: (>4f3B) 409 | x,y,z,confidence,type,heridity,tileSize 410 | """ 411 | _sectionHdrFmt = '>I' 412 | _surfaceHdrFmt = '>I' 413 | _lineHdrFmt = '>4f' 414 | 415 | def __init__(self, *args, **kwargs): 416 | """Accepts the same argument set as a standard python file object""" 417 | # Initalize the file object as normal 418 | self._file = open(*args, **kwargs) 419 | 420 | def readHeader(self): 421 | self._file.seek(0) 422 | return self._file.readline() 423 | 424 | def readPoints(self): 425 | numPoints = read_binary(self._file, self._surfaceHdrFmt) 426 | points = np.fromfile(self._file, count=numPoints, dtype=_point_dtype) 427 | return points 428 | 429 | def readSectionHeader(self): 430 | return read_binary(self._file, self._sectionHdrFmt) 431 | 432 | def readLineHeader(self): 433 | # TODO: Change this to a numpy array 434 | xdir,ydir,zdir,ID = read_binary(self._file, self._lineHdrFmt) 435 | return xdir, ydir, zdir, ID 436 | 437 | def readAll(self): 438 | """ 439 | Reads in the entire horizon file and returns a numpy array with the 440 | fields ('x', 'y', 'z', 'conf', 'type', 'herid', 'tileSize') for each 441 | point in the horizon. 442 | """ 443 | # Note: The total number of points in the file is not directly stored 444 | # on disk. Therefore, we must read through the entire file, store 445 | # each section's points in a list, and then create a contigious array 446 | # from them. Using numpy.append is much simpler, but quite slow. 447 | 448 | # Jump to start of file, past header 449 | self.readHeader() 450 | 451 | # Read points section 452 | self.readSectionHeader() # Should always return 19 453 | surface = self.readPoints() 454 | temp_points = [surface] 455 | 456 | # Read lines section 457 | line_info = [None] 458 | self.numlines = self.readSectionHeader() 459 | for i in six.moves.range(self.numlines): 460 | line_info.append(self.readLineHeader()) 461 | currentPoints = self.readPoints() 462 | temp_points.append(currentPoints) 463 | 464 | # Create a single numpy array from the list of arrays (temp_points) 465 | numpoints = sum(map(np.size, temp_points)) 466 | points = np.zeros(numpoints, dtype=_point_dtype) 467 | self.lines = [] 468 | i = 0 469 | for info, item in zip(line_info, temp_points): 470 | points[i : i + item.size] = item 471 | # self.surface is a view into the first part of the points array 472 | if i == 0: 473 | self.surface = points[i:i+item.size] 474 | # self.lines is a list of tuples, the first item is a tuple of 475 | # (xdir,ydir,zdir,ID) where form a vector in 476 | # the direction of the line. The second item is a view into the 477 | # points array containg the relevant x,y,z,etc points. 478 | else: 479 | self.lines.append((info, points[i:i+item.size])) 480 | i += item.size 481 | 482 | return points 483 | 484 | def writeHeader(self): 485 | header = "#GeoProbe Horizon V2.0 binary\n" 486 | self._file.seek(0) 487 | self._file.write(header) 488 | 489 | def writePoints(self, points): 490 | numPoints = points.size 491 | write_binary(self._file, self._surfaceHdrFmt, numPoints) 492 | points.tofile(self._file) 493 | 494 | def writeLineHeader(self, line_hdr): 495 | write_binary(self._file, self._lineHdrFmt, line_hdr) 496 | 497 | def writeSectionHeader(self, sec_hdr): 498 | write_binary(self._file, self._sectionHdrFmt, sec_hdr) 499 | 500 | def writeAll(self): 501 | self.writeHeader() 502 | self.writeSectionHeader(19) 503 | self.writePoints(self.surface) 504 | self.writeSectionHeader(len(self.lines)) 505 | for (info, line) in self.lines: 506 | self.writeLineHeader(info) 507 | self.writePoints(line) 508 | 509 | 510 | 511 | -------------------------------------------------------------------------------- /geoprobe/swfault.py: -------------------------------------------------------------------------------- 1 | import six 2 | import numpy as np 3 | import xml.etree.ElementTree as et 4 | import xml.dom.minidom as minidom 5 | from . import utilities 6 | # Note: matplotlib.delaunay is required for interpolation and triangulation. 7 | # If it isn't available 8 | 9 | class swfault(object): 10 | def __init__(self, arg): 11 | if isinstance(arg, six.string_types): 12 | # Assume it's a filename 13 | self._read(arg) 14 | else: 15 | # Assume it's a sequence of segments 16 | self._init_from_data(arg) 17 | self.dx = 1.0 18 | self.dy = 1.0 19 | 20 | def _read(self, filename): 21 | with open(filename, 'r') as infile: 22 | reader = SwfaultXMLReader(infile) 23 | self.segments = reader.segments 24 | self.color = reader.color 25 | self.name = reader.name 26 | self.linewidth = reader.linewidth 27 | self.seisworks_ids = reader.seisworks_ids 28 | 29 | def write(self, filename): 30 | with open(filename, 'w') as outfile: 31 | writer = SwfaultXMLWriter(self) 32 | writer.write(outfile) 33 | 34 | def _init_from_data(self, segments, name='Undefined'): 35 | self.segments = segments 36 | self.name = name 37 | self.color = (1, 0, 0, 1) 38 | self.linewidth = 3.0 39 | self.seisworks_ids = ['2147483647'] * len(segments) 40 | 41 | def _get_coord(self, axis): 42 | try: 43 | return self.xyz[:,axis] 44 | except IndexError: 45 | return np.array([]) 46 | def _set_coord(self, value, axis): 47 | self.xyz[:,axis] = value 48 | x = property(lambda self: self._get_coord(0), 49 | lambda self, value: self._set_coord(value, 0)) 50 | y = property(lambda self: self._get_coord(1), 51 | lambda self, value: self._set_coord(value, 1)) 52 | z = property(lambda self: self._get_coord(2), 53 | lambda self, value: self._set_coord(value, 2)) 54 | 55 | #-- Bounding values... ---------------------------------------------------- 56 | xmin = property(lambda self: self.x.min(), 57 | doc='Mininum X-coordinate (in inline/crossline)') 58 | ymin = property(lambda self: self.y.min(), 59 | doc='Mininum Y-coordinate (in inline/crossline)') 60 | zmin = property(lambda self: self.z.min(), 61 | doc='Mininum Z-coordinate (in inline/crossline)') 62 | xmax = property(lambda self: self.x.max(), 63 | doc='Maximum X-coordinate (in inline/crossline)') 64 | ymax = property(lambda self: self.y.max(), 65 | doc='Maximum Y-coordinate (in inline/crossline)') 66 | zmax = property(lambda self: self.z.max(), 67 | doc='Maximum Z-coordinate (in inline/crossline)') 68 | 69 | @property 70 | def grid_extents(self): 71 | try: 72 | return self._grid_extents 73 | except AttributeError: 74 | return (self.xmin, self.xmax, self.ymin, self.ymax) 75 | @grid_extents.setter 76 | def grid_extents(self, value): 77 | xmin, xmax, ymin, ymax = value 78 | self._grid_extents = (xmin, xmax, ymin, ymax) 79 | 80 | @property 81 | def segments(self): 82 | return [self.xyz[item] for item in self._indices] 83 | @segments.setter 84 | def segments(self, value): 85 | def sequence(item): 86 | length = len(item) 87 | sli = slice(sequence.i, sequence.i + length) 88 | sequence.i += length 89 | return sli 90 | sequence.i = 0 91 | self.xyz = np.array([item for segment in value for item in segment]) 92 | self._indices = [sequence(item) for item in value] 93 | 94 | @property 95 | def tri(self): 96 | try: 97 | from matplotlib.delaunay import Triangulation 98 | except ImportError: 99 | raise ImportError('Maplotlib is required for geoprobe.swfault ' 100 | 'triangulation') 101 | try: 102 | return self._tri 103 | except AttributeError: 104 | x, y, z = self._internal_xyz.T 105 | self._tri = Triangulation(x, y) 106 | self._tri.x = self.x 107 | self._tri.y = self.y 108 | return self._tri 109 | 110 | @property 111 | def interp(self): 112 | try: 113 | from matplotlib.delaunay import LinearInterpolator 114 | except ImportError: 115 | raise ImportError('Maplotlib is required for geoprobe.swfault ' 116 | 'interpolation') 117 | try: 118 | return self._interp 119 | except AttributeError: 120 | self._interp = LinearInterpolator(self.tri, self.z) 121 | return self._interp 122 | 123 | @property 124 | def grid(self): 125 | try: 126 | return self._grid 127 | except AttributeError: 128 | xmin, xmax, ymin, ymax = self.grid_extents 129 | #nx, ny = int((xmax-xmin) / self.dx), int((ymax-ymin) / self.dy) 130 | nx, ny = int((xmax-xmin) / self.dx)+1, int((ymax-ymin) / self.dy)+1 131 | self._grid = self.interp[ymin:ymax:ny*1j, xmin:xmax:nx*1j] 132 | return self._grid 133 | 134 | @property 135 | def _internal_xyz(self): 136 | try: 137 | return self._internal_xyz_data 138 | except: 139 | vecs, vals = utilities.principal_axes(self.x, self.y, self.z, True) 140 | rotated = self.xyz.dot(vecs) 141 | rotated -= rotated.mean(axis=0) 142 | rotated /= np.sqrt(vals) 143 | self._internal_xyz_data = rotated 144 | return rotated 145 | 146 | @property 147 | def _outline_order(self): 148 | rotated_segments = [self._internal_xyz[item] for item in self._indices] 149 | xyz = np.array(self._segment_endpoints(rotated_segments)) 150 | x, y, z = xyz.T 151 | 152 | theta = np.arctan2(y, x) 153 | order = theta.argsort() 154 | return order 155 | 156 | def _segment_endpoints(self, segments): 157 | return [seg[0] for seg in segments] + [seg[-1] for seg in segments] 158 | 159 | @property 160 | def outline(self): 161 | try: 162 | return self._outline 163 | except: 164 | xyz = np.array(self._segment_endpoints(self.segments)) 165 | outline = xyz[self._outline_order] 166 | return np.squeeze(outline) 167 | 168 | @property 169 | def _rotated_outline(self): 170 | rotated_segments = [self._internal_xyz[item] for item in self._indices] 171 | xyz = np.array(self._segment_endpoints(rotated_segments)) 172 | return np.squeeze(xyz[self._outline_order]) 173 | 174 | class SwfaultXMLReader(object): 175 | def __init__(self, f): 176 | data = f.read() 177 | try: 178 | self.tree = et.fromstring(data) 179 | except et.ParseError: 180 | # Stupid geoprobe produces invalid xml... 181 | self.tree = et.fromstring(data + '\n') 182 | self._parse_xml_tree() 183 | 184 | def _parse_xml_tree(self): 185 | def get_xyz(element): 186 | coords = [element.findall('item/'+key) for key in ['x','y','z']] 187 | xyz = zip(*[[float(coord.text) for coord in item] for item in coords]) 188 | return list(xyz) 189 | 190 | fault = self.tree.find('swFault') 191 | self.name = fault.find('name').text 192 | color_fields = ['lineColor_' + item for item in ('r', 'g', 'b', 'a')] 193 | self.color = [float(fault.find(item).text) for item in color_fields] 194 | self.linewidth = float(fault.find('lineWidth').text) 195 | 196 | self.segments = [get_xyz(elem) for elem in 197 | fault.findall('segments/item/Project_Coordinates')] 198 | 199 | self.seisworks_ids = [item.find('SWSegID').text for item in 200 | fault.findall('segments/item')] 201 | 202 | class SwfaultXMLWriter(object): 203 | def __init__(self, parent): 204 | self.parent = parent 205 | 206 | self.setup_document() 207 | self.write_metadata() 208 | self.write_all_segments() 209 | 210 | def write(self, f): 211 | f.write(self.doc.toprettyxml(indent=' ')) 212 | 213 | def setup_document(self): 214 | imp = minidom.getDOMImplementation() 215 | doctype = imp.createDocumentType('boost_serialization', '', '') 216 | self.doc = imp.createDocument(None, 'boost_serialization', doctype) 217 | self.doc.documentElement.setAttribute('signature', 'serialization::archive') 218 | self.doc.documentElement.setAttribute('version', '3') 219 | 220 | self.root = self.add_node(self.doc.documentElement, 'swFault', None, 221 | dict(class_id='0', tracking_level='0', version='3')) 222 | 223 | def write_metadata(self): 224 | self.add_node(self.root, 'name', self.parent.name) 225 | color_fields = ['lineColor_' + item for item in ('r', 'g', 'b', 'a')] 226 | for color, field in zip(self.parent.color, color_fields): 227 | self.add_node(self.root, field, repr(color)) 228 | self.add_node(self.root, 'lineWidth', repr(self.parent.linewidth)) 229 | 230 | def write_all_segments(self): 231 | segments = self.add_node(self.root, 'segments', None, 232 | dict(class_id='1', tracking_level='0', version='0')) 233 | self.add_node(segments, 'count', repr(len(self.parent.segments))) 234 | 235 | items = zip(self.parent.segments, self.parent.seisworks_ids) 236 | for i, (seg, sw_id) in enumerate(items): 237 | self.write_segment(i, seg, sw_id, segments) 238 | 239 | def write_segment(self, i, seg, sw_id, segment_node): 240 | # First items are different than the rest... 241 | if i == 0: 242 | item_attrs = dict(class_id='2', tracking_level='1', version='2', object_id='_0') 243 | coords_attrs = dict(class_id='3', tracking_level='0', version='0') 244 | else: 245 | item_attrs = dict(class_id_reference='2', object_id='_{}'.format(i)) 246 | coords_attrs = {} 247 | 248 | item = self.add_node(segment_node, 'item', None, item_attrs) 249 | self.add_node(item, 'SWSegID', sw_id) 250 | coords = self.add_node(item, 'Project_Coordinates', None, coords_attrs) 251 | self.add_node(coords, 'count', repr(len(seg))) 252 | 253 | firstrun = True 254 | for x, y, z in seg: 255 | if i == 0 and firstrun: 256 | point_attrs = dict(class_id='4', tracking_level='0', version='0') 257 | firstrun = False 258 | else: 259 | point_attrs = {} 260 | point = self.add_node(coords, 'item', None, point_attrs) 261 | for name, value in zip(['x', 'y', 'z'], [x, y, z]): 262 | self.add_node(point, name, repr(value)) 263 | 264 | def add_node(self, root, name, text=None, attributes=None): 265 | """Create and append node "name" to the node "root".""" 266 | node = self.doc.createElement(name) 267 | if text is not None: 268 | text = self.doc.createTextNode(text) 269 | node.appendChild(text) 270 | if attributes is not None: 271 | for key, value in six.iteritems(attributes): 272 | node.setAttribute(key, value) 273 | root.appendChild(node) 274 | return node 275 | -------------------------------------------------------------------------------- /geoprobe/tsurf.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | class tsurf(object): 4 | default_name = 'Undefined' 5 | default_color = (0, 1, 1, 1.0) 6 | def __init__(self, *args, **kwargs): 7 | """Accepts either a single filename or 4 arguments: x, y, z, triangles. 8 | keyword arguments are: "color" and "name" 9 | 10 | If a filename is given, the tsurf is read from the file. 11 | 12 | Otherwise: 13 | x, y, z are sequences of the x, y, and z coordinates of the vertices. 14 | triangles is a sequence of the indicies of the coord arrays making up 15 | each triangle in the mesh. E.g. [[0, 1, 2], [2, 1, 3], ...]""" 16 | if len(args) == 1: 17 | self._read_tsurf(args[0]) 18 | elif len(args) == 4: 19 | self._init_from_xyz(*args) 20 | else: 21 | raise ValueError('Invalid input arguments') 22 | color = kwargs.get('color', None) 23 | name = kwargs.get('name', None) 24 | if color is not None: 25 | self.color = color 26 | if name is not None: 27 | self.name = name 28 | self.header['name'] = self.name 29 | self.header['color'] = self.color 30 | 31 | def _read_tsurf(self, filename): 32 | with open(filename, 'r') as infile: 33 | firstline = next(infile).strip() 34 | if not firstline.startswith('GOCAD TSurf'): 35 | raise IOError('This is not a valid TSurf file!') 36 | 37 | # Parse Header 38 | self.header = {} 39 | line = next(infile).strip() 40 | if line.startswith('HEADER'): 41 | line = next(infile).strip() 42 | while '}' not in line: 43 | key, value = line.split(':') 44 | self.header[key.lstrip('*')] = value 45 | line = next(infile).strip() 46 | self.name = self.header.get('name', filename) 47 | try: 48 | self.color = [float(item) for item in self.header['color'].split()] 49 | self.color = tuple(self.color) 50 | except KeyError: 51 | self.color = self.default_color 52 | 53 | # Read vertices and triangles 54 | if not next(infile).startswith('TFACE'): 55 | raise IOError('Only "TFACE" format TSurf files are supported') 56 | self.vertices, self.triangles = [], [] 57 | for line in infile: 58 | line = line.strip().split() 59 | if line[0] == 'VRTX': 60 | self.vertices.append([float(item) for item in line[2:]]) 61 | elif line[0] == 'TRGL': 62 | self.triangles.append([int(item)-1 for item in line[1:]]) 63 | self.x, self.y, self.z = zip(*self.vertices) 64 | 65 | def _init_from_xyz(self, x, y, z, triangles): 66 | self.vertices = list(zip(x, y, z)) 67 | self.x, self.y, self.z = x, y, z 68 | self.triangles = triangles 69 | self.color = self.default_color 70 | self.name = self.default_name 71 | self.header = {'moveAs':'2', 'drawAs':'2', 'line':'3', 72 | 'clip':'0', 'intersect':'0', 'intercolor':' 1 0 0 1'} 73 | 74 | def write(self, outname): 75 | with open(outname, 'w') as outfile: 76 | # Write Header... 77 | outfile.write('GOCAD TSurf 1\n') 78 | outfile.write('HEADER {\n') 79 | """ 80 | for key in ['name', 'color', 'moveAs', 'drawAs', 'line', 'clip', 81 | 'intersect', 'intercolor']: 82 | value = self.header[key] 83 | """ 84 | for key, value in six.iteritems(self.header): 85 | if not isinstance(value, six.string_types): 86 | try: 87 | value = ' '.join(repr(item) for item in value) 88 | except TypeError: 89 | value = repr(item) 90 | outfile.write('*{}:{}\n'.format(key, value)) 91 | outfile.write('}\n') 92 | 93 | # Write data... 94 | outfile.write('TFACE\n') 95 | for i, (x, y, z) in enumerate(self.vertices, start=1): 96 | template = '\t'.join(['VRTX {}'] + 3*['{: >9.3f}']) + '\n' 97 | outfile.write(template.format(i, x, y, z)) 98 | for a, b, c in self.triangles: 99 | outfile.write('TRGL {} {} {}\n'.format(a+1, b+1, c+1)) 100 | outfile.write('END\n') 101 | 102 | 103 | -------------------------------------------------------------------------------- /geoprobe/utilities.py: -------------------------------------------------------------------------------- 1 | """Various utilities using the geoprobe format api's 2 | 07/2009""" 3 | 4 | import numpy as np 5 | import six 6 | 7 | from . import volume 8 | from . import horizon 9 | 10 | def bbox_intersects(bbox1, bbox2): 11 | """ 12 | Checks whether two bounding boxes overlap or touch. 13 | Input: 14 | bbox1: 4-tuple of (xmin, xmax, ymin, ymax) for the first region 15 | bbox2: 4-tuple of (xmin, xmax, ymin, ymax) for the second region 16 | Output: 17 | boolean True/False 18 | """ 19 | # Check for intersection 20 | xmin1, xmax1, ymin1, ymax1 = bbox1 21 | xmin2, xmax2, ymin2, ymax2 = bbox2 22 | xdist = abs( (xmin1 + xmax1) / 2.0 - (xmin2 + xmax2) / 2.0 ) 23 | ydist = abs( (ymin1 + ymax1) / 2.0 - (ymin2 + ymax2) / 2.0 ) 24 | xwidth = (xmax1 - xmin1 + xmax2 - xmin2) / 2.0 25 | ywidth = (ymax1 - ymin1 + ymax2 - ymin2) / 2.0 26 | return (xdist <= xwidth) and (ydist <= ywidth) 27 | 28 | def bbox_intersection(bbox1, bbox2): 29 | """ 30 | Extract the intersection of two bounding boxes. 31 | Input: 32 | bbox1: 4-tuple of (xmin, xmax, ymin, ymax) for the first region 33 | bbox2: 4-tuple of (xmin, xmax, ymin, ymax) for the second region 34 | Output: 35 | Returns a 4-tuple of (xmin, xmax, ymin, ymax) for overlap between 36 | the two input bounding-box regions or None if they are disjoint. 37 | """ 38 | if not bbox_intersects(bbox1, bbox2): 39 | return None 40 | 41 | output = [] 42 | for i, comparison in enumerate(zip(bbox1, bbox2)): 43 | # mininum x or y coordinate 44 | if i % 2 == 0: 45 | output.append(max(comparison)) 46 | # maximum x or y coordinate 47 | elif i % 2 == 1: 48 | output.append(min(comparison)) 49 | return output 50 | 51 | def bbox_union(bbox1, bbox2): 52 | """ 53 | Extract the union of two bounding boxes. 54 | Input: 55 | bbox1: 4-tuple of (xmin, xmax, ymin, ymax) for the first region 56 | bbox2: 4-tuple of (xmin, xmax, ymin, ymax) for the second region 57 | Output: 58 | Returns a 4-tuple of (xmin, xmax, ymin, ymax) 59 | """ 60 | output = [] 61 | for i, comparison in enumerate(zip(bbox1, bbox2)): 62 | # mininum x or y coordinate 63 | if i % 2 == 0: 64 | output.append(min(comparison)) 65 | # maximum x or y coordinate 66 | elif i % 2 == 1: 67 | output.append(max(comparison)) 68 | return output 69 | 70 | def create_isopach(hor1, hor2, extent='intersection'): 71 | """Create new horizon with the difference between hor1 and hor2.""" 72 | if isinstance(extent, six.string_types): 73 | if extent.lower() == 'union': 74 | extent = bbox_union(hor1.grid_extents, hor2.grid_extents) 75 | elif extent.lower() == 'intersection': 76 | extent = bbox_intersection(hor1.grid_extents, hor2.grid_extents) 77 | if extent is None: 78 | raise ValueError('Horizons do not overlap!') 79 | else: 80 | raise ValueError('Invalid extent type specified') 81 | xmin, xmax, ymin, ymax = extent 82 | x, y = np.mgrid[xmin:xmax+1, ymin:ymax+1] 83 | hor1.grid_extents = extent 84 | hor2.grid_extents = extent 85 | 86 | grid = hor1.grid - hor2.grid 87 | mask = ~grid.mask * np.ones_like(grid, dtype=np.bool) 88 | x, y, z, mask = x.ravel(), y.ravel(), grid.ravel(), mask.ravel() 89 | iso = horizon(x=x[mask], y=y[mask], z=z[mask]) 90 | iso._grid = grid 91 | return iso 92 | 93 | def extractWindow(hor, vol, upper=0, lower=None, offset=0, region=None, 94 | masked=False): 95 | """Extracts a window around a horizion out of a geoprobe volume 96 | Input: 97 | hor: a geoprobe horizion object or horizion filename 98 | vol: a geoprobe volume object or volume filename 99 | upper: (default, 0) upper window interval around horizion in voxels 100 | lower: (default, upper) lower window interval around horizion in voxels 101 | offset: (default, 0) amount (in voxels) to offset the horizion by along 102 | the Z-axis 103 | region: (default, overlap between horizion and volume) sub-region to 104 | use instead of full extent. Must be a 4-tuple of (xmin, xmax, 105 | ymin, ymax) 106 | masked: (default, False) if True, return a masked array where nodata 107 | values in the horizon are masked. Otherwise, return an array 108 | where the nodata values are filled with 0. 109 | Output: 110 | returns a numpy volume "flattened" along the horizion 111 | """ 112 | # If the lower window isn't specified, assume it's equal to the 113 | # upper window 114 | if lower == None: lower=upper 115 | 116 | # If filenames are input instead of volume/horizion objects, create 117 | # the objects 118 | if type(hor) == type('String'): 119 | hor = horizon(hor) 120 | if type(vol) == type('String'): 121 | vol = volume(vol) 122 | 123 | #Gah, changed the way hor.grid works... Should probably change it back 124 | depth = hor.grid.T 125 | 126 | # Find the overlap between the horizon and volume 127 | vol_extents = [vol.xmin, vol.xmax, vol.ymin, vol.ymax] 128 | hor_extents = [hor.xmin, hor.xmax, hor.ymin, hor.ymax] 129 | extents = bbox_intersection(hor_extents, vol_extents) 130 | 131 | # Raise ValueError if horizon and volume do not intersect 132 | if extents is None: 133 | raise ValueError('Input horizon and volume do not intersect!') 134 | 135 | # Find the overlap between the (optional) subregion current extent 136 | if region is not None: 137 | extents = bbox_intersection(extents, region) 138 | if extents is None: 139 | raise ValueError('Specified region does not overlap with'\ 140 | ' horizon and volume') 141 | elif len(extents) != 4: 142 | raise ValueError('"extents" must be a 4-tuple of'\ 143 | ' (xmin, xmax, ymin, ymax)') 144 | 145 | xmin, xmax, ymin, ymax = extents 146 | 147 | # Convert extents to volume indicies and select subset of the volume 148 | i, j = vol.model2index([xmin, xmax], [ymin, ymax]) 149 | data = vol.data[i[0]:i[1], j[0]:j[1], :] 150 | 151 | # Convert extents to horizion grid indicies and select 152 | # subset of the horizion 153 | 154 | hxmin, hxmax, hymin, hymax = hor.grid_extents 155 | xstart, ystart = xmin - hxmin, ymin - hymin 156 | xstop, ystop = xmax - hxmin, ymax - hymin 157 | depth = depth[xstart:xstop, ystart:ystop] 158 | 159 | nx,ny,nz = data.shape 160 | 161 | # convert z coords of horizion to volume indexes 162 | depth -= vol.zmin 163 | depth /= abs(vol.dz) 164 | depth = depth.astype(np.int) 165 | 166 | # Initalize the output array 167 | window_size = upper + lower + 1 168 | # Not creating a masked array here due to speed problems when 169 | # iterating through ma's 170 | subVolume = np.zeros((nx,ny,window_size), dtype=np.uint8) 171 | 172 | # Using fancy indexing to do this uses tons of memory... 173 | # As it turns out, simple iteration is much, much more memory 174 | # efficient, and almost as fast 175 | mask = depth.mask.copy() # Need to preserve the mask for later 176 | if not mask.shape: 177 | mask = np.zeros(depth.shape, dtype=np.bool) 178 | depth = depth.filled() # Iterating through masked arrays is much slower. 179 | for i in six.moves.xrange(nx): 180 | for j in six.moves.xrange(ny): 181 | if depth[i,j] != hor.nodata: 182 | # Where are we in data indicies 183 | z = depth[i,j] + offset 184 | if z < 0: 185 | mask[i,j] = True 186 | continue 187 | top = z - upper 188 | bottom = z + lower + 1 189 | 190 | # Be careful to extract the right vertical region in cases 191 | # where the window goes outside the data bounds (find region of 192 | # overlap) 193 | data_top = max([top, 0]) 194 | data_bottom = min([bottom, nz]) 195 | window_top = max([0, window_size - bottom]) 196 | window_bottom = min([window_size, nz - top]) 197 | 198 | # Extract the window out of data and store it in subVolume 199 | subVolume[i, j, window_top : window_bottom] \ 200 | = data[i, j, data_top : data_bottom] 201 | 202 | # If masked is True (input option), return a masked array 203 | if masked: 204 | nx,ny,nz = subVolume.shape 205 | mask = mask.reshape((nx,ny,1)) 206 | mask = np.tile(mask, (1,1,nz)) 207 | subVolume = np.ma.array(subVolume, mask=mask) 208 | 209 | # If upper==lower==0, (default) subVolume will be (nx,ny,1), so 210 | # return 2D array instead 211 | subVolume = subVolume.squeeze() 212 | 213 | return subVolume 214 | 215 | def wiggle(x, origin=0, posFill='black', negFill=None, lineColor='black', 216 | resampleRatio=10, rescale=False, ymin=0, ymax=None, ax=None): 217 | """Plots a "wiggle" trace 218 | Input: 219 | x: input data (1D numpy array) 220 | origin: (default, 0) value to fill above or below (float) 221 | posFill: (default, black) color to fill positive wiggles with (string 222 | or None) 223 | negFill: (default, None) color to fill negative wiggles with (string 224 | or None) 225 | lineColor: (default, black) color of wiggle trace (string or None) 226 | resampleRatio: (default, 10) factor to resample traces by before 227 | plotting (1 = raw data) (float) 228 | rescale: (default, False) If True, rescale "x" to be between -1 and 1 229 | ymin: (default, 0) The minimum y to use for plotting 230 | ymax: (default, len(x)) The maximum y to use for plotting 231 | ax: (default, current axis) The matplotlib axis to plot onto 232 | Output: 233 | a matplotlib plot on the current axes 234 | """ 235 | from matplotlib import pyplot as plt 236 | from scipy.signal import cspline1d, cspline1d_eval 237 | 238 | if ymax is None: 239 | ymax = x.size 240 | 241 | # Rescale so that x ranges from -1 to 1 242 | if rescale: 243 | x = x.astype(np.float) 244 | x -= x.min() 245 | x /= x.ptp() 246 | x *= 2 247 | x -= 1 248 | 249 | # Interpolate at resampleRatio x the previous density 250 | y = np.linspace(0, x.size, x.size) 251 | interp_y = np.linspace(0, x.size, x.size * resampleRatio) 252 | cj = cspline1d(x) 253 | interpX = cspline1d_eval(cj,interp_y) #,dx=1,x0=0 254 | newy = np.linspace(ymax, ymin, interp_y.size) 255 | if origin == None: 256 | origin = interpX.mean() 257 | 258 | # Plot 259 | if ax is None: 260 | ax = plt.gca() 261 | plt.hold(True) 262 | if posFill is not None: 263 | ax.fill_betweenx(newy, interpX, origin, 264 | where=interpX > origin, 265 | facecolor=posFill) 266 | if negFill is not None: 267 | ax.fill_betweenx(newy, interpX, origin, 268 | where=interpX < origin, 269 | facecolor=negFill) 270 | if lineColor is not None: 271 | ax.plot(interpX, newy, color=lineColor) 272 | 273 | def wiggles(grid, wiggleInterval=10, overlap=0.7, posFill='black', 274 | negFill=None, lineColor='black', rescale=True, extent=None, ax=None): 275 | """Plots a series of "wiggle" traces based on a grid 276 | Input: 277 | x: input data (2D numpy array) 278 | wiggleInterval: (default, 10) Plot 'wiggles' every wiggleInterval 279 | traces 280 | overlap: (default, 0.7) amount to overlap 'wiggles' by (1.0 = scaled 281 | to wiggleInterval) 282 | posFill: (default, black) color to fill positive wiggles with (string 283 | or None) 284 | negFill: (default, None) color to fill negative wiggles with (string 285 | or None) 286 | lineColor: (default, black) color of wiggle trace (string or None) 287 | resampleRatio: (default, 10) factor to resample traces by before 288 | plotting (1 = raw data) (float) 289 | extent: (default, (0, nx, 0, ny)) The extent to use for the plot. 290 | A 4-tuple of (xmin, xmax, ymin, ymax) 291 | ax: (default, current axis) The matplotlib axis to plot onto. 292 | Output: 293 | a matplotlib plot on the current axes 294 | """ 295 | # Rescale so that the grid ranges from -1 to 1 296 | if rescale: 297 | grid = grid.astype(np.float) 298 | grid -= grid.min() 299 | grid /= grid.ptp() 300 | grid *= 2 301 | grid -= 1 302 | 303 | if extent is None: 304 | xmin, ymin = 0, grid.shape[0] 305 | ymax, xmax = 0, grid.shape[1] 306 | else: 307 | xmin, xmax, ymin, ymax = extent 308 | 309 | ny,nx = grid.shape 310 | x_loc = np.linspace(xmin, xmax, nx) 311 | for i in range(wiggleInterval//2, nx, wiggleInterval): 312 | x = overlap * (wiggleInterval / 2.0) \ 313 | * (x_loc[1] - x_loc[0]) \ 314 | * grid[:,i] 315 | wiggle(x + x_loc[i], origin=x_loc[i], 316 | posFill=posFill, negFill=negFill, 317 | lineColor=lineColor, ymin=ymin, 318 | ymax=ymax, ax=ax) 319 | 320 | 321 | def roseDiagram(data, nbins=30, bidirectional=True, title='North'): 322 | """Plots a circular histogram or "rose diagram" 323 | Input: 324 | data: A list or 1D array of orientation data that the histogram 325 | will be made from. The data should be an in degrees clockwise 326 | from north. 327 | nbins (default: 30): The number of bins in the histogram 328 | bidirectional (default: True): Whether or not to treat the input data 329 | as bi-directional. (i.e. if True, the rose diagram will be 330 | symmetric) 331 | title (default: 'North'): The title of the plot 332 | """ 333 | # TODO: This needs to pass kwargs on to the plotting routines 334 | # TODO: (or just remove this function entirely? It shouldn't 335 | # TODO: really be here) 336 | from matplotlib import pyplot as plt 337 | data = np.asarray(data) 338 | n = data.size 339 | 340 | if bidirectional: 341 | # Rather than doing some sort of fancy binning, just 342 | # "double" the data with the complimentary end (+180) 343 | data = np.append(data, data+180) 344 | data[data>360] -= 360 345 | 346 | # Rotate the data so that north will plot at the top 347 | # (90deg, in polar space) 348 | data = 90-data 349 | data[data<0] += 360 350 | 351 | # Make a figure with polar axes 352 | fig = plt.figure() 353 | ax = fig.add_axes([0.1, 0.1, 0.8, 0.7], polar=True, axisbg='#d5de9c') 354 | 355 | # Change the labeling so that north is at the top 356 | plt.thetagrids(np.arange(0,360,45), 357 | ['90','45','0','315','270','225','180','135']) 358 | 359 | # Plot a histogram on the polar axes 360 | data = np.radians(data) 361 | plt.hist(data, bins=nbins, axes=ax) 362 | plt.title(title + '\nn=%i'%n) 363 | 364 | 365 | def array2geotiff(data, filename, nodata=-9999, transform=None, extents=None): 366 | """ 367 | Write a geotiff file called "filename" from the numpy array "data". 368 | Input: 369 | data: A 2D numpy array with shape (nx,ny) 370 | filename: The filename of the output geotiff 371 | nodata: The nodata value of the array (defaults to -9999) 372 | transform (optional): Either a 3x2 numpy array describing 373 | an affine transformation to georeference the geotiff 374 | with, or an object that has a transform attribute 375 | containing such an array (e.g. a geoprobe.volume object) 376 | extents (optional): Either a 2-tuple of the coords of the 377 | lower left corner of "data" (xmin, ymin) or an object 378 | with xmin, xmax, ymin, ymax attributes (e.g. a geoprobe 379 | volume object). This overrides the x and y offsets in 380 | "transform". If transform is not given, it is ignored. 381 | Note that if you're using a horizon object as input to 382 | "extents", you'll need to call volume.model2world on 383 | horizon.xmin & ymin, as a horizon's coordinates are 384 | stored in model (inline, crossline) space. 385 | """ 386 | try: import osgeo.gdal as gdal 387 | except ImportError: 388 | raise ImportError('Gdal not found! To use horizon.toGeotiff, gdal and the python bindings to gdal must be installed') 389 | 390 | if transform is not None: 391 | try: 392 | transform = transform.transform 393 | except AttributeError: 394 | # Else, assume it's already 2x3 array containg an affine transformation 395 | pass 396 | if extents is not None: 397 | try: 398 | xmin,ymin = extents 399 | except: 400 | try: 401 | xmin, ymin = extents.xmin, extents.ymin 402 | except AttributeError: 403 | raise ValueError('Extents must be either a 2-tuple of xmin,ymin or an object with xmin,ymin properities') 404 | else: 405 | xmin, ymin = 0,0 406 | 407 | # Determine output format from filename 408 | if filename[-4:] in ['.tif','tiff']: 409 | format = 'GTiff' 410 | elif filename[-3:] == 'img': 411 | format = 'HFA' 412 | else: 413 | # Assume geotiff and append ".tif" 414 | format = 'GTiff' 415 | filename += '.tif' 416 | 417 | # Need to change horizon.grid, and change this when I do... 418 | # Everything should probably expect a x by y array to maintain consistency with volume.data 419 | ysize,xsize = data.shape 420 | 421 | #-- Create and write output file 422 | driver = gdal.GetDriverByName(format) 423 | dataset = driver.Create(filename,xsize,ysize,1,gdal.GDT_Float32) #One band, stored as floats 424 | 425 | # Georeference volume if vol is given 426 | if transform is not None: 427 | dataset.SetGeoTransform( [xmin, transform[0,0], transform[0,1], 428 | ymin, transform[1,0], transform[1,1]] ) 429 | # Set the nodata value 430 | dataset.GetRasterBand(1).SetNoDataValue(nodata) 431 | # Write dataset 432 | dataset.GetRasterBand(1).WriteArray(data) 433 | 434 | def extract_section(data, x, y, zmin=None, zmax=None): 435 | """Extracts an "arbitrary" section from data defined by vertices in *x*,*y*. 436 | Input: 437 | *data*: A 2D or 3D numpy array. 438 | *x*: A sequence of indicies along the first axis. 439 | *y*: A sequence of indicies along the second axis. 440 | *zmin*: The minimum "z" index along the 3rd axis to be extracted. 441 | *zmin*: The maximum "z" index along the 3rd axis to be extracted.""" 442 | def interpolate_endpoints(x, y): 443 | distance = np.cumsum(np.hypot(np.diff(x), np.diff(y))) 444 | distance = np.r_[0, distance] 445 | i = np.arange(int(distance.max())) 446 | xi = np.interp(i, distance, x) 447 | yi = np.interp(i, distance, y) 448 | return xi, yi 449 | 450 | # For some things (e.g. hdf arrays), atleast_3d will inadvertently load 451 | # the entire array into memory. In those cases, we'll skip it... 452 | try: 453 | ndims = len(data.shape) 454 | except AttributeError: 455 | data = np.asarray(data) 456 | if ndims != 3: 457 | data = np.atleast_3d(data) 458 | nx, ny, nz = data.shape 459 | 460 | xi, yi = interpolate_endpoints(x, y) 461 | inside = (xi >= 0) & (xi < nx) & (yi >= 0) & (yi < ny) 462 | xi, yi = xi[inside], yi[inside] 463 | output = [] 464 | 465 | # Indicies must be ints with recent versions of h5py 466 | convert = lambda x: int(x) if x is not None else None 467 | zslice = slice(convert(zmin), convert(zmax)) 468 | 469 | # Using fancy indexing will only work in certain cases for hdf arrays... 470 | # Therefore we need to iterate through and take a slice at each point. 471 | for i, j in zip(xi.astype(int), yi.astype(int)): 472 | output.append(data[i, j, zslice]) 473 | try: 474 | # Need to make sure we properly handle masked arrays, thus np.ma... 475 | section = np.ma.vstack(output) 476 | except ValueError: 477 | # Just return an empty array if nothing is inside... 478 | section = np.array([]) 479 | return section, xi, yi 480 | 481 | def points2strikeDip(x, y, z, vol=None, velocity=None): 482 | """ 483 | Takes a point cloud defined by 3 vectors and returns a strike and dip 484 | following the Right-hand-rule. 485 | Input: 486 | x, y, z: numpy arrays or lists containg the x, y, and z 487 | coordinates, respectively 488 | vol (optional): A geoprobe volume object 489 | If specified, the x, y, and z units will be converted 490 | to world units based on the volume's header. 491 | velocity (optional): Velocity in meters/second 492 | If specified, the z units will be converted from time 493 | into depth using the velocity given. Assumes the z 494 | units are milliseconds!! 495 | Output: 496 | strike, dip (in degrees) 497 | """ 498 | # Get x,y, and z, converting to world coords if necessary 499 | if vol is not None: 500 | # If given a string, assume it's the filename of a volume 501 | if type(vol) == type('String'): 502 | vol = volume(vol) 503 | # Convert coords 504 | x,y = vol.model2world(x, y) 505 | 506 | #Negate z if depth is positive (which it usually is) 507 | if vol is None or vol.dz < 0: 508 | z = -z 509 | 510 | # Convert to depth using velocity (assumes velocity is in m/s and Z is in milliseconds) 511 | if velocity is not None: 512 | z = 0.5 * velocity * z / 1000 513 | 514 | a,b,c,d = fit_plane(x,y,z) 515 | strike, dip = normal2SD(a,b,c) 516 | 517 | return strike, dip 518 | 519 | def fit_plane(x, y, z): 520 | """Fits a plane to a point cloud using least squares. Should handle 521 | vertical and horizontal planes properly. 522 | Input: 523 | x,y,z: 524 | numpy arrays of x, y, and z, respectively 525 | Returns: 526 | a,b,c,d where 0 = ax + by + cz +d 527 | (The normal vector is < a, b, c >) 528 | """ 529 | basis = principal_axes(x, y, z) 530 | a, b, c = basis[:, -1] 531 | d = a * x + b * y + c * z 532 | return a, b, c, -d.mean() 533 | 534 | def principal_axes(x, y, z, return_eigvals=False): 535 | """Finds the principal axes of a 3D point cloud. 536 | Input: 537 | x, y, z: 538 | numpy arrays of x, y, and z, respectively 539 | return_eigvals (default: False): 540 | A boolean specifying whether to return the eigenvalues. 541 | Returns: 542 | eigvecs : A 3x3 numpy array whose columns represent 3 orthogonal 543 | vectors. The first column corresponds to the axis with the largest 544 | degree of variation (the principal axis) and the last column 545 | correspons to the axis with the smallest degree of variation. 546 | eigvals : (Only returned if `return_eigvals` is True) A 3-length vector 547 | of the eigenvalues of the point cloud. 548 | """ 549 | coords = np.vstack([x,y,z]) 550 | cov = np.cov(coords) 551 | eigvals, eigvecs = np.linalg.eigh(cov) 552 | order = eigvals.argsort()[::-1] 553 | eigvecs = eigvecs[:, order] 554 | eigvals = eigvals[order] 555 | if return_eigvals: 556 | return eigvecs, eigvals 557 | else: 558 | return eigvecs 559 | 560 | def normal2SD(x,y,z): 561 | """Converts a normal vector to a plane (given as x,y,z) 562 | to a strike and dip of the plane using the Right-Hand-Rule. 563 | Input: 564 | x: The x-component of the normal vector 565 | y: The y-component of the normal vector 566 | z: The z-component of the normal vector 567 | Output: 568 | strike: The strike of the plane, in degrees clockwise from north 569 | dip: The dip of the plane, in degrees downward from horizontal 570 | """ 571 | from math import asin, atan2, sqrt, degrees 572 | 573 | # Due to geologic conventions, positive angles are downwards 574 | z = -z 575 | 576 | # First convert the normal vector to spherical coordinates 577 | # (This is effectively a plunge/bearing of the normal vector) 578 | r = sqrt(x*x + y*y + z*z) 579 | plunge = degrees(asin(z/r)) 580 | bearing = degrees(atan2(y, x)) 581 | 582 | 583 | # Rotate bearing so that 0 is north instead of east 584 | bearing = 90-bearing 585 | if bearing<0: bearing += 360 586 | 587 | # If the plunge angle is upwards, get the opposite end of the line 588 | if plunge<0: 589 | plunge = -plunge 590 | bearing -= 180 591 | if bearing<0: 592 | bearing += 360 593 | 594 | # Now convert the plunge/bearing of the pole to the plane that it represents 595 | strike = bearing+90 596 | dip = 90-plunge 597 | if strike > 360: strike -= 360 598 | 599 | return strike, dip 600 | 601 | 602 | -------------------------------------------------------------------------------- /geoprobe/volume.py: -------------------------------------------------------------------------------- 1 | __license__ = "MIT License " 2 | __copyright__ = "2009, Free Software Foundation" 3 | __author__ = "Joe Kington " 4 | 5 | import os 6 | import numpy as np 7 | import six 8 | 9 | # Dictonary of header values and offsets for a geoprobe volume 10 | from ._volHeader import headerDef as _headerDef 11 | from ._volHeader import headerLength as _headerLength 12 | 13 | # Common methods 14 | from .common import read_binary, write_binary 15 | from .common import format_headerDef_docs 16 | 17 | # Factory function for creating new Volume objects... 18 | def volume(input, copyFrom=None, rescale=True, voltype=None): 19 | """ 20 | Read an existing geoprobe volue or make a new one based on input data 21 | Input: 22 | input: Either the path to a geoprobe volume file or data to 23 | create a geoprobe object from (either a numpy array or 24 | an object convertable into one). The following keyword 25 | arguments only apply when creating a new volume from 26 | data, not reading from a file. 27 | copyFrom (default: None): A geoprobe volume object or path to a 28 | geoprobe volume file to copy relevant header values 29 | from for the new volume object (used only when creating 30 | a new volume from a numpy array). 31 | rescale (default: True): Boolean True/False. If True, the data 32 | will be rescaled so that data.min(), data.max() 33 | correspond to 0, 255 in volume.data before being 34 | converted to 8-bit values. If False, the data data 35 | will not be rescaled before converting to type uint8. 36 | (i.e. 0.9987 --> 0, 257.887 --> 1, etc due to rounding 37 | and wrap around) 38 | voltype (default: None): Explicitly set the type of volume. 39 | By default, the volume type will be guessed from the 40 | input file. Valid options are: "hdf", "geoprobe_v2", and 41 | "voxel_geo". 42 | """ 43 | typestrings = {'hdf':HDFVolume, 'geoprobe_v2':GeoprobeVolumeV2, 44 | 'voxel_geo':VoxelGeoVolume} 45 | if voltype is None: 46 | voltype = formats[0] 47 | else: 48 | if voltype in typestrings: 49 | voltype = typestrings[voltype] 50 | 51 | # What were we given as input? 52 | if isinstance(input, six.string_types): 53 | # Assume strings are filenames of a geoprobe array 54 | for vol_format in formats: 55 | if _check_validity(vol_format, input): 56 | vol = vol_format() 57 | vol._readVolume(input) 58 | return vol 59 | else: 60 | raise IOError('This does not appear to be a valid geoprobe file!') 61 | 62 | else: 63 | # If it's not a string, just assume it's a numpy array or 64 | # convertable into one and try to make a new volume out of it 65 | vol = voltype() 66 | vol._newVolume(input, copyFrom=copyFrom, rescale=rescale) 67 | return vol 68 | 69 | def _check_validity(vol_format, filename): 70 | try: 71 | volfile = vol_format.format_type(filename, 'rb') 72 | is_valid = volfile.is_valid() 73 | volfile.close() 74 | return is_valid 75 | except (IOError, EOFError): 76 | return False 77 | 78 | def isValidVolume(filename): 79 | """Tests whether a filename is a valid geoprobe file. Returns boolean 80 | True/False.""" 81 | return any([_check_validity(format, filename) for format in formats]) 82 | 83 | #-- Base Volume Class --------------------------------------------------------- 84 | class Volume(object): 85 | # Not a "normal" docstring so that "useful attributes" is set at runtime 86 | __doc__ = """ 87 | Reads and writes geoprobe volumes 88 | 89 | Useful attributes set at initialization:\n%s 90 | """ % format_headerDef_docs(_headerDef) 91 | 92 | format_type = None 93 | 94 | def __init__(self): 95 | """See the "volume" function's docstring for constructor information""" 96 | pass 97 | 98 | def __getitem__(self, value): 99 | """Expects slices in model coordinates. Returns the equivalent slice 100 | of volume.data as a numpy array.""" 101 | new_values = self._convert_slice_to_indicies(value) 102 | dataslice = self.data.__getitem__(new_values) 103 | return dataslice 104 | 105 | def _convert_slice_to_indicies(self, value): 106 | """Converts a slice or integer passed into __getitem__ into indicies""" 107 | value = list(value) 108 | new_values = [] 109 | for item, axis in zip(value, ('x', 'y', 'z')): 110 | if isinstance(item, int) or isinstance(item, float): 111 | new_values.append(int(self.model2index(item, axis=axis))) 112 | elif isinstance(item, slice): 113 | newslice = [] 114 | for val in [item.start, item.stop, item.step]: 115 | if val is not None: 116 | newslice.append(int(self.model2index(val, axis=axis))) 117 | else: 118 | newslice.append(None) 119 | new_values.append(slice(*newslice)) 120 | else: 121 | new_values.append(item) 122 | if len(new_values) == 1: 123 | new_values.extend([None, None]) 124 | new_values = tuple(new_values) 125 | return new_values 126 | 127 | 128 | def _readVolume(self,filename): 129 | """ 130 | Reads the header of a geoprobe volume and sets attributes based on it 131 | """ 132 | self._filename = filename 133 | self._infile = self.format_type(filename, 'rb') 134 | self.headerValues = self._infile.read_header() 135 | 136 | def _newVolume(self, data, copyFrom=None, rescale=True, fix_axes=True): 137 | """Takes a numpy array and makes a geoprobe volume. This 138 | volume can then be written to disk using the write() method.""" 139 | 140 | data = np.asarray(data) 141 | 142 | #Set up the header Dictionary 143 | if copyFrom is not None: 144 | # Assume the string is the filname of a geoprobe volume 145 | if isinstance(copyFrom, six.string_types): 146 | copyFrom = volume(copyFrom) 147 | try: 148 | self.headerValues = copyFrom.headerValues 149 | except AttributeError: 150 | raise TypeError('This does not appear to be a valid'\ 151 | ' geoprobe volume object') 152 | else: 153 | # Set default attributes 154 | for varname, info in six.iteritems(_headerDef): 155 | setattr(self, varname, info['default']) 156 | (self.originalnx, self.originalny, self.originalnz) = data.shape 157 | 158 | if rescale: 159 | self.v0 = data.min() 160 | self.dv = data.ptp() / 255.0 161 | data -= self.v0 162 | data /= self.dv 163 | 164 | if fix_axes: 165 | data = self._fixAxes(data) 166 | 167 | self._data = data 168 | 169 | def _fixAxes(self, data): 170 | """ 171 | Reverses the x, y, and z axes if dx, dy, or dz (respectively) are 172 | negative. This ensures that self.data[0,0,0] always corresponds 173 | to self.xmin, self.ymin, self.zmin. 174 | """ 175 | if self.dz < 0: 176 | data = data[:,:,::-1] 177 | if self.dy < 0: 178 | data = data[:,::-1,:] 179 | if self.dx < 0: 180 | data = data[::-1,:,:] 181 | return data 182 | 183 | def load(self): 184 | """Reads an entire Geoprobe volume into memory and returns 185 | a numpy array contining the volume.""" 186 | try: 187 | dat = self._infile.read_data() 188 | dat = self._fixAxes(dat) 189 | except AttributeError: 190 | # If there's no self._infile attribute, then the volume 191 | # has been initialized from an array in memory, and we don't 192 | # need to load the data... 193 | dat = self.data 194 | return dat 195 | 196 | def write(self, filename): 197 | """Writes a geoprobe volume to disk.""" 198 | outfile = self.format_type(filename, 'wb') 199 | outfile.write_header(self.headerValues) 200 | outfile.write_data(self.data) 201 | 202 | def crop(self, xmin=None, xmax=None, ymin=None, ymax=None, 203 | zmin=None, zmax=None, copy_data=True): 204 | """Crops a volume to the limits specified (in model coordinates) 205 | by the input arguments. Returns a new volume instance containing 206 | data for the cropped region.""" 207 | 208 | # Set defaults (verbose, but I want to avoid metaprogramming...) 209 | if (xmin is None) or (xmin < self.xmin): 210 | xmin = self.xmin 211 | if (xmax is None) or (xmax > self.xmax): 212 | xmax = self.xmax 213 | 214 | if (ymin is None) or (ymin < self.ymin): 215 | ymin = self.ymin 216 | if (ymax is None) or (ymax > self.ymax): 217 | ymax = self.ymax 218 | 219 | if (zmin is None) or (zmin < self.zmin): 220 | zmin = self.zmin 221 | if (zmax is None) or (zmax > self.zmax): 222 | zmax = self.zmax 223 | 224 | # Convert input units to indicies... 225 | xstart, xstop = self.model2index([xmin, xmax], axis='x') 226 | ystart, ystop = self.model2index([ymin, ymax], axis='y') 227 | zstart, zstop = self.model2index([zmin, zmax], axis='z') 228 | 229 | # Crop data 230 | data = self.data[xstart:xstop, ystart:ystop, zstart:zstop] 231 | if copy_data: 232 | data = data.copy() 233 | 234 | # Make a new volume instance and set it's mininum model coords 235 | vol = type(self)() 236 | vol._newVolume(data, copyFrom=self, rescale=False, fix_axes=False) 237 | vol.xmin, vol.ymin, vol.zmin = xmin, ymin, zmin 238 | 239 | return vol 240 | 241 | #-- data property -------------------------------------------------------- 242 | def _getData(self): 243 | """A 3D numpy array of the volume data. Contains raw uint8 values. 244 | If the volume object is based on a file, this is a memory-mapped-file 245 | array.""" 246 | # Prevents _getData from being called more than once 247 | try: 248 | return self._data 249 | except AttributeError: 250 | self._data = self._fixAxes(self._infile.memmap_data()) 251 | return self._data 252 | 253 | def _setData(self, newData): 254 | """Set self.data without making a copy in-memory""" 255 | newData = np.asanyarray(newData, dtype=np.uint8) 256 | # Make sure we're dealing with a 3D numpy array 257 | try: 258 | self._nx, self._ny, self._nz = newData.shape 259 | except (ValueError, AttributeError): 260 | raise TypeError('Data must be a 3D numpy array') 261 | 262 | # We don't update dv and d0 here. This is to avoid overwriting the 263 | # "real" dv and d0 when you're manipulating the data in some way. When 264 | # making a new volume object from an existing numpy array (in 265 | # _newVolume), dv and d0 are set 266 | self._data = newData 267 | 268 | data = property(_getData, _setData) 269 | #-------------------------------------------------------------------------- 270 | 271 | 272 | #-- headerValues property (avoiding @headerValues.setter to keep 273 | #-- compatibility w/ python2.5)---- 274 | def _getHeaderValues(self): 275 | """ 276 | A dict with a key, value pair for each value in the geoprobe volume 277 | file header. These are stored as class attributes in each volume 278 | instance. Do not set vol.headerValues['something'] = newValue. This 279 | will silently fail to update vol.something. However, setting 280 | vol.headerValues = {'something':newValue} will work fine. Normally, 281 | header values should be set directly through the volume's attributes, 282 | e.g. vol.something = newValue. 283 | """ 284 | # Return the current instance attributes that are a part of the 285 | # header definition 286 | values = {} 287 | for key in _headerDef: 288 | # If it's been deleted for some reason, return the default value 289 | default = _headerDef[key]['default'] 290 | values[key] = getattr(self, key, default) 291 | return values 292 | 293 | def _setHeaderValues(self, input_val): 294 | # This needs to raise an error if set via headerValues['x'] = y! 295 | # Don't know how to do it easily, though... 296 | for key, value in six.iteritems(input_val): 297 | # Only set things in input that are normally in the header 298 | if key in _headerDef: 299 | setattr(self, key, value) 300 | 301 | headerValues = property(_getHeaderValues, _setHeaderValues) 302 | #-------------------------------------------------------------------------- 303 | 304 | 305 | #-- xmin, xmax, etc ------------------------------------------------------- 306 | def _getVolumeBound(self, axis, max=True): 307 | n = [self.nx, self.ny, self.nz][axis] 308 | d = [self.dx, self.dy, self.dz][axis] 309 | offset = [self.x0, self.y0, self.z0][axis] 310 | stop = offset + d * (n-1) 311 | start = offset 312 | if ((max is True) & (d>0)) or ((max is False) & (d<0)): 313 | return stop 314 | else: 315 | return start 316 | 317 | def _setVolumeBound(self, value, axis, max=True): 318 | axisLetter = ['x','y','z'][axis] 319 | n = [self.nx, self.ny, self.nz][axis] 320 | d = [self.dx, self.dy, self.dz][axis] 321 | if ((max is True) & (d>0)) or ((max is False) & (d<0)): 322 | value = value + (n-1) * abs(d) 323 | setattr(self, axisLetter+'0', value) 324 | 325 | xmin = property(lambda self: self._getVolumeBound(axis=0, max=False), 326 | lambda self, value: self._setVolumeBound(value, axis=0, max=False), 327 | doc="Mininum x model coordinate") 328 | 329 | ymin = property(lambda self: self._getVolumeBound(axis=1, max=False), 330 | lambda self, value: self._setVolumeBound(value, axis=1, max=False), 331 | doc="Mininum y model coordinate") 332 | 333 | zmin = property(lambda self: self._getVolumeBound(axis=2, max=False), 334 | lambda self, value: self._setVolumeBound(value, axis=2, max=False), 335 | doc="Mininum z model coordinate") 336 | 337 | xmax = property(lambda self: self._getVolumeBound(axis=0, max=True), 338 | lambda self, value: self._setVolumeBound(value, axis=0, max=True), 339 | doc="Maximum x model coordinate") 340 | 341 | ymax = property(lambda self: self._getVolumeBound(axis=1, max=True), 342 | lambda self, value: self._setVolumeBound(value, axis=1, max=True), 343 | doc="Maximum y model coordinate") 344 | 345 | zmax = property(lambda self: self._getVolumeBound(axis=2, max=True), 346 | lambda self, value: self._setVolumeBound(value, axis=2, max=True), 347 | doc="Maximum z model coordinate") 348 | #-------------------------------------------------------------------------- 349 | 350 | 351 | #-- nx, ny, nz ------------------------------------------------------------ 352 | @property 353 | def nx(self): 354 | """The number of x values in the array (read-only)""" 355 | return self.data.shape[0] 356 | @property 357 | def ny(self): 358 | """The number of y values in the array (read-only)""" 359 | return self.data.shape[1] 360 | @property 361 | def nz(self): 362 | """The number of z values in the array (read-only)""" 363 | return self.data.shape[2] 364 | #-------------------------------------------------------------------------- 365 | 366 | 367 | # Need to fix these... should be 3x2 instead of 2x3... 368 | # fix transform when I do! 369 | 370 | #-- worldCoords property------------------------------------- 371 | def _getWorldCoords(self): 372 | """A 2x3 array containing 3 points in world coordinates corresponding 373 | to the points in modelCoords. 374 | [[x1, x2, x3], 375 | [y2, y2, y3]]""" 376 | return np.asarray(self.georef[:6]).reshape(2,3) 377 | def _setWorldCoords(self, newValues): 378 | self.georef[:6] = np.asarray(newValues).flatten() 379 | worldCoords = property(_getWorldCoords, _setWorldCoords) 380 | #------------------------------------------------------------ 381 | 382 | #-- modelCoords property------------------------------------- 383 | # georef format: Xw1,Xw2,Xw3,Yw1,Yw2,Yw3,Ym1,Ym2,Ym3,Xm1,Xm2,Xm3 384 | # This means that we need to flipud the modelCoords!! 385 | # (looks suspicious, double-check) 386 | def _getModelCoords(self): 387 | """A 2x3 array containing 3 points in model coordinates corresponding 388 | to the points in worldCoords. 389 | [[x1, x2, x3], 390 | [y2, y2, y3]]""" 391 | return np.flipud( np.asarray(self.georef[6:]).reshape(2,3) ) 392 | def _setModelCoords(self, newValues): 393 | self.georef[6:] = np.asarray(newValues).flatten() 394 | modelCoords = property(_getModelCoords, _setModelCoords) 395 | #----------------------------------------------------------- 396 | 397 | @property 398 | def dxW(self): 399 | """X-spacing interval in world coordinates""" 400 | X,Y = self.model2world([self.xmin, self.xmax], [self.ymin, self.ymin]) 401 | dist = np.sqrt(X.ptp()**2 + Y.ptp()**2) 402 | return dist / self.nx 403 | @property 404 | def dyW(self): 405 | """Y-spacing interval in world coordinates""" 406 | X,Y = self.model2world([self.xmin, self.xmin], [self.ymin, self.ymax]) 407 | dist = np.sqrt(X.ptp()**2 + Y.ptp()**2) 408 | return dist / self.ny 409 | 410 | def YSlice(self, Ypos): 411 | """Takes a slice of the volume at a constant y-value (i.e. the 412 | slice is in the direction of the x-axis) This is a convience 413 | function to avoid calling volume.model2index before slicing 414 | and transposing (for easier plotting) after. 415 | Input: 416 | Ypos: Y-Value given in model coordinates 417 | Output: 418 | A 2D (NZ x NX) numpy array""" 419 | Ypos = self.model2index(Ypos, axis='y') 420 | return self.data[:,Ypos,:].transpose() 421 | 422 | def XSlice(self, Xpos): 423 | """Takes a slice of the volume at a constant x-value (i.e. the 424 | slice is in the direction of the y-axis) This is a convience 425 | function to avoid calling volume.model2index before slicing 426 | and transposing (for easier plotting) after. 427 | Input: 428 | Xpos: X-Value given in model coordinates 429 | Output: 430 | A 2D (NZ x NY) numpy array""" 431 | Xpos = self.model2index(Xpos) 432 | return self.data[Xpos,:,:].transpose() 433 | 434 | def ZSlice(self, Zpos): 435 | """Takes a slice of the volume at a constant z-value (i.e. 436 | a depth slice) This is a convience function to avoid calling 437 | volume.model2index before slicing and transposing (for 438 | easier plotting) after. 439 | Input: 440 | Zpos: Z-Value given in model coordinates 441 | Output: 442 | A 2D (NY x NX) numpy array""" 443 | Zpos = self.model2index(Zpos, axis='z') 444 | return self.data[:,:,Zpos].transpose() 445 | 446 | def extract_section(self, x, y, zmin=None, zmax=None, coords='model'): 447 | """Extracts an "arbitrary" section defined by vertices in *x* and *y*. 448 | Input: 449 | *x*: A sequence of x coords in the coordinate system specified by 450 | *coords*. (*coords* defaults to "model") 451 | *y*: A sequence of y coords in the coordinate system specified by 452 | *coords* 453 | *zmin*: The minimum "z" value for the returned section 454 | (in depth/time) 455 | *zmax*: The maximum "z" value for the returned section 456 | (in depth/time) 457 | *coords*: Either "model" or "world", specifying whether *x* and *y* 458 | are given in model or world coordinates.""" 459 | # Delayed import to avoid circular import 460 | from . import utilities 461 | 462 | if coords == 'world': 463 | x, y = self.world2model(x, y) 464 | elif coords != 'model': 465 | raise ValueError('"coords" must be either "world" or "model".') 466 | if zmin is None: 467 | zmin = self.zmin 468 | if zmax is None: 469 | zmax = self.zmax 470 | zmax = self.model2index(zmax, axis='z') + 1 471 | zmin = self.model2index(zmin, axis='z') 472 | 473 | # If zmin and zmax are out of bounds, things will work fine, but the 474 | # returned array won't represent data between zmin and zmin, only the 475 | # data between self.zmin and self.zmax. Best to be noisy! 476 | if (zmin < 0) or (zmax > self.nz): 477 | msg = '"zmin" and "zmax" must be between %.1f and %.1f' 478 | raise ValueError(msg % (self.zmin, self.zmax)) 479 | 480 | x, y = self.model2index(x, y) 481 | section, xi, yi = utilities.extract_section(self.data, x, y, zmin, zmax) 482 | xi, yi = self.index2model(xi, yi) 483 | if coords == 'world': 484 | xi, yi = self.model2world(xi, yi) 485 | return section, xi, yi 486 | 487 | def model2index(self, *coords, **kwargs): 488 | """Converts model coordinates into array indicies 489 | Input: 490 | *coords: x,y,z model coordinates to be converted. Will accept numpy 491 | arrays. If only one coordinate is specified, it is assumed to 492 | be x, and if two are specified, they are assumed to be x,y. 493 | To override this choice (i.e. convert just y or z) use the 494 | axis keyword argument 495 | axis (default None): The axis that the given model coordinate 496 | is from. Use 0,1,2 or 'x','y','z'. 497 | Output: 498 | A tuple of converted coordinates (or just the coord if only one 499 | was input) 500 | Examples: 501 | XI = volume.model2index(X) 502 | XI,YI = volume.model2index(X,Y) 503 | XI,YI,ZI = volume.model2index(X,Y,Z) 504 | ZI = volume.model2index(Z, axis=2) # (or axis='z') 505 | YI,ZI = volume.model2index(Y,Z,axis='y') 506 | """ 507 | return self._modelIndex(*coords, **kwargs) 508 | 509 | def index2model(self, *coords, **kwargs): 510 | """Converts array indicies to model coordinates 511 | Input: 512 | *coords: x,y,z array indicies to be converted. Will accept numpy 513 | arrays. If only one index is specified, it is assumed to 514 | be x, and if two are specified, they are assumed to be x,y. 515 | To override this choice (i.e. convert just y or z) use the 516 | axis keyword argument 517 | axis (default None): The axis that the given model coordinate 518 | is from. Use 0,1,2 or 'x','y','z'. 519 | Output: 520 | A tuple of converted indicies (or just the index if only one was 521 | input) 522 | Examples: 523 | X = volume.index2model(XI) 524 | X,Y = volume.index2model(XI,YI) 525 | X,Y,Z = volume.index2model(XI,YI,ZI) 526 | Z = volume.index2model(ZI, axis=2) # (or axis='z') 527 | """ 528 | kwargs['inverse'] = True 529 | return self._modelIndex(*coords, **kwargs) 530 | 531 | def _modelIndex(self, *coords, **kwargs): 532 | """ 533 | Consolidates model2index and index2model. See docstrings of 534 | index2model and model2index for more detail. 535 | Input: 536 | value: The model coordinate or volume index to be converted 537 | axis (default 0): Which axis the coordinate represents. 538 | Can be either 0,1,2 or 'X','Y','Z' 539 | inverse (defalut False): True to convert index to model coords 540 | Output: 541 | converted coordinate(s) 542 | """ 543 | #-- Again, this looks needlessly complex... ------------------ 544 | # Unfortunately, I can't figure out a more simple method while 545 | # still retaining the flexibility I want this function to have 546 | # Also, I'd really rather not use **kwagrs here, but I have to 547 | # use *args first, and I can't have *args and the axis=None. 548 | 549 | def convert(value,axis,inverse): 550 | """Actually convert the coordinates""" 551 | value = np.asarray(value) 552 | 553 | # Select the proper starting point and step value 554 | # The "axis % 3" is to allow some odd but useful stuff... 555 | # E.g. converting Y,Z pairs 556 | axis = axis % 3 557 | mins = [self.xmin, self.ymin, self.zmin] 558 | Ds = [abs(self.dx), abs(self.dy), abs(self.dz)] 559 | minimum, d = mins[axis], Ds[axis] 560 | 561 | # Convert the coordinates 562 | if inverse: # index2model 563 | return value * d + minimum 564 | else: # model2index 565 | idx = (value - minimum) / d 566 | if int_conversion: 567 | return idx.astype(int) 568 | else: 569 | return idx 570 | 571 | #-- Handle user input ------------------------------------------------- 572 | 573 | # Set the default values of axis and inverse 574 | axis = kwargs.get('axis', 0) 575 | inverse = kwargs.get('inverse', False) 576 | int_conversion = kwargs.get('int_conversion', True) 577 | 578 | # Make sure we have a valid axis 579 | if axis not in [0,1,2,'x','y','z','X','Y','Z']: 580 | raise ValueError('"axis" must be one of 0,1,2 or "x","y","z"') 581 | 582 | # Allow both 0,1,2 and 'x','y','z' (or 'X','Y','Z') for axis 583 | if isinstance(axis, six.string_types): 584 | axis = axis.upper() 585 | axis = {'X':0, 'Y':1, 'Z':2}[axis] 586 | 587 | # Handle calling f(x), f(x,y), f(x,y,z), f(z,axis=2), etc 588 | converted = [convert(x, i+axis, inverse) for i,x in enumerate(coords)] 589 | 590 | # If there's just one value, return it, otherwise return a sequence 591 | if len(converted) == 1: 592 | return converted[0] 593 | else: 594 | return converted 595 | 596 | def model2world(self,crossline,inline=None): 597 | """ 598 | Converts model coordinates to world coordinates. Accepts either a Nx2 599 | list/numpy.array or 2 seperate lists/numpy.array's with crossline, 600 | inline coordinates. Returns 2 numpy arrays X and Y with world 601 | coordinates. If a Nx2 or 2xN array was given, returns a single Nx2 or 602 | 2xN array instead of seperate x & y 1D arrays. 603 | """ 604 | return self._transformCoords(crossline, inline, self.transform) 605 | 606 | def world2model(self, x, y=None): 607 | """ 608 | Converts world coordinates to model coordinates. Accepts either a Nx2 609 | list/numpy.array or 2 seperate lists/numpy.array's with x, y 610 | coordinates. Returns 2 numpy arrays X and Y with model coordinates. 611 | If a Nx2 or 2xN array was given, returns a single Nx2 or 2xN array 612 | instead of seperate x & y 1D arrays. 613 | """ 614 | return self._transformCoords(x, y, self.invtransform) 615 | 616 | def _transformCoords(self,x,y,transform): 617 | """ 618 | Consolidates model2world and world2model. Takes x,y and a transform 619 | matrix and ouputs transformed coords X and Y (both are Nx1). If y is 620 | None, assumes x is a Nx2 matrix where y is the 2nd column 621 | """ 622 | #-- Process input ------------------ 623 | return_array, transpose = False, False 624 | x = np.squeeze(np.asarray(x)) 625 | 626 | # If only one array is given... 627 | if y is None: 628 | return_array = True 629 | # Assume x is 2xN or Nx2 and slice appropriately 630 | if 2 in x.shape: 631 | if x.shape[0] == 2: 632 | x,y = x[0,:], x[1,:] 633 | elif x.shape[1] == 2: 634 | transpose = True 635 | x,y = x[:,0], x[:,1] 636 | x = np.squeeze(x) 637 | else: 638 | raise ValueError('If Y is not given, X must be an' 639 | ' Nx2 or 2xN array!') 640 | y = np.squeeze(np.asarray(y)) 641 | shape = x.shape 642 | x, y = x.flatten(), y.flatten() 643 | 644 | #-- Convert Coordinates ------------------------------------- 645 | try: 646 | # Make a G-matrix from the input coords 647 | dataIn = np.vstack((x,y,np.ones(x.size))) 648 | except ValueError: 649 | raise ValueError('X and Y inputs must be the same size!!') 650 | # Output world coords 651 | dataOut = np.dot(transform,dataIn) 652 | 653 | #-- Output Data with Same Shape as Input -------------------- 654 | if return_array: 655 | if transpose: 656 | dataOut = dataOut.T 657 | return dataOut 658 | else: 659 | # Return x, y 660 | X,Y = dataOut[0,:], dataOut[1,:] 661 | if X.size == 1: 662 | return X[0], Y[0] 663 | else: 664 | return X.reshape(shape), Y.reshape(shape) 665 | 666 | @property 667 | def transform(self): 668 | """A 2x3 numpy array describing an affine transformation between 669 | model and world coordinates. (Read only property. Calculated from 670 | volume.modelCoords and volume.worldCoords.)""" 671 | # Detailed explanation of inversion... 672 | # Ok: 673 | # Ax_m + By_m + C = x_w 674 | # Ex_m + Fy_m + G = y_w 675 | # where x_m, y_m are the model coords 676 | # x_w, y_w are the world coords 677 | # A,B,C,E,F,G are the transformation between them (m) 678 | # 679 | # This can be expressed in matrix form as: 680 | # G = |x_m1, y_m1, 1| m = |A, E| d = |x_w1, y_w1| 681 | # |x_m2, y_m2, 1| |B, F| |x_w2, y_w2| 682 | # | . . .| |C, G| | . . | 683 | # |x_mn, y_mn, 1| |x_wn, y_wn| 684 | # Where: 685 | # G*m = d 686 | # 687 | # Then we solve for m, as we know G and d 688 | # m = d*inv(G) 689 | G = np.vstack((self.modelCoords, [1,1,1])) 690 | d = self.worldCoords 691 | m = np.dot(d,np.linalg.inv(G)) 692 | return m 693 | 694 | @property 695 | def invtransform(self): 696 | """A 2x3 numpy array to transform between world and 697 | model coordinates. (Read only property. Calculated from 698 | volume.modelCoords and volume.worldCoords.)""" 699 | # See explanation in self.transform 700 | G = np.vstack((self.worldCoords, [1,1,1])) 701 | d = self.modelCoords 702 | m = np.dot(d,np.linalg.inv(G)) 703 | return m 704 | 705 | class GeoprobeVolumeFileV2(object): 706 | """Low-level operations for reading and writing to Geoprobe Volume format 707 | version 2.0 seismic data files.""" 708 | _magic_number = 43970 709 | def __init__(self, filename, mode): 710 | self.filename = filename 711 | self.mode = mode 712 | self._file = open(self.filename, self.mode) 713 | 714 | def is_valid(self): 715 | """Returns a boolean indicating whether this is a valid file.""" 716 | header = self.read_header() 717 | nx, ny, nz = [header[item] for item in ('_nx', '_ny', '_nz')] 718 | volSize = os.stat(self.filename).st_size 719 | predSize = nx*ny*nz + _headerLength 720 | 721 | if (header['magicNum'] != self._magic_number) or (volSize != predSize): 722 | return False 723 | else: 724 | return True 725 | 726 | 727 | def read_header(self): 728 | """Reads and returns the header of a geoprobe volume.""" 729 | header = dict() 730 | for varname, info in six.iteritems(_headerDef): 731 | self._file.seek(info['offset']) 732 | value = read_binary(self._file, info['type']) 733 | header[varname] = value 734 | return header 735 | 736 | def read_data(self): 737 | """Reads an entire Geoprobe volume into memory and returns 738 | a numpy array contining the volume.""" 739 | header = self.read_header() 740 | nx, ny, nz = [header[item] for item in ('_nx', '_ny', '_nz')] 741 | dat = np.fromfile(self.filename, dtype=np.uint8) 742 | dat = dat[_headerLength:] 743 | dat = dat.reshape((nz, ny, nx)).T 744 | return dat 745 | 746 | def memmap_data(self): 747 | """Return an object similar to a memory-mapped numpy array.""" 748 | header = self.read_header() 749 | nx, ny, nz = [header[item] for item in ('_nx', '_ny', '_nz')] 750 | dat = np.memmap(filename=self.filename, mode='r', 751 | offset=_headerLength, order='F', 752 | shape=(nx, ny, nz) 753 | ) 754 | return dat 755 | 756 | def write_header(self, header): 757 | """Write the values in the dict "header" to the file.""" 758 | for varname, info in six.iteritems(_headerDef): 759 | value = header.get(varname, info['default']) 760 | self._file.seek(info['offset']) 761 | write_binary(self._file, info['type'], value) 762 | 763 | def write_data(self, data): 764 | """Writes a geoprobe volume to disk.""" 765 | self._file.seek(_headerLength) 766 | # Write to file in Fortran order... 767 | # Not using data.ravel('F'), as it appears to make a copy if the 768 | # array is C-ordered (doubling memory usage). Instead, we just 769 | # write the transpose with the tofile method. (tofile writes in 770 | # C-order regardless of the order of the input array, thus 771 | # requring the transpose for both F and C ordered arrays) 772 | data.T.tofile(self._file) 773 | 774 | def close(self): 775 | return self._file.close() 776 | 777 | class VoxelGeoVolumeFile(GeoprobeVolumeFileV2): 778 | """ 779 | Appears to be identical to a geoprobe volume, but uses a different magic 780 | number. It's possible (and likely?) that there are other differences, but 781 | I don't have access to VoxelGeo to test and see. 782 | """ 783 | _magic_number = 43970 784 | 785 | class HDFVolumeFile(object): 786 | """Low level operations for reading and writing to hdf5 files.""" 787 | dataset_name = '/volume' 788 | def __init__(self, filename, mode): 789 | import h5py 790 | self.filename = filename 791 | self.mode = mode 792 | if 'b' in self.mode: 793 | self.mode = self.mode.replace('b', '') 794 | self._file = h5py.File(self.filename, self.mode) 795 | if 'w' not in self.mode: 796 | self.dataset = self._file[self.dataset_name] 797 | 798 | def is_valid(self): 799 | return self.dataset_name in self._file 800 | 801 | def read_header(self): 802 | return self._file.attrs 803 | 804 | def read_data(self): 805 | out = np.empty(self.dataset.shape, self.dataset.dtype) 806 | self.dataset.read_direct(out) 807 | return out 808 | 809 | def memmap_data(self): 810 | return self.dataset 811 | 812 | def write_header(self, header): 813 | for key, value in six.iteritems(header): 814 | self._file.attrs[key] = value 815 | 816 | def write_data(self, data): 817 | header = self.read_header() 818 | dx, dy, dz = [header[item] for item in ['dx', 'dy', 'dz']] 819 | x0, y0, z0 = [header[item] for item in ['x0', 'y0', 'z0']] 820 | 821 | # Because h5py doesn't support negative steps when indexing, we have to 822 | # reverse the x0, y0, z0 before writing and make all d's positive! 823 | if dz < 0: 824 | z0 = z0 + (data.shape[2] - 1) * dz 825 | data = data[:,:,::-1] 826 | if dy < 0: 827 | y0 = y0 + (data.shape[1] - 1) * dy 828 | data = data[:,::-1,:] 829 | if dx < 0: 830 | x0 = x0 + (data.shape[0] - 1) * dx 831 | data = data[::-1,:,:] 832 | 833 | self.write_header(dict(dx=abs(dx), dy=abs(dy), dz=abs(dz), 834 | x0=x0, y0=y0, z0=z0)) 835 | self.dataset = self._file.create_dataset(self.dataset_name, data=data) 836 | 837 | def close(self): 838 | return self._file.close() 839 | 840 | #-- Valid file formats -------------------------------------------------------- 841 | class GeoprobeVolumeV2(Volume): 842 | format_type = GeoprobeVolumeFileV2 843 | 844 | class VoxelGeoVolume(Volume): 845 | format_type = VoxelGeoVolumeFile 846 | 847 | class HDFVolume(Volume): 848 | format_type = HDFVolumeFile 849 | 850 | formats = [GeoprobeVolumeV2, VoxelGeoVolume] 851 | 852 | # If h5py is available, add HDFVolume's to the list of available formats 853 | try: 854 | import h5py 855 | formats.append(HDFVolume) 856 | except ImportError: 857 | pass 858 | 859 | 860 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = 'geoprobe', 5 | version = '0.4.0', 6 | description = "Reads and (partially) writes seismic data in Landmark's Geoprobe format", 7 | author = 'Joe Kington', 8 | author_email = 'joferkington@gmail.com', 9 | license = 'MIT', 10 | url = 'https://github.com/joferkington/python-geoprobe', 11 | packages = find_packages(), 12 | install_requires = ['numpy', 'six'], 13 | classifiers=[ 14 | "Programming Language :: Python :: 2", 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Intended Audience :: Science/Research", 19 | "Topic :: Scientific/Engineering", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /tests/test_swfault.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import geoprobe 4 | 5 | datadir = os.path.join(os.path.dirname(__file__), '..', 'examples', 'data/') 6 | faultdir = os.path.join(datadir, 'swFaults') 7 | 8 | class TestBasic: 9 | def setup(self): 10 | self.normal = geoprobe.swfault(faultdir + '/example_normal.swf') 11 | self.strike_slip = geoprobe.swfault(faultdir + '/example_ss.swf') 12 | self.empty = geoprobe.swfault(faultdir + '/empty.swf') 13 | 14 | def test_empty(self): 15 | x, y, z = self.empty.x, self.empty.y, self.empty.z 16 | assert np.allclose(x, []) 17 | assert np.allclose(y, []) 18 | assert np.allclose(z, []) 19 | 20 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | from geoprobe import utilities 2 | import geoprobe 3 | import numpy as np 4 | 5 | class TestBboxMethods: 6 | def setup(self): 7 | self.bbox1 = [ 0, 1, 1, 2] 8 | self.bbox2 = [0.5, 2, 1.5, 3] 9 | self.bbox3 = [10, 20, 30, 40] 10 | 11 | def test_intersects(self): 12 | assert utilities.bbox_intersects(self.bbox1, self.bbox2) 13 | assert not utilities.bbox_intersects(self.bbox1, self.bbox3) 14 | 15 | def test_intersection(self): 16 | overlap = utilities.bbox_intersection(self.bbox1, self.bbox2) 17 | assert overlap == [0.5, 1, 1.5, 2] 18 | assert utilities.bbox_intersection(self.bbox1, self.bbox3) is None 19 | 20 | def test_union(self): 21 | assert utilities.bbox_union(self.bbox1, self.bbox2) == [0, 2, 1, 3] 22 | assert utilities.bbox_union(self.bbox1, self.bbox3) == [0, 20, 1, 40] 23 | 24 | class TestPlaneFitting: 25 | def generate_random_basis(self, state=None): 26 | if state is None: 27 | state = np.random.RandomState() 28 | a, b = state.rand(2,3) 29 | c = np.cross(a, b) 30 | b = np.cross(b, c) 31 | a, b, c = [item / np.sqrt(np.sum(item**2)) for item in (a,b,c)] 32 | return np.vstack([a, b, c]).T 33 | 34 | def setup(self): 35 | x, y = np.mgrid[40:50, 30:50] 36 | z = np.ones_like(x) + 8 37 | x, y, z = (item.astype(np.float).ravel() for item in [x,y,z]) 38 | self.coords = np.vstack([x, y, z]).T 39 | self.x, self.y, self.z = x, y, z 40 | 41 | def test_xy_plane(self): 42 | norm = geoprobe.utilities.fit_plane(self.x, self.y, self.z) 43 | assert np.allclose(norm, [0, 0, 1, -9]) 44 | 45 | def test_xz_plane(self): 46 | xz_basis = np.array([[1, 0, 0], [0, 0, 1], [0, 1, 0]]).T 47 | x, y, z = self.coords.dot(np.linalg.inv(xz_basis)).T 48 | norm = geoprobe.utilities.fit_plane(x, y, z) 49 | assert np.allclose(norm, [0, 1, 0, -9]) 50 | 51 | def test_yz_plane(self): 52 | yz_basis = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]]).T 53 | x, y, z = self.coords.dot(np.linalg.inv(yz_basis)).T 54 | norm = geoprobe.utilities.fit_plane(x, y, z) 55 | assert np.allclose(norm, [1, 0, 0, -9]) 56 | 57 | def test_assortment(self): 58 | state = np.random.RandomState(1977) 59 | for _ in range(10): 60 | basis = self.generate_random_basis(state) 61 | x, y, z = self.coords.dot(np.linalg.inv(basis)).T 62 | norm = np.array(geoprobe.utilities.fit_plane(x, y, z)) 63 | a, b, c = basis[:,-1] 64 | expected = [a, b, c, -9] 65 | assert np.allclose(norm, expected) or np.allclose(-norm, expected) 66 | 67 | def test_isopach(): 68 | x, y = np.mgrid[50:100, 50:100] 69 | z1 = np.hypot(x - x.mean(), y - y.mean()) 70 | z2 = 2 * z1 71 | 72 | hor1 = geoprobe.horizon(x=x.ravel(), y=y.ravel(), z=z1.ravel()) 73 | hor2 = geoprobe.horizon(x=x.ravel(), y=y.ravel(), z=z2.ravel()) 74 | 75 | iso = geoprobe.utilities.create_isopach(hor2, hor1) 76 | assert np.allclose(iso.x, x.ravel()) 77 | assert np.allclose(iso.y, y.ravel()) 78 | assert np.allclose(z2 - z1, iso.grid) 79 | -------------------------------------------------------------------------------- /tests/test_volume.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import pytest 4 | import geoprobe 5 | 6 | 7 | class TestBase: 8 | def setup_method(self, method): 9 | self.basedir = os.path.dirname(geoprobe.__file__) 10 | self.datadir = os.path.join(self.basedir, '..', 'examples', 'data') 11 | volname = os.path.join(self.datadir, 'Volumes', 'example.vol') 12 | self.vol = geoprobe.volume(volname) 13 | 14 | class TestExtents(TestBase): 15 | def test_bounds_sanity(self): 16 | assert self.vol.xmin <= self.vol.xmax 17 | assert self.vol.ymin <= self.vol.ymax 18 | assert self.vol.zmin <= self.vol.zmax 19 | 20 | def test_bounds(self): 21 | assert self.vol.xmin == 2168.0 22 | assert self.vol.xmax == 2537.0 23 | assert self.vol.ymin == 4900.0 24 | assert self.vol.ymax == 5154.0 25 | assert self.vol.zmin == 2720.0 26 | assert self.vol.zmax == 3990.0 27 | 28 | def test_dx_dy_dz(self): 29 | """Ensure that bounds stay min <= max even when d's are negative.""" 30 | for item in ['dx', 'dy', 'dz']: 31 | setattr(self.vol, item, -getattr(self.vol, item)) 32 | self.test_bounds_sanity() 33 | setattr(self.vol, item, -getattr(self.vol, item)) 34 | 35 | 36 | class TestConversions(TestBase): 37 | def test_model2index_mins(self): 38 | assert self.vol.model2index(self.vol.xmin, axis='x') == 0 39 | assert self.vol.model2index(self.vol.ymin, axis='y') == 0 40 | assert self.vol.model2index(self.vol.zmin, axis='z') == 0 41 | 42 | def test_model2index_maxs(self): 43 | assert self.vol.model2index(self.vol.xmax, axis='x') == self.vol.nx - 1 44 | assert self.vol.model2index(self.vol.ymax, axis='y') == self.vol.ny - 1 45 | assert self.vol.model2index(self.vol.zmax, axis='z') == self.vol.nz - 1 46 | 47 | def test_dx_dy_dz(self): 48 | """Ensure that bounds work even when d's are negative.""" 49 | for item in ['dx', 'dy', 'dz']: 50 | setattr(self.vol, item, -getattr(self.vol, item)) 51 | self.test_model2index_mins() 52 | self.test_model2index_maxs() 53 | setattr(self.vol, item, -getattr(self.vol, item)) 54 | 55 | class TestCrop(TestBase): 56 | def test_crop_in_memory(self): 57 | self.vol.load() 58 | v = self.vol.crop(xmin=2200, xmax=2300, ymin=5000, ymax=5100, 59 | zmin=3000, zmax=3100) 60 | vdat = self.vol[2200:2300, 5000:5100, 3000:3100] 61 | assert np.allclose(v.data, vdat) 62 | 63 | class TestExtraction(TestBase): 64 | def test_arbitrary_section(self): 65 | x = [self.vol.xmin, self.vol.xmax] 66 | y = [self.vol.ymin, self.vol.ymax] 67 | dist = np.hypot(np.diff(x), np.diff(y)) 68 | section, xi, yi = self.vol.extract_section(x, y) 69 | assert section.shape == (int(dist), self.vol.nz) 70 | 71 | def test_arbitrary_section_interpolation_shape(self): 72 | x, y = self.vol.index2model([0, 2, 5, 10], [0, 0, 0, 0]) 73 | section, xi, yi = self.vol.extract_section(x, y) 74 | assert section.shape[0] == 10 75 | 76 | def test_arbitrary_section_interpolation_non_increasing(self): 77 | x, y = self.vol.index2model([0, 5, 2, 10], [0, 0, 0, 0]) 78 | section, xi, yi = self.vol.extract_section(x, y) 79 | assert section.shape[0] == 16 80 | 81 | def test_arbitrary_section_interpolation_zlimits(self): 82 | x, y = self.vol.index2model([0, 2, 5, 10], [0, 0, 0, 0]) 83 | zmin = self.vol.zmin + 50 84 | zmax = self.vol.zmax - 50 85 | section, xi, yi = self.vol.extract_section(x, y, zmin, zmax) 86 | assert section.shape == (10, 235) 87 | 88 | def test_xslice(self): 89 | slice1 = self.vol.XSlice(self.vol.xmin) 90 | slice2 = self.vol[self.vol.xmin, :, :].T 91 | slice3 = self.vol.data[0, :, :].T 92 | assert np.all(slice1 == slice2) 93 | assert np.all(slice2 == slice3) 94 | 95 | --------------------------------------------------------------------------------