# -*- coding: utf-8 -*-
# License: BSD-3-Clause
# Author: LKouadio <etanoyau@gmail.com>
# Created date: Thu Sep 22 10:50:13 2022
"""
Core geology
=====================
The core module deals with geological data, the structural infos
and borehole data.
"""
from __future__ import annotations
import os
import warnings
from importlib import resources
import numpy as np
from ..utils.baseutils import get_remote_data
from ..utils.funcutils import (
ellipsis2false,
smart_format,
is_iterable,
key_search,
)
from ..utils.geotools import (
find_similar_structures,
)
from ..property import (
Config
)
from ..utils.validator import _is_arraylike_1d
from ..exceptions import (
GeoPropertyError,
)
from .._watexlog import watexlog
from .._typing import List
_logger = watexlog().get_watex_logger(__name__ )
__all__=[
"GeoBase",
"set_agso_properties",
"mapping_stratum",
"get_agso_properties"
]
#++++ configure the geological rocks from AGSO DataBase +++++++++++++++++++++++
EMOD = 'watex.etc' ; buffer_file = 'AGSO.csv'
with resources.path (EMOD, buffer_file) as buff :
props_buf = str(buff)
AGSO_PROPERTIES =dict(
GIT_REPO = 'https://github.com/WEgeophysics/watex',
GIT_ROOT ='https://raw.githubusercontent.com/WEgeophysics/watex/master/',
props_dir = os.path.dirname (props_buf),
props_files = ['AGSO.csv', 'AGSO_STCODES.csv'],
props_codes = ['code', 'label', 'name', 'pattern','size',
'density', 'thickness', 'color']
)
#++++ end configuration +++++++++++++++++++++++++++++++++++++++++++++++++++++++
[docs]
class GeoBase:
"""
Base class of container of geological informations for stratigraphy model
log creation of exploration area.
Each station is condidered as an attribute and framed by two closest
points from the station offsets. The class deals with the true resistivity
values collected on exploration area from drilling or geolgical companies.
Indeed, the input true resistivity values into the occam2d inversion data
could yield an accuracy underground map. The challenge to build a pseudolog
framed between two stations allow to know the layers disposal or supperposition
from to top to the investigation depth. The aim is to emphasize a large
conductive zone usefful in of groundwater exploration.
The class `Geodrill` deals at this time with Occam 2D inversion files or
Bo Yang model (x,y,z) files. We intend to extend later with other external
softwares like the Modular System EM (MODEM) and else.
It's also possible to generate output straighforwardly for others external
softwares like Golder sofwares('surfer')or Oasis Montaj:
- Surfer: :https://www.goldensoftware.com/products/surfer)
- Oasis: http://updates.geosoft.com/downloads/files/how-to-guides/Oasis_montaj_Gridding.pdf
<https://www.seequent.com/products-solutions/geosoft-oasis-montaj/>
Note:
If the user has a golder software installed on its computer, it 's
possible to use the output files generated here to yield a 2D map so
to compare both maps to see the difference between model map (
only inversion files and detail-sequences map after including the
input true resistivity values and layer names)
Futhermore, the "pseudosequences model" could match and describe better
the layers disposal (thcikness and contact) in underground than the
raw model map which seems to be close to the reality when `step descent`
parameter is not too small at all.
"""
def __init__(self, verbose: int =0 , **kwargs):
self._logging = watexlog.get_watex_logger(self.__class__.__name__)
self.verbose= verbose
for key in list(kwargs.keys()):
setattr(self, key, kwargs[key])
[docs]
@staticmethod
def find_properties(
data=None, *,
constraint: bool=...,
keep_acronyms: bool=...,
fill_value: str=None,
kind: str='geology',
attribute :str='code',
property: str='description',
):
""" Find rock/structural properties or constraint the geological info
to fit the rock in AGSO database.
Parameters
-----------
data: Arraylike one-dimensional or pd.Series, optional
Arraylike containing the geological or structural informations.
constraint: bool, default=False
fit the geological or structual info to match the AGSO geology
or structural database infos.
keep_acronym: bool, default=False,
Use the acronym of the structural and geological database
info to replace the full geological/structural name.
fill_value: str, optional
If None, the undetermined structured are not replaced. They are
kept in the data. However, if the fill_value is provided, the
missing structure from data is replaced by the fill_value.
kind: str, default='geology'
Is the type of geo-data to fetch in the database. It can be
['geology'|'samples'|'structural']. Any other values except
the 'geology' will returns the structural samples.
attribute: str, default='code'
The name of attribute to collect as the keys. It can be
- 'code', 'label', 'description', 'pattern' or
- 'pat_size', 'pat_density', 'pat_thickness', 'color'
property: str, default='description'
The name of property to collect as the values. By default is
the geological or structural mames.
Returns
---------
- attributes/property: tuple (str )
A key, value in pair of the attribute and property fetched
in the AGSO database.
- data constrainted. Pd.series or array
Data with items fitted with the property names in the AGSO
database.
Examples
---------
>>> from watex.geology.core import GeoBase
>>> GeoBase.find_properties () [:7]
Out[7]:
(('AGLT', 'argillite'),
('ALUV', 'alluvium'),
('AMP', 'amphibolite'),
('ANS', 'anorthosite'),
('ANT', 'andesite'),
('APL', 'aplite'),
('ARKS', 'arkose'))
>>> # make data
>>> geodata =['gran', 'basalt', 'migmatite', 'sand', 'tuff']
>>> GeoBase.find_properties (geodata, constraint =True )
Out[8]:
array(['granodior', 'basalt', 'migmatite', 'sandstone', 'tuff'],
dtype='<U9')
>>> geodata +=['Unknow structure']
>>> GeoBase.find_properties (geodata, constraint =True,
fill_value='NA' )
Out[9]:
array(['granodiorite', 'basalt', 'migmatite', 'sandstone', 'tuff', 'NA'],
dtype='<U16')
>>> GeoBase.find_properties (geodata, constraint =True,
fill_value='NA' , keep_acronyms=True )
Out[10]: array(['grd', 'blt', 'mig', 'sdst', 'tuf', 'NA'], dtype='<U16')
"""
constraint, keep_acronyms= ellipsis2false(constraint, keep_acronyms)
kind = str(kind).lower().strip()
if 'geology'.find (kind) >=0: kind ='geology'
fname = buffer_file if kind =='geology' else 'AGSO_STCODES.csv'
path_file = os.path.join( AGSO_PROPERTIES.get("props_dir"), fname)
_agso_data=get_agso_properties(path_file)
dp =list ()
for cod in ( attribute, property ):
d = key_search (str( cod),
default_keys= list(_agso_data.keys()),
deep= True,
parse_keys= False,
raise_exception= True
)
dp.append (d[0])
# unpack attribute and properties
attribute , property = dp
attribute = attribute.upper(); property = str(property).upper()
prop_data={key:value for key , value in zip (
_agso_data[attribute], _agso_data[property])}
if not constraint:
return tuple (prop_data.items() )
if data is None:
raise TypeError (
"Data cannot be None when constraint is set to ``True``")
# for consistency
data = np.array (
is_iterable (data , exclude_string= True, transform= True) )
if not _is_arraylike_1d(data ):
raise GeoPropertyError (
"Geology or Geochemistry samples expects"
f" one dimensional array. Got shape ={data.shape}")
found=False # flags if structure is found
for kk, item in enumerate ( data) :
for key, value in prop_data.items():
if str(value).lower().find (str(item).lower() )>=0:
data[kk] = str(key).lower() if keep_acronyms else value
found = True
break
# if item not found then
# property data.
if not found:
if fill_value is not None:
data[kk] = fill_value
found =False
return data
[docs]
@staticmethod
def getProperties(properties =['electrical_props', '__description'],
sproperty ='electrical_props'):
""" Connect database and retrieve the 'Eprops'columns and 'LayerNames'
:param properties: DataBase columns.
:param sproperty : property to sanitize. Mainly used for the properties
in database composed of double parenthesis. Property value
should be removed and converted to tuple of float values.
:returns:
- `_gammaVal`: the `properties` values put on list.
The order of the retrieved values is function of
the `properties` disposal.
"""
#------------------------------------
from .database import GeoDataBase
#-----------------------------------
def _fs (v):
""" Sanitize value and put on list
:param v: value
:Example:
>>> _fs('(416.9, 100000.0)'))
...[416.9, 100000.0]
"""
try :
v = float(v)
except :
v = tuple([float (ss) for ss in
v.replace('(', '').replace(')', '').split(',')])
return v
# connect to geodataBase
try :
_dbObj = GeoDataBase()
except:
_logger.debug('Connection to database failed!')
else:
_gammaVal = _dbObj._retreive_databasecolumns(properties)
if sproperty in properties:
indexEprops = properties.index(sproperty )
try:
_gammaVal [indexEprops] = list(map(lambda x:_fs(x),
_gammaVal[indexEprops]))
except TypeError:
_gammaVal= list(map(lambda x:_fs(x),
_gammaVal))
return _gammaVal
[docs]
@staticmethod
def findGeostructures(
res: float|List[float, ...], /,
db_properties=['electrical_props', '__description']
):
""" Find the layer from database and keep the ceiled value of
`_res` calculated resistivities"""
structures = find_similar_structures(res)
if len(structures) !=0 or structures is not None:
if structures[0].find('/')>=0 :
ln = structures[0].split('/')[0].lower()
else: ln = structures[0].lower()
return ln, res
else:
valEpropsNames = GeoBase.getProperties(db_properties)
indeprops = db_properties.index('electrical_props')
for ii, elecp_value in enumerate(valEpropsNames[indeprops]):
if elecp_value ==0.: continue
elif elecp_value !=0 :
try :
iter(elecp_value)
except : pass
else :
if min(elecp_value)<= res<= max(elecp_value):
ln= valEpropsNames[indeprops][ii]
return ln, res
#---------------------------CORE Utilities ------------------------------------
[docs]
def set_agso_properties (download_files = True ):
""" Set the rocks and their properties from inner files located in
< 'watex/etc/'> folder."""
msg= ''.join([
"Please don't move or delete the properties files located in",
f" <`{AGSO_PROPERTIES['props_dir']}`> directory."])
mf =list()
__agso= [ os.path.join(os.path.realpath(AGSO_PROPERTIES['props_dir']),
f) for f in AGSO_PROPERTIES['props_files']]
for f in __agso:
agso_exists = os.path.isfile(f)
if not agso_exists:
mf.append(f)
continue
if len(mf)==0: download_files=False
if download_files:
for file_r in mf:
success = get_remote_data(props_files = file_r,
savepath = os.path.join(
os.path.realpath('.'),
AGSO_PROPERTIES['props_dir']),
)
if not success:
msg_ = ''.join([ "Unable to retreive the geostructure ",
f"{os.path.basename(file_r)!r} property file from",
f" {AGSO_PROPERTIES['GIT_REPO']!r}."])
warnings.warn(f"Geological structure file {file_r} "
f"is missing. {msg_}")
_logger.warn( msg_)
raise GeoPropertyError(
f"No property file {os.path.basename(file_r)!r}"
f" is found. {msg}.")
for f in __agso:
with open(f,'r' , encoding ='utf8') as fs:
yield([stratum.strip('\n').split(',')
for stratum in fs.readlines()])
[docs]
def mapping_stratum(download_files =True):
""" Map the rocks properties from _geocodes files and fit
each rock to its properties.
:param download_files: bool
Fetching data from repository if the geostrutures files are missing.
:return: Rocks and structures data in two diferent dictionnaries
"""
# get code description _index
ix_= AGSO_PROPERTIES['props_codes'].index('name')
def mfunc_(d):
""" Set individual layer in dict of properties """
_p= {c: k.lower() if c not in ('code', 'label', 'name') else k
for c, k in zip(AGSO_PROPERTIES['props_codes'], d) }
id_= d[ix_].replace('/', '_').replace(
' ', '_').replace('"', '').replace("'", '').lower()
return id_, _p
rock_and_structural_props =list()
for agso_data in tuple(set_agso_properties(download_files)):
# remove the header of the property file
rock_and_structural_props.append(
dict(map( lambda x: mfunc_(x), agso_data[1:])))
return tuple(rock_and_structural_props)
[docs]
def get_agso_properties(config_file =None, orient ='series'):
""" Get the geostructures files from <'watex/etc/'> and
set the properties according to the desire type. When `orient` is
``series`` it will return a dictionnary with key equal to
properties name and values are the properties items.
:param config_file: Path_Like or str
Can be any property file provided that its obey the disposal of
property files found in `AGSO_PROPERTIES`.
:param orient: string value, ('dict', 'list', 'series', 'split',
'records’, ''index') Defines which dtype to convert
Columns(series into).For example, 'list' would return a
dictionary of lists with Key=Column name and Value=List
(Converted series). For furthers details, please refer to
https://www.geeksforgeeks.org/python-pandas-dataframe-to_dict/
:Example:
>>> import watex.geology.core import get_agso_properties
>>> data=get_agso_properties('watex/etc/AGSO_STCODES.csv')
>>> code_descr={key:value for key , value in zip (data["CODE"],
data['__DESCRIPTION'])}
"""
msg= ''.join(["<`{0}`> is the software property file. Please don't move"
" or delete the properties files located in <`{1}`> directory."])
pd_pos_read = Config().parsers
ext='none'
if config_file is None:
config_file = os.path.join(os.path.realpath('.'), os.path.join(
AGSO_PROPERTIES['props_dir'],
AGSO_PROPERTIES ['props_files'][0]))
if config_file is not None:
is_config = os.path.isfile(config_file)
if not is_config :
if os.path.basename(config_file) in AGSO_PROPERTIES['props_files']:
_logger.error(f"Unable to find the geostructure property"
f"{os.path.basename(config_file)!r} file."
)
warnings.warn(msg.format(os.path.basename(config_file) ,
AGSO_PROPERTIES['props_dir']))
raise FileExistsError(f"File `{config_file}`does not exist")
_, ext = os.path.splitext(config_file)
if ext not in pd_pos_read.keys():
_logger.error(f"Unable to read {config_file!r}. Acceptable formats"
f" are {smart_format(list(pd_pos_read.keys()))}.")
raise TypeError(
f"Format {ext!r} cannot be read. Can only read "
f"{smart_format(list(pd_pos_read.keys()))} files."
)
agso_rock_props = pd_pos_read[ext](config_file).to_dict(orient)
if ('name' or 'NAME') in agso_rock_props.keys():
agso_rock_props['__DESCRIPTION'] = agso_rock_props ['name']
del agso_rock_props['name']
return agso_rock_props