#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
iotools.py: Contains input/output utilities/functions for pygeostat. Many of which
are based off of Pandas builtin functions.
'''
#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
import warnings
import pandas as pd
import numpy as np
from .. pygeostat_parameters import Parameters
[docs]
def read_file(flname, fltype=None, headeronly=False, delimiter=r'\s*', h5path=None, h5datasets=None,
columns=None, ireal=1, griddef=None, tmin=None):
'''
Reads in a GSLIB-style Geo-EAS data file, CSV, GSB or HDF5 data files.
Parameters:
flname (str): Path (or name) of file to read.
Keyword Args:
fltype (str): Type of file to read: either ``csv``, ``gslib``, or ``hdf5``.
headeronly (bool): If True, only reads in the 1st line from the data file
which is useful for just getting column numbers or testing. OR
it allows you to open a hdf5 object with Pandas HDFStore functionality
delimiter (str): Delimiter specified instead of sniffing
h5path (str): Forward slash (/) delimited path through the group hierarchy you wish to
read the dataset(s) specified by the argument ``datasets`` from. The dataset name
cannot be passed using this argument, it is interpreted as a group name only. A
value of ``None`` places the dataset into the root directory of the HDF5 file. A value
of ``False`` loads a blank pd.DataFrame().
h5datasets (str or list): Name of the dataset(s) to read from the group specified by
``h5path``. Does nothing if ``h5path`` points to a dataset.
column (list): List of column labels to use for resulting frame
ireal (int): Number of realizaitons in the file
griddef (GridDef): griddef for the realization
tmin (float): values less than this number are convernted to NaN, since NaN's are
natural handled within matplotlib, pandas, numpy, etc. If None, set to
pygeostat.Parameters['data.tmin'].
Returns:
data (pandas.DataFrame): Pandas DataFrame object with input data.
Note:
Functions can also be called seperately with the following code
>>> data.data = pygeostat.read_gslib(flname)
>>> data.data = pygeostat.read_csv(flname)
>>> data.data = pygeostat.read_h5(flname, h5path='')
>>> data.data = pygeostat.read_gsb(flname)
>>> data.data = pygeostat.open_hdf5(flname)
Examples:
>>> data.data = gs.read_gsb('testgsb.gsb')
>>> data = gs.DataFile('testgsb.gsb')
'''
# Infer filetype if none specified based on the file extention
if fltype is None:
import os
_, extention = os.path.splitext(flname)
extention = extention.lower()
if extention in ['.out', '.data']:
fltype = 'gslib'
elif extention in ['.csv']:
fltype = 'csv'
elif extention in ['.h5', '.hdf5', '.hd5']:
fltype = 'hdf5'
elif flname.lower().endswith('gsb') or flname.endswith('GSB'):
fltype = 'gsb'
else:
# Otherwise just assume GSLIB for now but this may change
fltype = 'gslib'
# Call specific read functions based on data type
if fltype == 'gslib':
data = read_gslib(flname=flname, headeronly=headeronly, delimiter=delimiter,
tmin=tmin)
elif fltype == 'csv':
data = read_csv(flname=flname, headeronly=headeronly, tmin=tmin)
elif fltype == 'hdf5' or fltype == 'hd5' or fltype == 'h5':
from .h5_io import read_h5
if headeronly:
data = pd.DataFrame()
else:
# Note that tmin isn't implemented for h5 yet, due to my inexperience
# with this underlying module/format
data = read_h5(flname=flname, h5path=h5path, datasets=h5datasets)
elif fltype == 'gsb':
data = read_gsb(flname=flname, ireal=ireal, tmin=tmin)
else:
print('File type unsupported! Try "gslib", "csv" or "hdf5"')
if columns is not None:
data.columns = columns
return data
def _test_file_open(filename):
""" test and raise if a file cannot be opened """
try:
with open(filename, 'r'):
pass
except FileNotFoundError:
raise FileNotFoundError('{} does not exist!'.format(filename))
except IOError:
raise IOError('Could not open {}'.format(filename))
[docs]
def read_gslib(flname, headeronly=False, delimiter=r'\s*', tmin=None):
'''Reads in a GSLIB-style Geo-EAS data file
Parameters:
flname (str): Path (or name) of file to read.
Keyword Args:
headeronly (bool): If True, only reads in the 1st line from the data file
which is useful for just getting column numbers or testing
delimiter (str): Delimiter specified instead of sniffing
tmin (float): values less than this number are convernted to NaN, since NaN's are
natural handled within matplotlib, pandas, numpy, etc. If None, set to
pygeostat.Parameters['data.tmin'].
Returns:
data (pandas.DataFrame): Pandas DataFrame object with input data.
'''
# Can the file be opened?
_test_file_open(flname)
# Only read in the header + 1 line of the data file?
if headeronly:
nrows = 1
else:
nrows = None
# Reading the header for a GSLIB "Geo-EAS" format file
with open(flname, 'r') as datafl:
# Geo-EAS Header
_ = datafl.readline().strip() # Title
nvar = int(datafl.readline().split()[0])
varnames = []
for _ in range(nvar):
varnames.append(datafl.readline().strip())
nrowtoskip = 2 + nvar
engine = Parameters['data.io.pandas_engine']
# Use the new pandas chunked reading method
# with the "python" engine since the C engine sometimes does not work with
# realizations with trailing spaces
try:
tpdf = pd.read_csv(flname, skiprows=nrowtoskip, header=None,
delimiter=delimiter, skipinitialspace=True,
nrows=nrows, engine=engine, chunksize=100000)
data = pd.concat(tpdf, ignore_index=True)
# Fallback to old method using the python engine
except (ValueError, NotImplementedError):
data = pd.read_csv(flname, skiprows=nrowtoskip, header=None, delimiter=r'\s*',
skipinitialspace=True, nrows=nrows, engine='python')
# Replace values below tmin with nan
data = _data_trim(data, tmin=tmin)
# Assign variable names
data.columns = varnames
# return only the Pandas DataFrame
return data
[docs]
def read_csv(flname, headeronly=False, tmin=None):
'''Reads in a GSLIB-style CSV data file.
Parameters:
flname (str): Path (or name) of file to read.
Keyword Args:
headeronly (bool): If True, only reads in the 1st line from the data file
which is useful for just getting column numbers or testing
delimiter (str): Delimiter specified instead of sniffing
tmin (float): values less than this number are convernted to NaN, since NaN's are
natural handled within matplotlib, pandas, numpy, etc. If None, set to
pygeostat.Parameters['data.tmin'].
Returns:
data (pandas.DataFrame): Pandas DataFrame object with input data.
'''
if headeronly:
nrows = 1
else:
nrows = None
# Reading a GSLIB "Geo-EAS" format file
tpdf = pd.read_csv(flname, header=0, nrows=nrows, engine='c', chunksize=100000)
data = pd.concat(tpdf, ignore_index=True)
# Replace values below tmin with nan
data = _data_trim(data, tmin=tmin)
# Only return the DataFrame
return data
[docs]
def compile_pygsb():
'''
Compiles 'pygeostat/fortran/src/pygsb.f90' using 'pygeostat/fortran/compile.py'
and tries to import pygsb.pyd
Note:
How to install a gfortran compiler:
- Install chocolatey from:
chocolatey.org/install
(chocolatey is a package manager that let you install software using command prompt and PowerShell)
- After installing chocolatey, then install the ‘gnu Fortran compiler’ by writing the below in a PowerShell:
choco install mingw --version 8.1
choco install visualstudio2019community
choco install visualstudio2019-workload-vctools
- When installing "mingw" through "chocolatey", ensure that the path of the "mingw" 's "bin" folder is added to the environment variables path.
'''
import os
import subprocess
cwd1 = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fortran'))
if not os.path.isfile(os.path.join(cwd1,'pygsb.pyd')):
compiler = 'gnu'
subprocess.call('python compile.py -clean pygsb', cwd=cwd1)
subprocess.call('python compile.py -compiler={} pygsb'.format(compiler), cwd=cwd1)
else:
# Ensure the right version of .pyd exists.
try:
from ..fortran import pygsb as pygsb
except ImportError:
compiler = 'gnu'
subprocess.call('python compile.py -clean pygsb', cwd=cwd1)
subprocess.call('python compile.py -compiler={} pygsb'.format(compiler), cwd=cwd1)
try:
from ..fortran import pygsb as pygsb
except ImportError:
raise ImportError("Could not import 'pygsb' from 'pygeostat.fortran'('pygsb.f90' did not compile to create 'pygsb.pyd'. Consider installing a gfortran compiler with tools such as mingw (see documnetation to help you how install the Fortran compiler).")
return pygsb
def isbinary(file):
"""
From http://stackoverflow.com/a/7392391/5545005
Its hard to understand what's going on here.. but it seems to work for gsb files ....
H5 has a handy check but this fills the gap for gsb files when trying to read ascii
"""
textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
def is_binary_string(bytes):
return bool(bytes.translate(None, textchars))
return is_binary_string(open(file, 'rb').read(1024))
[docs]
def read_gsb(flname, ireal=-1, tmin=None, null=None):
'''Reads in a CCG GSB (GSLIB-Binary) file.
Parameters:
flname (str): Path (or name) of file to read.
Keyword Args:
ireal (int): 1-indexed realization number to read (reads 1 at a time), -1 to read all
tmin (float): values less than this number are convernted to NaN, since NaN's are
natural handled within matplotlib, pandas, numpy, etc. If None, set to
pygeostat.Parameters['data.tmin'].
null (float): when the gsb array has a keyout, on reconstruction this value fills the array
in keyed out locations. If `None` taken from Parameters['data.null']
Returns:
data (pandas.DataFrame): Pandas DataFrame object with input data.
.. codeauthor:: Jared Deutsch 2016-02-19
'''
pygsb = compile_pygsb()
# Can the file be opened?
_test_file_open(flname)
if not isbinary(flname):
raise ValueError('The file %s appears to be non-binary formatted ' % flname)
# Load required header information
nvar, nx, ny, nz, nreal, errorvalue = pygsb.pyreadgsbheader(flname)
if errorvalue != 0:
raise AssertionError("Error reading GSB header!, Error #{}".format(errorvalue))
# RMB - fixing an observed bug, which occurs if any of the dimensions are zero
if nx == 0:
nx = 1
if ny == 0:
ny = 1
if nz == 0:
nz = 1
if ireal == 0:
raise ValueError(('ireal shoud be greater than 1 (read a specified 1-index realization)'
'or -1 (read all realizations)'))
if null is None:
null = Parameters.get('data.null')
if null is None:
null = -999.99 # matching the GSB fortran defaults
# Get the data
nxyz = nx * ny * nz
if ireal <= -1:
errorvalue, reals, vnames = pygsb.pyreadgsbdata(flname, nvar, nxyz, 1, null)
for ireal in range(1, nreal):
errorvalue, values, vnames = pygsb.pyreadgsbdata(
flname, nvar, nxyz, ireal + 1, null)
reals = np.append(reals, values, axis=1)
else:
errorvalue, reals, vnames = pygsb.pyreadgsbdata(flname, nvar, nxyz, ireal, null)
if errorvalue != 0:
raise AssertionError("Error reading GSB data!, Error #{}".format(errorvalue))
# Clean up column names and trimmed data
vnames = [''.join([v.decode("utf-8") for v in vname]).strip() for vname in vnames]
# Convert to pandas dataframe
data = pd.DataFrame(data=reals.transpose(), columns=vnames)
# Replace values below tmin with nan
data = _data_trim(data, tmin)
# Only return the DataFrame
return data
def _data_trim(data, tmin):
"""Replace values less than tmin with NaN. This routine is private
to the iotools module, since it is only intended for use on the
import of data.
Parameters:
data (pandas.DataFrame): data to trim
tmin (int or float): values less than tmin are assigned np.nan
"""
from pandas.api.types import is_numeric_dtype
if tmin is None:
# Use the Parameters tmin, which may also be None (leading to no trimming)
tmin = Parameters['data.tmin']
if tmin is None or not tmin:
return data
if not isinstance(tmin, int) and not isinstance(tmin, float):
raise ValueError('tmin must be an integer or float!')
if tmin is not None:
if Parameters['data.fix_legacy_null'] and Parameters['data.null'] is not None:
warned = False
for legacy_null in Parameters['data.legacy_null']:
if not warned and legacy_null != Parameters['data.null']:
for col in data.columns:
if is_numeric_dtype(data[col]):
if (np.isclose(data[col], legacy_null)).any():
warnings.warn(
f'found {legacy_null} in {col} during read!')
warned = True
break
data.replace(legacy_null, Parameters['data.null'], inplace=True)
for col in data.columns:
if is_numeric_dtype(data[col]):
data.loc[data[col] < tmin, col] = np.nan
return data
[docs]
def write_gslib(data, flname, title=None, variables=None,
fmt=None, sep=' ', null=None):
"""Writes out a GSLIB-style data file.
Parameters:
data (pygeostat.DataFile or pandas.DataFrame): data to write out
flname (str): Path (or name) of file to write out.
Keyword Args:
title (str): Title for output file.
variables (List(str)): List of variables to write out if only a subset is desired.
fmt (str): Format to use for floating point numbers.
sep (str): Delimiter to use for file output, generally don't need to change.
null (float): NaN numbers are converted to this value prior to writing. If None, set
to data.null. If data.Null is None, set to pygeostat.Parameters['data.null'].
"""
from .data import DataFile as DataFile
data = _data_fillnan(data, null)
# If a DataFile is used, check the arguments
if isinstance(data, DataFile):
flname, variables, sep, fltype = data.check_datafile(flname, variables,
sep, 'gslib')
else:
# If variables is none, then get the columns
if variables is None:
variables = data.columns.tolist()
# If title is STILL none after introspection, then at least include something
if title is None:
title = 'pygeostat_saved_data'
if fmt is None:
fmt = Parameters.get('data.write.python_floatfmt', '%.5f')
# Force a write
with open(flname, 'w') as outfl:
# GSLIB Header
outfl.write(title.strip() + '\n' + str(len(variables)) + '\n')
for variable in variables:
outfl.write(str(variable) + '\n')
# GSLIB separated data
if isinstance(data, DataFile):
data.data[variables].to_csv(outfl, header=False, index=False, sep=sep,
float_format=fmt, lineterminator='\n')
else:
data[variables].to_csv(outfl, header=False, index=False, sep=sep,
float_format=fmt, lineterminator='\n')
[docs]
def write_csv(data, flname, variables=None,
fmt='%.5f', sep=',', fltype='csv', null=None):
"""Writes out a CSV or Excel (XLSX) data file.
Parameters:
data (pygeostat.DataFile or pandas.DataFrame): data to write out
flname (str): Path (or name) of file to write out.
Keyword Args:
variables (List(str)): List of variables to write out if only a subset is desired.
fmt (str): Format to use for floating point numbers.
sep (str): Delimiter to use for file output, generally don't need to change.
fltype (str): Type of file to write either ``csv`` or ``xlsx``.
null (float): NaN numbers are converted to this value prior to writing. If None, set
to data.null. If data.Null is None, set to pygeostat.Parameters['data.null'].
"""
from .data import DataFile as DataFile
data = _data_fillnan(data, null)
# If a DataFile is used, check the arguments
if isinstance(data, DataFile):
flname, variables, sep, fltype = data.check_datafile(flname, variables,
sep, fltype)
else:
# If variables is none, then get the columns
if variables is None:
variables = data.columns.tolist()
# Force a write
with open(flname, 'w') as outfl:
if fltype.lower() == 'csv':
# CSV using pandas native CSV writer
if isinstance(data, DataFile):
data.data[variables].to_csv(outfl, header=True, index=False, sep=sep,
float_format=fmt, lineterminator='\n')
else:
data[variables].to_csv(outfl, header=True, index=False, sep=sep,
float_format=fmt, lineterminator='\n')
elif (fltype.lower() == 'xlsx') or (fltype.lower() == 'excel'):
# Excel file writer - only xlsx is supported naturally in pygeostat,
# but XLWT could be used if XLS files needed to be created
# NOTE: this requires the Python package openpyxl
if isinstance(data, DataFile):
data.data[variables].to_excel(flname, header=True, index=False,
float_format=fmt)
else:
data[variables].to_excel(flname, header=True, index=False,
float_format=fmt)
[docs]
def write_gsb(data, flname, tvar=None, nreals=1, variables=None, griddef=None, fmt=0):
"""
Writes out a GSB (GSLIB-Binary) style data file. NaN values of tvar are compressed
in the output with no tmin now provided.
Parameters:
data (pygeostat.DataFile or pandas.DataFrame): data to write out
flname (str): Path (or name) of file to write out.
tvar (str): Variable to trim by or None for no trimming. Note that all variables are
trimmed in the data file (for compression) when this variable is trimmed.
nreals (int): number of realizations in data
Keyword Args:
griddef (pygeostat.griddef.GridDef): This is required if the data is gridded and you
want other gsb programs to read it
fmt (int): if 0 then will write out all variables as float 64. Otherwise should be
an list with a length equal to number of variables and with the following format codes
1=int32, 2=float32, 3=float64
variables (List(str)): List of variables to write out if only a subset is desired.
.. codeauthor:: Jared Deutsch 2016-02-19, modified by Ryan Barnett 2018-04-12
"""
from .data import DataFile as DataFile
pygsb = compile_pygsb()
null = Parameters.get('data.null', None)
data = _data_fillnan(data, null)
# If variables is none, then get the columns
# Also configure the data for output
if not isinstance(data, DataFile):
if variables is None:
variables = data.columns.tolist()
datamat = np.array(data[variables].values).transpose()
else:
if griddef is None:
if data.griddef is not None:
griddef = data.griddef
if variables is None:
variables = data.data.columns.tolist()
else:
if isinstance(variables, str):
variables = [variables]
datamat = np.array(data.data[variables].values).transpose()
# format
if fmt == 0 or fmt is None:
vkinds = [3 for x in range(len(variables))]
elif not isinstance(fmt, list):
raise ValueError("fmt needs to be a list. You passed a %s" % type(fmt))
else:
vkinds = []
for f in fmt:
vkinds.append(f)
# Dimensioning
nvar = len(variables)
if griddef is not None:
nx = griddef.nx
ny = griddef.ny
nz = griddef.nz
if np.size(datamat, 1) != griddef.count() * nreals:
raise ValueError("the passed data has the wrong number of elements for nx, ny, nz, "
"nreal: %i %i %i %i" % (nx, ny, nz, nreals))
else:
if nreals == 1:
nx = len(data[variables[0]])
ny = 1
nz = 1
else:
nx = np.size(datamat, 1) / nreals
ny = 1
nz = 1
# Trimming
if isinstance(tvar, str):
tvar = variables.index(tvar) + 1
if tvar <= 0:
raise ValueError("Could not trimming variable {} in variable list {}".format(tvar,
variables))
elif tvar is None:
tvar = 1
elif isinstance(tvar, bool):
if tvar:
tvar = 1
else:
tvar = 0
elif tvar == 0:
tvar = 1
else:
raise ValueError("Invalid trimming variable {}".format(tvar))
# Character conversion of strings
cvariables = []
for varname in [var.ljust(64) for var in variables]:
cvariables.append([v for v in varname])
tmin = Parameters.get('data.tmin', None)
if tmin is None:
tmin = -1e21
# Can the file be opened?
try:
_test_file_open(flname)
except FileNotFoundError:
pass # this case is okay .. I guess we're just checking if we can open it?
# Save out the data
errorvalue = pygsb.pywritegsbdata(gsbfl=flname, datamat=datamat, vnames=cvariables, nvar=nvar,
nreal=nreals, nx=nx, ny=ny, nz=nz, tmin=tmin,
tmax=1.0e21, tvar=tvar, vkinds=vkinds)
if errorvalue != 0:
raise AssertionError("Error writing GSB data!, Error #{}".format(errorvalue))
[docs]
def write_vtk(data, flname, dftype=None, x=None, y=None, z=None, variables=None, griddef=None,
null=None, vdtype=None, cdtype=None):
"""
Writes out an XML VTK data file. A required dependency is pyevtk, which may be installed using
the following command:
>>> pip install pyevtk
Users are also recommended to install the latest Paraview, as versions from 2017 were
observed to have odd precision bugs with the XML format.
Parameters:
data (pygeostat.DataFile): data to write out
flname (str): Path (or name) of file to write out (without extension)
Keyword Args:
dftype (str): type of datafile options ``grid`` or ``point``, which if None,
is drawn from data.dftype
x (str): name of the x-coordinate, which is used if ``point``. Drawn
from data.x if the kwarg=None. If not provided by these means for ```sgrid```,
calculated via ``sim.griddef.get_coordinates()``.
y (str): name of the y-coordinate, which is used if ``point``. Drawn
from data.y if the kwarg=None. If not provided by these means for ```sgrid```,
calculated via sim.griddef.get_coordinates().
z (str): name of the z-coordinate, which is used if ``point``. Drawn
from data.z if the kwarg=None. If not provided by these means for ```sgrid```,
calculated via sim.griddef.get_coordinates().
griddef (pygeostat.GridDef): grid definition, which is required if ``grid``.
Drawn from data.griddef if the kwarg=None.
variables (list or str): List or string of variables to write out. If None, then all
columns aside from coordinates are written out by default.
null (float): NaNs are converted to this value prior to writing. If None, set to
pygeostat.Parameters['data.null_vtk'].
vdtype (dict(str)) : Dictionary of the format {'varname': dtype}, where dtype is
a numpy data format. May be used for reducing file size, by
specifying ``int``, ``float32``, etc. If a format string is provided instead
of a dictionary, that format is applied to all variables. This is not applied
to coordinate variables (if applicable). If None, the value is drawn from
Parameters['data.write_vtk.vdtype'].
cdtype (str) : Numpy format to use for the output of coordinates, where valid formats
are ``float64`` (default) and ``float32``. The later is recommended for reducing
file sizes, but may not provide the requisite precision for UTM coordinates. If None,
the value is drawn from Parameters['data.write_vtk.cdtype'].
``dftype`` should be one of:
1. 'point' (irregular points) where ``data.x``, ``data.y`` and ``data.z`` are columns in ``data.data``
2. 'grid' (regular or rectilinear grid) where ``data.griddef`` must be initialized
3. 'sgrid' (structured grid) where ``data.x``, ``data.y`` and ``data.z`` are columns in ``data.data``. ``data.griddef`` should also be initialized, although only ``griddef.nx``, ``griddef.ny`` and ``griddef.nz`` are utilized (since the grid is assumed to not be regular)
"""
from copy import deepcopy
# Ensure dependencies are satisfied
try:
from pyevtk.hl import pointsToVTK, gridToVTK
except:
raise Exception('Looks like pyevtk is not installed, use:\n' +
'>>> pip install pyevtk\n')
# Place in a new variable so potential changes aren't returned
dat1 = deepcopy(data)
# Ensure valid VTK option
if dftype is None:
if dat1.dftype == 'point':
dftype = 'point'
elif dat1.dftype == 'grid':
dftype = 'grid'
elif dat1.dftype == 'sgrid':
dftype = 'sgrid'
else:
raise ValueError(" specified data.dftype is not yet available or invalid;\n" +
" see the docstring!")
else:
if not any([dftype == test for test in ['point', 'grid', 'sgrid']]):
raise ValueError(" specified dftype is not yet available or invalid;\n" +
" see the docstring!")
# Ensure a grid is present if necessary
if dftype in ['grid', 'sgrid']:
if griddef is None:
griddef = dat1.griddef
if griddef is None:
raise ValueError(('griddef must be a kwarg or exist as data.griddef if '
'dftype is grid or sgrid!'))
# Ensure coordinates are present if necessary
if dftype in ['point', 'sgrid']:
if x is None:
x = dat1.x
if y is None:
y = dat1.y
if z is None:
z = dat1.z
test = dict(x=x, y=y, z=z)
if dftype == 'point':
for coord in test.keys():
if test[coord] is None:
raise ValueError(('{} must be a kwarg or exist as data.{} if'
'dftype is point'.format(coord, coord)))
else:
t = [t is None for t in test.values()]
if all(t):
raise ValueError(('at least one grid coordinate should be irregular (specified '
'as a column in data) if using dftype="sgrid". Specify one '
'or multiple irregular coordinate columns, or use dftype="grid"'))
else:
# A coordinate is not provided, so assume it follows the regular grid
tx, ty, tz = griddef.get_coordinates()
if x is None:
x, dat1['X'] = 'X', tx
if y is None:
y, dat1['Y'] = 'Y', ty
if z is None:
z, dat1['Z'] = 'Z', tz
# Replace nan with null values
if null is None:
null = Parameters['data.null_vtk']
dat1 = _data_fillnan(dat1, null)
# Check the grid length
if dftype != 'point':
nx = griddef.nx
ny = griddef.ny
nz = griddef.nz
nxyz = nx * ny * nz
if dat1.shape[0] != nxyz:
raise ValueError('nx*ny*nz should be equal to dat1.shape[0]!')
if vdtype is None:
vdtype = Parameters['data.write_vtk.vdtype']
if cdtype is None:
cdtype = Parameters['data.write_vtk.cdtype']
# Generate a dictionary of the variables, including their speficied precision
columns = dat1.data.columns.tolist()
if variables is None:
variables = []
for col in columns:
if col == x or col == y or col == z:
continue
variables.append(col)
if len(variables) == 0:
# Paraview requires some dat1...
variables = [x]
elif isinstance(variables, str):
variables = [variables]
# Generate a dictionary of variables precisions
vdtyped = {col: 'float64' for col in columns}
if isinstance(vdtype, dict):
for var in vdtype:
vdtyped[var] = vdtype[var]
elif isinstance(vdtype, str):
for var in vdtyped:
if var == x or var == y or var == var == z:
vdtyped[var] = cdtype
else:
vdtyped[var] = vdtype
vardict = dict()
for var in variables:
if any(var in i for i in columns):
t = np.ascontiguousarray(dat1.data[var].values)
vardict[var] = t.astype(vdtyped[var])
if dftype != 'point':
vardict[var] = vardict[var].reshape((nx, ny, nz), order='F')
else:
raise KeyError(var + " is not a column name in dat1.data!")
# General checks and variable assembly completed - write out the
# data type
if dftype == 'point':
# Require specified coodinates for each point
if any(x in i for i in columns):
x = np.ascontiguousarray(dat1.data[x].values)
x = x.astype(cdtype)
else:
raise KeyError(" x is not a column name in data.data!")
if any(y in i for i in columns):
y = np.ascontiguousarray(dat1.data[y].values)
y = y.astype(cdtype)
else:
raise KeyError(" y is not a column name in data.data!")
if any(z in i for i in columns):
z = np.ascontiguousarray(dat1.data[z].values)
z = z.astype(cdtype)
else:
raise KeyError(" z is not a column name in data.data!")
if dftype == 'point':
pointsToVTK(flname, x, y, z, data=vardict)
elif dftype == 'grid':
# Rectilinear VTK type
grid_arr = data.griddef.grid_array
xsiz, ysiz, zsiz = grid_arr[2], grid_arr[5], grid_arr[8]
xmin, ymin, zmin = grid_arr[1] - xsiz * .5, grid_arr[4] - \
ysiz * .5, grid_arr[7] - zsiz * .5
xmax, ymax, zmax = xmin + nx * xsiz, ymin + ny * ysiz, zmin + nz * zsiz
x = np.arange(xmin, xmax + xsiz, xsiz, dtype=cdtype)
y = np.arange(ymin, ymax + ysiz, ysiz, dtype=cdtype)
z = np.arange(zmin, zmax + zsiz, zsiz, dtype=cdtype)
gridToVTK(flname, x, y, z, cellData=vardict)
else:
pass
def _data_fillnan(data, null):
"""Replace nan values prior to writing out. Private since only intended
for use within the iotools
Parameters:
data (pandas.DataFrame or pygeostat.DataFrame): data that may have NaN values
null (int or float): NaN occurence are replaced with this value
"""
import pygeostat as gs
import copy
if null is None:
if isinstance(data, gs.DataFile):
if data.null is None:
# Use the Parameters null, which may also be None (leading to no nan assignment)
null = Parameters['data.null']
else:
# Use the DataFile.null, which may also be None (leading to no nan assignment)
null = data.null
else:
null = Parameters['data.null']
if null is None or not null:
return data
if not isinstance(null, int) and not isinstance(null, float):
raise ValueError('null must be an integer or float!')
if null is not None:
data = copy.deepcopy(data)
if isinstance(data, gs.DataFile):
data.data.fillna(value=null, inplace=True)
elif isinstance(data, pd.DataFrame):
data.fillna(value=null, inplace=True)
else:
# Shouldn't get here... but just in case
raise ValueError('only DataFrame and DataFile is implemented!')
return data
[docs]
def write_hvtk(data, flname, griddef, variables=None):
"""
Writes out an H5 file and corresponding xdmf file that Paraview can read. Currently only
supports 3D gridded datasets. This function will fail if the length of the DataFile or
DataFrame does not equal ``griddef.count()``.
The extension xdmf is silently enforced. Any other extension passed is replaced.
Parameters:
data (pd.DataFrame): The DataFrame to writeout
flname (str): Path (or name) of file to write out.
griddef (GridDef): Grid definitions for the realizations to be written out
variables (str or list): optional set of variables to write out from the DataFrame
"""
import os
import subprocess
from .h5_io import write_h5
# setup the temporary strings to write things too:
temp_attr = """ <Attribute Name="{varname}"
AttributeType="Scalar"
Center="Cell">
<DataItem Dimensions="{nx} {ny} {nz}" NumberType="Float" Precision="8" Format="HDF">
{h5flname}:{attrvarpath}
</DataItem>
</Attribute>"""
xmlstr = """<?xml version="1.0" ?>
<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>
<Xdmf Version="2.0">
<Domain>
<Grid Name="mesh" GridType="Uniform">
<Topology TopologyType="3DCoRectMesh" Dimensions="{nz} {ny} {nx}"></Topology>
<Geometry Type="ORIGIN_DXDYDZ">
<DataItem Format="XML" Dimensions="{dim}">{zmn} {ymn} {xmn}</DataItem>
<DataItem Format="XML" Dimensions="{dim}">{zsiz} {ysiz} {xsiz}</DataItem>
</Geometry>
{attributes}
</Grid>
</Domain>
</Xdmf>
"""
# Checks
if not isinstance(data, pd.DataFrame):
print('ERROR: Pass a pd.DataFrame for writeout!')
return
if 1 in (griddef.nx, griddef.ny, griddef.nz) or len(data) != griddef.count():
print('ERROR: This function only supports 3D grids! Check data or griddefs!')
return
attributes = ''
ext = flname[flname.rfind('.'):]
if ext.lower() != '.xdmf':
flname = flname.replace(ext, '.xdmf')
print('WARNING: Paraview h5-reader likes xdmf extensions - \nnew file'
' is: %s' % flname)
outh5name = flname.replace('.xdmf', '.hvtk')
# Always overwrites the old hvtk file, but has to clear it first
if os.path.isfile(outh5name):
try:
os.remove(outh5name)
except:
try:
subprocess.call('rm ' + outh5name)
except:
print('ERROR: os.remove failed, and `rm` not found on this system. Please '
'remove %s manually' % outh5name)
return
# do some string enchantments
if '/' in outh5name or '\\' in outh5name:
h5path = outh5name
# take `\` and `/` out of the path
while '/' in h5path:
h5path = h5path[h5path.rfind('/') + 1:]
while '\\' in h5path:
h5path = h5path[h5path.rfind('\\') + 1:]
else:
h5path = outh5name
h5path = h5path.replace(' ', '')
# write out all the data in the dataframe if no variables are passed
if variables is not None:
if isinstance(variables, str):
variables = [variables]
data = data[variables]
# write out each variable to the hvtk data storage
for var in data.columns:
variable = var.lower().replace(' ', '')
varpath = 'data/%s' % variable
write_h5(data[var].values, outh5name, h5path=varpath)
varpath += '/data'
attributes += temp_attr.format(nx=griddef.nx, ny=griddef.ny, nz=griddef.nz,
h5flname=h5path, varname=variable,
attrvarpath=varpath) + '\n'
# write out the XDMF pointing to the correct attributes in the hvtk
xmlstr_nx = griddef.nx + 1
xmlstr_ny = griddef.ny + 1
xmlstr_nz = griddef.nz + 1
xmlstr_xmn = griddef.xmn - (griddef.xsiz / 2)
xmlstr_ymn = griddef.ymn - (griddef.ysiz / 2)
xmlstr_zmn = griddef.zmn - (griddef.zsiz / 2)
xmlstr_xsiz = griddef.xsiz
xmlstr_ysiz = griddef.ysiz
xmlstr_zsiz = griddef.zsiz
xmlstr = xmlstr.format(attributes=attributes, nx=xmlstr_nx, ny=xmlstr_ny, nz=xmlstr_nz,
xmn=xmlstr_xmn, ymn=xmlstr_ymn, zmn=xmlstr_zmn, xsiz=xmlstr_xsiz,
ysiz=xmlstr_ysiz, zsiz=xmlstr_zsiz, dim=3)
with open(flname, 'w') as fh:
fh.write(xmlstr)
[docs]
def file_nlines(flname):
'''Open a file and get the total number of lines. Seems pretty fast. Copied from stackoverflow
http://stackoverflow.com/questions/845058/how-to-get-line-count-cheaply-in-python
Parameters:
flname (str): Name of the file to read
'''
f = open(flname)
lines = 0
buf_size = 1024 * 1024
read_f = f.read
buf = read_f(buf_size)
while buf:
lines += buf.count('\n')
buf = read_f(buf_size)
return lines
[docs]
def writeout_gslib_gmm(gmm, outfile):
"""
Writeout a fitted Gaussian mixture to the format consistent with ``gmmfit`` from the CCG
Knowledge Base. Assume ``gmm`` is a an ``sklearn.mixture.GaussianMixture`` class fitted to data
Note:
Recently GMM was replaced with GaussianMixture, and there are subtle differences in
attributes between the different versions..
Parameters:
gmm (GaussianMixture): a fitted mixture model
outfile (str): the output file
"""
if not gmm.converged_:
print('GMM has not converged! ')
return
gmmstr = ''
nvar = np.size(gmm.means_, 1)
# write the header:
gmmstr += 'sklearn.mixture.GMM Model for CCG gmm programs \n'
gmmstr += '%i %i \n' % (gmm.n_components, nvar)
for i in range(gmm.n_components):
# weights on one line
gmmstr += '%.16g %.16g \n' % (0.0, gmm.weights_[i])
# means on one line
gmmstr += ' '.join(['%.16g' % val for val in gmm.means_[i, :]])
gmmstr += '\n'
# covariance on another line
for j in range(nvar):
for k in range(j, nvar):
gmmstr += '%.16g ' % gmm.covariances_[i, j, k]
gmmstr += '\n'
with open(outfile, 'w') as file:
file.write(gmmstr)
file.close()
def readvarg(vargflname, vargnum):
"""
Reads in a variogram file and returns a pygeostat data file or list of pygeostat DataFiles with
the variograms
The parameter `vargnum` can be one of the following:
* a single variogram number to return one pygeostat DataFile
* a list of variogram numbers to return a list of DataFiles
* the string 'all' to return a list with all variograms
The returned `gs.DataFrame` objects contain the following parameters:
* data['Number'] - lag number
* data['Distance'] - lag distance
* data['Value'] - variogram value
* data['Points'] - number of points
* data['Head'] - head value of variogram
* data['Tail'] - tail value of variogram
* tailname - tail variable name
* headname - head variable name
* direction - direction number
or for gamv2004 variograms:
* azm - azimuth
* azmtol - azimuth tolerance
* azmbandw - azimuth bandwidth
* dip - dip
* diptol - dip tolerance
* dipbandw - dip bandwidth
* lags - number of lags
* lagdist - lag distance
* lagtol - lag tolerance
* vartype - variogram type
* tailvar - tail variable number
* headvar - head variable number
* indcatcut - indicator category or cutoff (None if missing)
Parameters:
vargflname: name of variogram file in either gamv or gamv2004 style
vargnum: permissible vargnum parameter as specified above
Returns:
vargs: A gs.DataFile (or list of DataFiles) with data set to the variogram data as
as specified above
"""
import io
from . data import DataFile
# What variogram number/numbers are we reading:
if isinstance(vargnum, str):
if vargnum.lower() == 'all':
# Count the number of variograms
vargnum = 1
with open(vargflname, 'r') as vargfl:
for line in vargfl:
if line[:1].isalpha():
vargnum += 1
vargnum = list(range(1, vargnum))
else:
raise Exception('Invalid vargnum', vargnum)
# Convert vargnum to list if just reading a single variogram
if isinstance(vargnum, int):
vargnum = [vargnum]
# Read in each variogram
vargidx = 0
vargs = []
with open(vargflname, 'r') as vargfl:
savedline = None
for line in vargfl:
if not (line[:1].isalpha()) and savedline is None:
continue
# Increment the variogram index number
vargidx += 1
# Are we reading this variogram?
if vargidx not in vargnum:
savedline = None
continue
varg = DataFile(flname=vargflname, readfl=False, fltype='gslib')
varg.vartype = '-1'
# Get the head variable, tail variable and direction
if savedline is None:
headerline = line
else:
headerline = savedline
# Try and get header data, might not work for vmodel outputs
try:
varg.tailname = headerline.partition('tail:')[2].partition('head:')[0].strip()
if 'direction:' in headerline:
varg.headname = headerline.partition('head:')[2].partition('direction:')[0].strip()
else:
varg.headname = headerline.partition('head:')[2].partition('direction')[0].strip()
varg.direction = int(headerline.partition('direction')[2])
except ValueError:
pass
if savedline is None:
line = next(vargfl)
# Check for gamv2004 output type
if line.startswith('-HDIR'):
# Read in gamv2004 specific information
varg.azm = line.split()[1]
varg.azmtol = line.split()[2]
varg.azmbandw = line.split()[3]
line = next(vargfl)
varg.dip = line.split()[1]
varg.diptol = line.split()[2]
varg.dipbandw = line.split()[3]
line = next(vargfl)
varg.lags = line.split()[1]
varg.lagdist = line.split()[2]
varg.lagtol = line.split()[3]
line = next(vargfl)
varg.vartype = line.split()[1]
varg.tailvar = line.split()[2]
varg.headvar = line.split()[3]
try:
varg.indcatcut = line.split()[3]
except IndexError:
varg.indcatcut = None
vargstr = next(vargfl)
else:
vargstr = line
while True:
line = None
# Try and read the next line
try:
line = next(vargfl)
except StopIteration:
break
# Add this line to the variogram string
if line is not None:
if not line[:1].isalpha():
vargstr = vargstr + line
else:
savedline = line
break
iovarg = io.StringIO(vargstr)
# Read in the data
if varg.vartype.strip() != '4':
varg.data = pd.read_csv(iovarg, header=None, delim_whitespace=True,
skipinitialspace=True,
names=['Number', 'Distance', 'Value', 'Points',
'Head', 'Tail'],
engine='python')
else:
varg.data = pd.read_csv(iovarg, header=None, delim_whitespace=True,
skipinitialspace=True,
names=['Number', 'Distance', 'Value', 'Points',
'Head', 'Tail', 'HeadVar', 'TailVar'],
engine='python')
vargs.append(varg)
# Return with variogram(s)
if len(vargs) == 1:
return vargs[0]
else:
return vargs