Source code for watex.methods.electrical

# -*- coding: utf-8 -*-
#   License: BSD-3-Clause
#   Author: LKouadio <etanoyau@gmail.com>
#   Created date: Thu Apr 14 17:45:55 2022
"""
:mod:`~watex.methods.electrical` computes some DC parameters from ERP and VES. 
From these parameters, it provides intelligent methods to propose the 
right place to locate a drill and predicts the flow rate before any drilling 
operations.
"""
from __future__ import annotations 
import os 
import re 
import copy
import warnings
import numpy as np 
import pandas as pd

from .._docstring import refglossary
from .._typing import  ( 
    List, 
    Optional, 
    NDArray, 
    Series , 
    DataFrame,
    )
from .._watexlog import watexlog 
from ..decorators import refAppender 
from ..utils._dependency import ( 
    import_optional_dependency )
from ..utils.funcutils import (
    repr_callable_obj,
    smart_format,
    smart_strobj_recognition , 
    make_ids, 
    show_stats,
    get_xy_coordinates, 
    )
from ..utils.coreutils import (
    _assert_station_positions,
    defineConductiveZone, 
    fill_coordinates, 
    erpSelector, 
    vesSelector,
    parseDCArgs ,
    plotAnomaly, 
    erpSmartDetector
) 
from ..utils.exmath import (
    shape, 
    type_, 
    power, 
    magnitude, 
    sfi,
    ohmicArea, 
    invertVES,
    plotOhmicArea
    )
from ..utils.validator import ( 
    _is_valid_erp , 
    _is_valid_ves,
    get_estimator_name, 
    )
from ..property import( 
    ElectricalMethods
    ) 
from ..exceptions import (
    NotFittedError, 
    VESError, 
    ERPError,
    StationError, 
    DCError, 
    )

TQDM= False 
try : 
    import tqdm 
except ImportError: 
    warnings.warn("'tqdm' package is missing. Some DC methods expect 'tqdm'"
                  " to be installed. Use pip or conda to install it.")
else: TQDM = True 

__all__=['DCProfiling', 'DCSounding',
         'ResistivityProfiling', 'VerticalSounding'
         ]

[docs] class DCProfiling(ElectricalMethods) : """ A collection of DC-resistivity profiling classes. It reads and compute electrical parameters. Each line compose a specific object and gather all the attributes of :class:`~.ResistivityProfiling` for easy use. For instance, the expeced drilling location point and its resistivity value for two survey lines ( line1 and line2) can be fetched as:: >>> <object>.line1.sves_ ; <object>.line1.sves_resistivity_ >>> <object>.line2.sves_ ; <object>.line2.sves_resistivity_ Parameters ------------ stations: list or str (path-like object ) list of station name where the drilling is expected to be located. It strongly linked to the name of used to specify the center position of each dipole when the survey data is collected. Each survey can have its own way for numbering the positions, howewer if the station is given it should be one ( presumed to be the suitable point for drilling) in the survey lines. Commonly it is called the `sves` which mean at this point, the DC-sounding will be operated. Be sure to provide the correct station to compute the electrical parameters. It is recommed to provide the positioning of the station expected to hold the drillings. However if `stations` is None, the auto-way for computing electrical features should be triggered. User can also provide the list of stations by hand. In that case, each station should numbered from 1 not 0. For instance: * in a survey line of 20 positions. We considered the station 13 as the best point to locate the drilling. Therefore the name of the station should be 'S13'. In other survey line (line2) the second point of my survey is considered the suitable one to locate my drilling. Considering the two survey lines, the list of stations sould be '['S13', 'S2'] * `stations` can also be arrange in a single to be parsed which refer to the string arguments. dipole: float The dipole length used during the exploration area. If `dipole` value is set as keyword argument,i.e. the station name is overwritten and is henceforth named according to the value of the dipole. For instance for `dipole` equals to ``10m``, the first station should be ``S00``, the second ``S10`` , the third ``S20`` and so on. However, it is recommend to name the station using counting numbers rather than using the dipole position. auto: bool Auto dectect the best conductive zone. If ``True``, the station position should be the `station` of the lower resistivity value in |ERP|. keep_params: bool, default=False, If ``True`` , keeps only the predicted parameters in the summary table, otherwise, returns the usefull details of the line like geographical coordinates where the DC predicted parameters are computed. read_sheets: bool, Read the data in sheets. Here its assumes the data of each survey lines are arrange in a single excell worksheets. Note that if `read_sheets` is set to ``True`` and the file is not in excell format, a TypError will raise. force: bool, default=False, By default, :class:`DCProfiling` expects users to provide either DC objects or pandas dataframe. This assumes users have already transformed its data from sheets to data frame. If not the case, setting `force` to ``True`` constrains the algorithm to do the both tasks at once. .. versionadded:: 0.2.0 fit_params: dict Additional |ERP| keywords arguments Examples --------- (1) -> Get DC -resistivity profiling from the individual Resistivity object >>> from watex.methods import ResistivityProfiling >>> from watex.methods import DCProfiling >>> robj1= ResistivityProfiling(auto=True) # auto detection >>> robj1.utm_zone = '50N' >>> robj1.fit('data/erp/testsafedata.xlsx') >>> robj1.sves_ ... 'S036' >>> robj2= ResistivityProfiling(auto=True, utm_zone='40S') >>> robj2.fit('data/erp/l11_gbalo.xlsx') >>> robj2.sves_ ... 'S006' >>> # read the both objects >>> dcobjs = DCProfiling() >>> dcobjs.fit([robj1, robj2]) >>> dcobjs.sves_ ... array(['S036', 'S006'], dtype=object) >>> dcobjs.line1.sves_ # => robj1.sves_ >>> dcobjs.line2.sves_ # => robj2.sves_ (2) -> Read from a collection of excell data >>> datapath = r'data/erp' >>> dcobjs.read_sheets=True >>> dcobjs.fit(datapath) >>> dcobjs.nlines_ # getting the number of survey lines ... 9 >>> dcobjs.sves_ # stations of the best conductive zone ... array(['S017', 'S006', 'S000', 'S036', 'S036', 'S036', 'S036', 'S036', 'S001'], dtype='<U33') >>> dcobjs.sves_resistivities_ # the lower conductive resistivities ... array([ 80, 50, 1101, 500, 500, 500, 500, 500, 93], dtype=int64) >>> dcobjs.powers_ ... array([ 50, 60, 30, 60, 60, 180, 180, 180, 40]) >>> dcobjs.sves_ # stations of the best conductive zone ... array(['S017', 'S006', 'S000', 'S036', 'S036', 'S036', 'S036', 'S036', 'S001'], dtype='<U33') (3) -> Read data and all sheets, assumes all data are arranged in a sheets >>> dcobjs.read_sheets=True >>> dcobjs.fit(datapath) >>> dcobjs.nlines_ # here it assumes all the data are in single worksheets. ... 4 >>> dcobjs.line4.conductive_zone_ # conductive zone of the line 4 ... array([1460, 1450, 950, 500, 1300, 1630, 1400], dtype=int64) >>> dcobjs.sfis_ >>> array([1.05085691, 0.07639077, 0.03592814, 0.07639077, 0.07639077, 0.07639077, 0.07639077, 0.07639077, 1.08655919]) >>> dcobjs.line3.sfi_ # => robj1.sfi_ ... array([0.03592814]) # for line 3 """ def __init__( self, stations: List[str]= None, dipole: float = 10., auto: bool = False, keep_params:bool=False, read_sheets:bool=False, force:bool=False, **kws ): super().__init__(**kws) self._logging=watexlog.get_watex_logger(self.__class__.__name__) self.stations=stations self.dipole=dipole self.auto=auto self.keep_params=keep_params self.read_sheets=read_sheets self.force=force
[docs] def fit(self, *data : List[str] | List [DataFrame], **fit_params)-> "DCProfiling" : """ Read and fit the collections of data Parameters ---------- **data**: List of path-like obj, or :class:`~.ResistivityProfiling` object. Data containing the collection of DC-resistivity values of of multiple survey areas. **fit_params**: str, Additional keyword from :func:watex.utils.coreutils.parseStations`. It refers to the `station_delimiter` parameters. If the attribute :attr:`~.ResistivityProfilings.stations` is given as a path-like object. If the stations are disposed in the same line, it is convenient to provide the delimiter to parse the stations. Returns ------- object instanciated from :class:`~.ResistivityProfiling`. Notes ------ The stations should numbered from 1 not 0 and might fit the number of the survey line. Each survey line expect to hold one positionning drilling. """ self._logging.info (f"{self.__class__.__name__!r} collects the " "resistivity objects ") ex=(f"{self.__class__.__name__!r} expects 'tqdm' to be installed." " Can get 'tqdm' at <https://pypi.org/project/tqdm/>. ") import_optional_dependency ('tqdm', extra= ex ) #-> Initialize collection objects # - collected the unreadable data; readable data self.isnotvalid_= list() ; self.data_= list() # check whether object is readable as ERP objs # -> if not assume a path or file is given _readfromdcObjs (self, *data ) if self.verbose : if len(self.isnotvalid_)!=0: warnings.warn (f"Found {len(self.isnotvalid_)} invalid data.") if len(self.data_) ==0: raise ERPError("None ERP data detected. Please check your data. If" " data is passed as a Path-like object (F|P-types)," " set ``force=True`` to constrain the readable DC-" " format. See documentation for details.") # makeids objects self.ids_ = np.array(make_ids (self.survey_names_,'line',None, True)) # set each line as an object with attributes # can be retrieved like self.line1_.sves_ self.lines_ = np.empty_like (self.ids_, dtype =object ) for kk, (id_ , line) in enumerate (zip( self.ids_, self.data_)) : obj = type (f"{line}", (ElectricalMethods,), line.__dict__ ) self.__setattr__(f"{id_}", obj) self.lines_[kk]= obj # set lines objects # -> lines numbers self.nlines_ = self.lines_.size if self.verbose > 3: print("Each line is an object class inherits from all attributes" " of DC-resistivity profiling object. For instance the" "the right drilling point of the first line can be fetched" " as: <self.line1.sves_> ") # can also retrieve an attributes in other ways # make usefull attributess if self.verbose > 7: print("Populate the other attributes and data can be" " fetched as array of N-number of survey lines. ") # set expected the drilling point positions and resistivity values self.sves_ = _geterpattr ('sves_', self.data_) self.sves_resistivities_ = _geterpattr ( 'sves_resistivity_', self.data_ ).astype(float) # set the expected drilling points coordinates at each line for name in ('lat', 'lon', 'east','north'): setattr (self, f"sves_{name}s_", _geterpattr ( f"sves_{name}_", self.data_).astype(float)) # set the predictor parameter attributes for name in ('power', 'magnitude', 'type','sfi', 'shape'): setattr (self, f"{name}s_", _geterpattr (f"{name}_", self.data_) if name in ('type', 'shape') else _geterpattr ( f"{name}_", self.data_).astype(float) ) return self
[docs] def summary (self, return_table = True): """ Agregate the DC-Profiling parameters to compose a param-table :param return_table: bool, default=True returns table of DC parameters at all sites if ``True`` and 'DCProfiling' instanciated object otherwise. :returns: - table if `return_table` is ``True`` and `DCProfiling` instanciated object otherwise. """ self.inspect return _summary(self, return_table = return_table )
@property def inspect (self): """ Inspect object whether is fitted or not""" msg = ( "{obj.__class__.__name__} instance is not fitted yet." " Call 'fit' with appropriate arguments before using" " this method" ) if not hasattr (self, 'sves_resistivities_'): raise NotFittedError(msg.format( obj=self) ) return 1 def __repr__(self): """ Pretty format for programmer guidance following the API... """ return repr_callable_obj (self, 'line') def __getattr__(self, name): rv = smart_strobj_recognition(name, self.__dict__, deep =True) appender = "" if rv is None else f'. Do you mean {rv!r}' if name =='table_': err_msg =(". Call 'summary' method to fetch attribute 'table_'") else: err_msg = f'{appender}{"" if rv is None else "?"}' raise AttributeError ( f'{self.__class__.__name__!r} object has no attribute {name!r}' f'{err_msg}' )
[docs] @refAppender(refglossary.__doc__) class DCSounding(ElectricalMethods) : """ Direct-Current Electrical Sounding A collection of |VES| class and computed predictors paramaters accordingly. The VES is carried out to speculate about the existence of a fracture zone and the layer thicknesses. Commonly, it comes as supplement methods to |ERP| after selecting the best conductive zone when survey is made on one-dimensional. Data from each DC-sounding site can be retrieved using:: >>> <object>.site<number>.<:attr:`~.VerticalSounding.<attr>_` For instance to fetch the DC-sounding data position and the resistivity in depth of the fractured zone for the first site, we use:: >>> <object>.site1.fractured_zone_ >>> <object>.site1.fractured_zone_resistivity_ Parameters ----------- search: float , list of float The collection of the depth in meters from which one expects to find a fracture zone outside of pollutions. Indeed, the `search` parameter is used to speculate about the expected groundwater in the fractured rocks under the average level of water inrush in a specific area. For instance in `Bagoue region`_ , the average depth of water inrush is around ``45m``.So the `search` can be specified via the water inrush average value. rho0: float Value of the starting resistivity model. If ``None``, `rho0` should be the half minumm value of the apparent resistivity collected. Units is in Ω.m not log10(Ω.m) h0: float Thickness in meter of the first layers in meters.If ``None``, it should be the minimum thickess as possible ``1.m`` . strategy: str Type of inversion scheme. The defaut is Hybrid Monte Carlo (HMC) known as ``HMCMC``. Another scheme is Bayesian neural network approach (``BNN``). vesorder: int The index to retrieve the resistivity data of a specific sounding point. Sometimes the sounding data are composed of the different sounding values collected in the same survey area into different |ERP| line. For instance: +------+------+----+----+----+----+----+ | AB/2 | MN/2 |SE1 | SE2| SE3| ...|SEn | +------+------+----+----+----+----+----+ Where `SE` are the electrical sounding data values and `n` is the number of the sounding points selected. `SE1`, `SE2` and `SE3` are three points selected for |VES| i.e. 3 sounding points carried out either in the same |ERP| or somewhere else. These sounding data are the resistivity data with a specific numbers. Commonly the number are randomly chosen. It does not refer to the expected best fracture zone selected after the prior-interpretation. After transformation via the function :func:`~watex.utils.coreutils.vesSelector`, the header of the data should hold the `resistivity`. For instance, refering to the table above, the data should be: +----+----+-------------+-------------+-------------+-----+ | AB | MN |resistivity | resistivity | resistivity | ... | +----+----+-------------+-------------+-------------+-----+ Therefore, the `vesorder` is used to select the specific resistivity values i.e. select the corresponding sounding number of the |VES| expecting to locate the drilling operations or for computation. For esample, `vesorder`=1 should figure out: +------+------+----+--------+----+----+------------+ | AB/2 | MN/2 |SE2 | --> | AB | MN |resistivity | +------+------+----+--------+----+----+------------+ If `vesorder` is ``None`` and the number of sounding curves are more than one, by default the first sounding curve is selected ie `rhoaIndex` equals to ``0`` typeofop: str Type of operation to apply to the resistivity values `rhoa` of the duplicated spacing points `AB`. The *default* operation is ``mean``. Sometimes at the potential electrodes ( `MN` ),the measurement of `AB` are collected twice after modifying the distance of `MN` a bit. At this point, two or many resistivity values are targetted to the same distance `AB` (`AB` still remains unchangeable while while `MN` is changed). So the operation consists whether to the average ( ``mean`` ) resistiviy values or to take the ``median`` values or to ``leaveOneOut`` (i.e. keep one value of resistivity among the different values collected at the same point `AB` ) at the same spacing `AB`. Note that for the ``LeaveOneOut``, the selected resistivity value is randomly chosen. objective: str Type operation to output. By default, the function outputs the value of pseudo-area in :math:`$ohm.m^2$`. However, for plotting purpose by setting the argument to ``view``, its gives an alternatively outputs of X and Y, recomputed and projected as weel as the X and Y values of the expected fractured zone. Where X is the AB dipole spacing when imaging to the depth and Y is the apparent resistivity computed. keep_params: bool, default=False, If ``True`` , keeps only the predicted parameters in the summary table, otherwise, returns the usefull details of the site like the depth AB/2 where the DC predicted area parameter is computed. kws: dict Additionnal keywords arguments from |VES| data operations. See :func:`watex.utils.exmath.vesDataOperator` for futher details. Examples -------- (1) -> read a single DC Electrical Sounding file >>> from watex.methods.electrical import DCSounding >>> dsobj = DCSounding () >>> dsobj.search = 30. # start detecting the fracture zone from 30m depth. >>> dsobj.fit('data/ves/ves_gbalo.xlsx') >>> dsobj.ohmic_areas_ ... array([523.25458506]) >>> dsobj.site1.fractured_zone_ # show the positions of the fracture zone ... array([ 28., 32., 36., 40., 45., 50., 55., 60., 70., 80., 90., 100.]) >>> dsobj.line1.fractured_zone_resistivity_ ... array([ 68.74273843, 71.57116555, 74.39959268, 77.2280198 , 80.76355371, 84.29908761, 87.83462152, 91.37015543, 98.44122324, 105.51229105, 112.58335886, 119.65442667]) (2) -> read multiple sounding files >>> dsobj.fit('data/ves') >>> dsobj.ohmic_areas_ ... array([ 523.25458506, 523.25458506, 1207.41759558]) >>> dsobj.nareas_ ... array([2., 2., 3.]) >>> dsobj.survey_names_ ... ['ves_gbalo', 'ves_gbalo', 'ves_gbalo_unique'] >>> dsobj.nsites_ ... 3 >>> dsobj.site1.ohmic_area_ ... 523.2545850558677 # => dsobj.ohmic_areas_ -> line 1:'ves_gbalo' """ def __init__( self, search:float=45., rho0:float=None, h0 :float=1., read_sheets:bool=False, strategy:str='HMCMC', vesorder:int=None, typeofop:str='mean', objective: Optional[str] = 'coverall', keep_params:bool=False, **kws ): super().__init__(**kws) self._logging = watexlog.get_watex_logger(self.__class__.__name__) self.search=search self.vesorder=vesorder self.typeofop=typeofop self.objective=objective self.rho0=rho0 self.h0=h0 self.strategy=strategy self.keep_params=keep_params self.read_sheets= read_sheets for key in list( kws.keys()): setattr(self, key, kws[key])
[docs] def fit(self, *data : List[str] | List [DataFrame], **fit_params)->'DCSounding': """ Fit the DC- electrical sounding Fit the sounding |VES| curves and computed the ohmic-area and set all the features for demarcating fractured zone from the selected anomaly. Parameters ----------- data: list of path-like object, or DataFrames The string argument is a path-like object. It must be a valid file wich encompasses the collected data on the field. It shoud be composed of spacing values `AB` and the apparent resistivity values `rhoa`. By convention `AB` is half-space data i.e `AB/2`. So, if `data` is given, params `AB` and `rhoa` should be kept to ``None``. If `AB` and `rhoa` is expected to be inputted, user must set the `data` to ``None`` values for API purpose. If not an error will raise. Or the recommended way is to use the `vesSelector` tool in :func:`watex.utils.vesSelector` to buid the |VES| data before feeding it to the algorithm. See the example below. fit_params: dict additional keywords arguments, specific to the readable files. Refer to :method:`watex.property.Config.parsers` . Use the key() to get all the readables format. Returns ------- object: A collection of |VES| objects """ self._logging.info (f"{self.__class__.__name__!r} collects the " "resistivity objects ") ex=(f"{self.__class__.__name__!r} expects 'tqdm' to be installed." " Can get 'tqdm' at <https://pypi.org/project/tqdm/>. ") import_optional_dependency ('tqdm', extra= ex ) #-> Initialize collection objects # - collected the unreadable data ; readable data self.isnotvalid_= list() ; self.data_= list() # check whether object is readable as ERP objs # -> if not assume a path or file is given _readfromdcObjs (self, *data, dcmethod ="VerticalSounding" ) if self.verbose : if len(self.isnotvalid_)!=0: warnings.warn (f"Found {len(self.isnotvalid_)} invalid data.") if len(self.data_) ==0: raise VESError("Unknown VES data. It seems not match to VES." " Please check your data." ) # valid data, then cast it to keep only the valid data self.ids_ = np.array(make_ids (self.survey_names_, 'site', None, True)) # set each line as an object with attributes # can be retrieved like self.site1_.fractured_zone_ self.sites_ = np.empty_like (self.ids_, dtype =object ) for kk, (id_ , site) in enumerate (zip( self.ids_, self.data_)) : obj = type (f"{site}", (ElectricalMethods,), site.__dict__ ) self.__setattr__(f"{id_}", obj) self.sites_[kk]= obj # set site objects # -> lines numbers # rather using sites_ self.nsites_ = self.sites_.size if self.verbose > 3: print("Each line is an object class inherits from all attributes" " of DC-electrical sounding object. For instance the number" " of ohmic areas computed of the first line can be fetched" " as: <self.site1.ohmic_area_> ") # can also retrieve an attributes in other ways # make usefull attributess if self.verbose > 7: print("Populate the other attributes and data can be" " fetched as array of N-number of survey lines. ") # set expected the drilling point positions and resistivity values self.ohmic_areas_ = _geterpattr ('ohmic_area_', self.data_).astype(float) self.nareas_ = _geterpattr ( 'nareas_', self.data_ ).astype(float) # All other attributes can be retrieved. For instance line1 # self.site1.XY_, self.site1.XYarea_ or # self.site1.AB_ , self.site.XY_ if self.verbose > 7: print("Parameters numbers are well computed ") return self
[docs] def summary (self, return_table = True): """ Agregate the DC-Sounding parameters to compose a param-table :param return_table: bool, default=True returns table of DC parameters at all sites if ``True`` and 'DCSounding' instanciated object otherwise. :returns: - table if `return_table` is ``True`` and `DCSounding` instanciated object otherwise. """ self.inspect return _summary(self, return_table = return_table )
@property def inspect (self): """ Inspect object whether is fitted or not""" msg = ( "{obj.__class__.__name__} instance is not fitted yet." " Call 'fit' with appropriate arguments before using" " this method" ) if not hasattr (self, 'nareas_'): raise NotFittedError(msg.format( obj=self) ) return 1 def __repr__(self): """ Pretty format for programmer guidance following the API... """ return repr_callable_obj (self, 'line') def __getattr__(self, name): rv = smart_strobj_recognition(name, self.__dict__, deep =True) appender = "" if rv is None else f'. Do you mean {rv!r}' if name =='table_': err_msg =(". Call 'summary' method to fetch attribute 'table_'") else: err_msg = f'{appender}{"" if rv is None else "?"}' raise AttributeError ( f'{self.__class__.__name__!r} object has no attribute {name!r}' f'{err_msg}' )
[docs] @refAppender(refglossary.__doc__) class ResistivityProfiling(ElectricalMethods): """ Class deals with the Electrical Resistivity Profiling (ERP). The electrical resistivity profiling is one of the cheap geophysical subsurface imaging method. It is most preferred to find groundwater during the campaigns of drinking water supply, especially in developing countries. Commonly, it is used in combinaision with the the vertical electrical sounding |VES| to speculated about the layer thickesses and the existence of the fracture zone. Parameters ---------- station: str Station name where the drilling is expected to be located. The station should numbered from 1 not 0. So if ``S00` is given, the station name should be set to ``S01``. Moreover, if `dipole` value is set as keyword argument,i.e. the station is named according to the value of the dipole. For instance for `dipole` equals to ``10m``, the first station should be ``S00``, the second ``S10`` , the third ``S20`` and so on. However, it is recommend to name the station using counting numbers rather than using the dipole position. dipole: float The dipole length used during the exploration area. auto: bool Auto dectect the best conductive zone. If ``True``, the station position should be the `station` of the lower resistivity value in |ERP|. constraints: list or dict, It determines the restriction observed in the site during the survey area. Any station close to a restriction area must be listed and should be ignored when the best location for drilling operations is automatically detected. A restricted stations can be enumerated as a dictionnary of ``key='restricted station'`` and ``value='reason`` why the station must be ignored. For instance:: constraints ={'S10': 'Heritage site, no authorization for drilling' 'S25': 'Close to the household waste' "S45": 'Station close to a municipality domain' 'S50': 'Marsh area' ... } Note that, commonly ``constraints`` is mostly needed when the automatic detection is triggered. However, it can be `coerce` with the explicit defined `station`. force: bool, default=False, By default, :class:`ResistivityProfiling` expects users to provide either DC objects or pandas dataframe. This supposes users have already transformed its data from sheets to data frame. If not the case, setting `force` to ``True`` constrains the algorithm to do the both tasks at once. .. versionadded:: 0.2.0 kws: dict Additional |ERP| keywords arguments Examples -------- >>> from watex.methods.electrical import ResistivityProfiling >>> rObj = ResistivityProfiling(AB= 200, MN= 20,station ='S7') >>> rObj.fit('data/erp/testunsafedata.csv') >>> rObj.sfi_ ... array([0.03592814]) >>> rObj.power_, robj.position_zone_ ... 90, array([ 0, 30, 60, 90]) >>> rObj.magnitude_, robj >>> rObj.magnitude_, rObj.conductive_zone_ ... 268, array([1101, 1147, 1345, 1369], dtype=int64) >>> robj.dipole ... 30 """ def __init__ ( self, station: str = None, dipole: float = 10., auto: bool = False, constraints:list|dict=None, coerce: bool=False, force: bool=False, **kws): super().__init__(**kws) self._logging = watexlog.get_watex_logger(self.__class__.__name__) self.dipole=dipole self.station=station self.auto=auto self.constraints=constraints self.coerce=coerce self.force=force for key in list( kws.keys()): setattr(self, key, kws[key])
[docs] def fit(self, data : str | NDArray | Series | DataFrame , **fit_params ) -> 'ResistivityProfiling': """ Fitting the :class:`~.ResistivityProfiling` and populate the class attributes. Parameters ---------- **data**: Path-like obj, Array, Series, Dataframe. Data containing the the collected resistivity values in survey area. **columns**: list, Only necessary if the `data` is given as an array. No need to to explicitly define it when `data` is a dataframe or a Pathlike object. **fit_params**: dict, Additional keyword arguments; e.g. to force the station to match at least the best minimal resistivity value in the whole data collected in the survey area. Returns ------- self: object instanciated for chaining methods. Notes ------ The station should numbered from 1 not 0. So if ``S00` is given, the station name should be set to ``S01``. Moreover, if `dipole` value is set as keyword argument, i.e. the station is named according to the value of the dipole. For instance for `dipole` equals to ``10m``, the first station should be ``S00``, the second ``S10``, the third ``S20`` and so on. However, it is recommend to name the station using counting numbers rather than using the dipole position. """ columns = fit_params.pop('columns', None) # force =fit_params.pop("force", False) self._logging.info(f'`Fit` method from {self.__class__.__name__!r}' ' is triggered ') if isinstance(data, str): if not os.path.isfile (data): raise TypeError ( f'{data!r} object should be a file,' f' got {type(data).__name__!r}' ) data = erpSelector(data, columns, force= self.force , utm_zone= self.utm_zone, epsg = self.epsg ) if not _is_valid_erp(data): raise ERPError("Invalid ERP data. Data must contain at least" " 'resistivity' and 'station' position." ) self.data_ = copy.deepcopy(data) # for consistency compute easting/northing if # are missing. self.data_, self.utm_zone = fill_coordinates( self.data_, utm_zone= self.utm_zone, datum = self.datum , epsg= self.epsg , verbose = self.verbose ) self.resistivity_ = self.data_.resistivity # convert app.rho to the concrete value # if log10 rho are provided. if self.fromlog10: self.resistivity_ = np.power( 10, self.resistivity_) if self.verbose > 7 : print("Resistivity profiling data should be overwritten to " " take the concrete values rather than log10(ohm.meters)" ) self.data_['resistivity'] = self.resistivity_ self._logging.info(f'Retrieving the {self.__class__.__name__!r} ' ' components and recompute the coordinate values...') self.position_ = self.data_.station self.lat_ = self.data_.latitude self.lon_= self.data_.longitude self.east_ = self.data_.easting self.north_ = self.data_.northing if self.verbose > 7: print(f'Compute {self.__class__.__name__!r} parameter numbers.' ) self._logging.info(f'Assert the station {self.station!r} if given' 'or auto-detected otherwise.') # assert station and use the automatic station renaming if self.auto and self.station is not None: warnings.warn ( f"Station {self.station!r} is given while 'auto' is 'True'." " The auto-detection takes the priority...", UserWarning) self.station = None if self.station is None: cmsg = " and constraints" if self.constraints is None else '' if not self.auto: warnings.warn(f"Missing Station number{cmsg}. The naive" " auto-detection is triggered instead.") self.auto = True if self.station is not None: if self.verbose > 7 : print("Assert the given station and recomputed the array" " position.") self._logging.warn( f'Station value {self.station!r} in the given data ' 'should be overwritten...') # recompute the position and dipolelength self.position_, self.dipole = _assert_station_positions( df = self.data_, **fit_params) self.data_['station'] = self.position_ ############################################################ # check whether constraints are given and find the suitable # position if self.constraints: constr_r = erpSmartDetector( self.constraints, erp= self.resistivity_, station = self.station, coerce= self.coerce, # turn off to reformulate the message raise_warn=False, return_cz=True, ) # if none suitable location is found if constr_r is None: warnings.warn( "No suitable location for drilling operations is detected" " after applying the constraints. Procedure to compute the" " DC-profiling parameters is aborted. To force proposing" " an alternative location using the naive auto-detection" " set ``constraints=None`` and re-fit the" f" {self.__class__.__name__!r} object. Use it at your" " own risk.") return self self.station ,_, self.constr_ix_ = constr_r # turn off auto detection since the best station is found. self.auto=False # Define the selected anomaly (conductive_zone ) # ix: s the index of drilling point in the selected # conductive zone while # pos: is the index of drilling point in the whole # survey position self.conductive_zone_, self.position_zone_, ix, pos, =\ defineConductiveZone( self.resistivity_, station= self.station, auto = self.auto, #keep Python numbering index (from 0 ->...), #index = 'py', # No need to implement the dipole computation # for detecting the station position if the # station is given # dipole =self.dipole if self.station is None else None, position = self.position_ ) ############################################################ if self.verbose >7 : print('Compute the property values at the station location ' ' expecting for drilling location <`sves`> at' f' position {str(pos+1)!r}') # Note that `sves` is the station location expecting to # hold the drilling operations at this point. It is considered # as the select point of the conductive zone. self.sves_ = f'S{pos:03}' self._logging.info ('Loading main params value from the expecting' f' drilling location: {self.sves_!r}') self.sves_lat_ = self.lat_[pos] self.sves_lon_= self.lon_[pos] self.sves_east_ = self.east_[pos] self.sves_north_= self.north_[pos] self.sves_resistivity_= self.resistivity_[pos] # Compute the predictor parameters self.power_ = power(self.position_zone_) self.shape_ = shape(self.conductive_zone_ , s= ix , p= self.position_) self.magnitude_ = magnitude(self.conductive_zone_) self.type_ = type_ (self.resistivity_) self.sfi_ = sfi(cz = self.conductive_zone_, p = self.position_zone_, s = ix, dipolelength= self.dipole ) if self.verbose > 7 : pn = ('type', 'shape', 'magnitude', 'power' , 'sfi') print(f"Parameter numbers {smart_format(pn)}" " were successfully computed.") return self
[docs] def summary(self, keep_params: bool = False, return_table: bool =False, ) -> object | DataFrame : """ Summarize the most import parameters for prediction purpose. Parameters ------------- keep_params: bool, default=False, If `keep_params` is set to ``True``. Method should output only the main important params for prediction purpose. Otherwise, returns all main DC-resistivity attributes return_tables: bool, default=False, Returns attributes of parameters in a pandas dataframe. Returns -------- self or table_: :class:`~.ResistivityProfiling` or class:`pd.DataFrame` Returns DC- profiling object or dataframe. """ self.inspect if self.constraints and not hasattr(self, 'constr_ix_'): raise ERPError(" Can't summary the DC-parameters. NO suitable " " location was found for drilling operations" " when constraints are applied.") usefulparams = ( 'station','dipole', 'sves_lon_', 'sves_lat_','sves_east_', 'sves_north_', 'sves_resistivity_', 'power_', 'magnitude_', 'shape_','type_','sfi_') table_= pd.DataFrame ( dict( _make_param_table(self, usefulparams)), index=range(1) ) table_.station = self.sves_ table_.set_index ('station', inplace =True) table_.rename (columns= {'sves_lat':'latitude', 'sves_lon':'longitude', 'sves_east':'easting', 'sves_north':'northing'}, inplace =True) if keep_params: table_.reset_index(inplace =True ) table_.drop( ['station', 'dipole', 'sves_resistivity', 'latitude', 'longitude', 'easting', 'northing'], axis='columns', inplace =True ) self.table_ = table_ return self.table_ if return_table else self
[docs] def plotAnomaly (self, **plot_kws): """ Plot the best conductive zone found in the |ERP| :param plot_kws: dict, additional keyword arguments passed to :func:`~watex.utils.coreutils.plotAnomaly`. """ self.inspect ax= plotAnomaly(self.resistivity_, cz = self.conductive_zone_, station= self.sves_, **plot_kws) if ( self.constraints is not None and getattr( self, 'constr_ix_') is not None ): lab=f"Restricted station{'s' if len(self.constr_ix_)>1 else ''}" ax.scatter (self.constr_ix_, self.resistivity_ [self.constr_ix_ ], marker="s", s=77, color='r', alpha = .8, label=lab) ax.legend () return ax
def __repr__(self): """ Pretty format for programmer guidance following the API... """ return repr_callable_obj (self) def __getattr__(self, name): rv = smart_strobj_recognition(name, self.__dict__, deep =True) appender = "" if rv is None else f'. Do you mean {rv!r}' if name =='table_': err_msg =(". Call 'summary' method to fetch attribute 'table_'") else: err_msg = f'{appender}{"" if rv is None else "?"}' raise AttributeError ( f'{self.__class__.__name__!r} object has no attribute {name!r}' f'{err_msg}' ) @property def inspect (self): """ Inspect object whether is fitted or not""" msg = ( "{obj.__class__.__name__} instance is not fitted yet." " Call 'fit' with appropriate arguments before using" " this method" ) if not hasattr (self, 'sfi_'): raise NotFittedError(msg.format( obj=self) ) return 1
[docs] @refAppender(refglossary.__doc__) class VerticalSounding (ElectricalMethods): """ Vertical Electrical Sounding (VES) class; inherits of ElectricalMethods base class. The VES is carried out to speculate about the existence of a fracture zone and the layer thicknesses. Commonly, it comes as supplement methods to |ERP| after selecting the best conductive zone when survey is made on one-dimensional. Parameters ----------- **search**: float The depth in meters from which one expects to find a fracture zone outside of pollutions. Indeed, the `search` parameter is used to speculate about the expected groundwater in the fractured rocks under the average level of water inrush in a specific area. For instance in `Bagoue region`_ , the average depth of water inrush is around ``45m``.So the `search` can be specified via the water inrush average value. **rho0**: float Value of the starting resistivity model. If ``None``, `rho0` should be the half minumm value of the apparent resistivity collected. Units is in Ω.m not log10(Ω.m) **h0**: float Thickness in meter of the first layers in meters.If ``None``, it should be the minimum thickess as possible ``1.m`` . **strategy**: str Type of inversion scheme. The defaut is Hybrid Monte Carlo (HMC) known as ``HMCMC``. Another scheme is Bayesian neural network approach (``BNN``). **vesorder**: int The index to retrieve the resistivity data of a specific sounding point. Sometimes the sounding data are composed of the different sounding values collected in the same survey area into different |ERP| line. For instance: +------+------+----+----+----+----+----+ | AB/2 | MN/2 |SE1 | SE2| SE3| ...|SEn | +------+------+----+----+----+----+----+ Where `SE` are the electrical sounding data values and `n` is the number of the sounding points selected. `SE1`, `SE2` and `SE3` are three points selected for |VES| i.e. 3 sounding points carried out either in the same |ERP| or somewhere else. These sounding data are the resistivity data with a specific numbers. Commonly the number are randomly chosen. It does not refer to the expected best fracture zone selected after the prior-interpretation. After transformation via the function :func:`~watex.utils.coreutils.vesSelector`, the header of the data should hold the `resistivity`. For instance, refering to the table above, the data should be: +----+----+-------------+-------------+-------------+-----+ | AB | MN |resistivity | resistivity | resistivity | ... | +----+----+-------------+-------------+-------------+-----+ Therefore, the `vesorder` is used to select the specific resistivity values i.e. select the corresponding sounding number of the |VES| expecting to locate the drilling operations or for computation. For esample, `vesorder`=1 should figure out: +------+------+----+--------+----+----+------------+ | AB/2 | MN/2 |SE2 | --> | AB | MN |resistivity | +------+------+----+--------+----+----+------------+ If `vesorder` is ``None`` and the number of sounding curves are more than one, by default the first sounding curve is selected ie `rhoaIndex` equals to ``0`` **typeofop**: str Type of operation to apply to the resistivity values `rhoa` of the duplicated spacing points `AB`. The *default* operation is ``mean``. Sometimes at the potential electrodes ( `MN` ),the measurement of `AB` are collected twice after modifying the distance of `MN` a bit. At this point, two or many resistivity values are targetted to the same distance `AB` (`AB` still remains unchangeable while while `MN` is changed). So the operation consists whether to the average ( ``mean`` ) resistiviy values or to take the ``median`` values or to ``leaveOneOut`` (i.e. keep one value of resistivity among the different values collected at the same point `AB` ) at the same spacing `AB`. Note that for the ``LeaveOneOut``, the selected resistivity value is randomly chosen. **objective**: str Type operation to output. By default, the function outputs the value of pseudo-area in :math:`$ohm.m^2$`. However, for plotting purpose by setting the argument to ``view``, its gives an alternatively outputs of X and Y, recomputed and projected as weel as the X and Y values of the expected fractured zone. Where X is the AB dipole spacing when imaging to the depth and Y is the apparent resistivity computed. **kws**: dict Additionnal keywords arguments from |VES| data operations. See :func:`watex.utils.exmath.vesDataOperator` for futher details. References ---------- *Koefoed, O. (1970)*. A fast method for determining the layer distribution from the raised kernel function in geoelectrical sounding. Geophysical Prospecting, 18(4), 564–570. https://doi.org/10.1111/j.1365-2478.1970.tb02129.x . *Koefoed, O. (1976)*. Progress in the Direct Interpretation of Resistivity Soundings: an Algorithm. Geophysical Prospecting, 24(2), 233–240. https://doi.org/10.1111/j.1365-2478.1976.tb00921.x . Examples -------- >>> from watex.methods import VerticalSounding >>> from watex.tools import vesSelector >>> vobj = VerticalSounding(search= 45, vesorder= 3) >>> vobj.fit('data/ves/ves_gbalo.xlsx') >>> vobj.ohmic_area_ # in ohm.m^2 ... 349.6432550517697 >>> vobj.nareas_ # number of areas computed ... 2 >>> vobj.area1_, vobj.area2_ # value of each area in ohm.m^2 ... (254.28891096053943, 95.35434409123027) >>> vobj.roots_ # different boundaries in pairs ... [array([45. , 57.55255255]), array([ 96.91691692, 100. ])] >>> data = vesSelector ('data/ves/ves_gbalo.csv', index_rhoa=3) >>> vObj = VerticalSounding().fit(data) >>> vObj.fractured_zone_ # AB/2 position from 45 to 100 m depth. ... array([ 45., 50., 55., 60., 70., 80., 90., 100.]) >>> vObj.fractured_zone_resistivity_ ...array([57.67588974, 61.21142365, 64.74695755, 68.28249146, 75.35355927, 82.42462708, 89.4956949 , 96.56676271]) >>> vObj.nareas_ ... 2 >>> vObj.ohmic_area_ ... 349.6432550517697 """ def __init__( self, search: float = 45., rho0: float = None, h0 : float = 1., strategy: str = 'HMCMC', vesorder: int = None, typeofop: str = 'mean', objective: Optional[str] = 'coverall', xycoords: tuple=None, **kws ): super().__init__(**kws) self._logging = watexlog.get_watex_logger(self.__class__.__name__) self.search=search self.vesorder=vesorder self.typeofop=typeofop self.objective=objective self.rho0=rho0 self.h0=h0 self.strategy=strategy self.xycoords=xycoords for key in list( kws.keys()): setattr(self, key, kws[key])
[docs] def fit(self, data: str | DataFrame, **fit_params )-> "VerticalSounding": """ Fit the sounding |VES| curves and computed the ohmic-area and set all the features for demarcating fractured zone from the selected anomaly. Parameters ----------- data: Path-like object, DataFrame The string argument is a path-like object. It must be a valid file wich encompasses the collected data on the field. It shoud be composed of spacing values `AB` and the apparent resistivity values `rhoa`. By convention `AB` is half-space data i.e `AB/2`. So, if `data` is given, params `AB` and `rhoa` should be kept to ``None``. If `AB` and `rhoa` is expected to be inputted, user must set the `data` to ``None`` values for API purpose. If not an error will raise. Or the recommended way is to use the `vesSelector` tool in :func:`watex.utils.vesSelector` to buid the |VES| data before feeding it to the algorithm. See the example below. AB: array-like The spacing of the current electrodes when exploring in deeper. Units are in meters. Note that the `AB` is by convention equals to `AB/2`. It's taken as half-space of the investigation depth. MN: array-like Potential electrodes distances at each investigation depth. Note by convention the values are half-space and equals to `MN/2`. rhoa: array-like Apparent resistivity values collected in imaging in depth. Units are in Ω.m not log10(Ω.m) fit_params: dict additional keywords arguments, specific to the readable files. Refer to :method:`watex.property.Config.parsers` . Use the key() to get all the readables format. Returns ------- object: a DC -resistivity |VES| object. """ def prettyprinter (n, r,v): """ Display some details when verbose is higher... :param n: int : number of areas :param r: array-like. Pair values of integral bounds (-inf, +inf) :param v: array-float - values of pseudo-areas computed. """ print('=' * 73 ) print('| {0:^15} | {1:>15} | {2:>15} | {3:>15} |'.format( 'N-area', 'lb:-AB/2 (m)','ub:-AB/2(m)', 'ohmS (Ω.m^2)' )) print('=' * 73 ) for ii in range (n): print('| {0:^15} | {1:>15} | {2:>15} | {3:>15} |'.format( ii+1, round(r[ii][0]), round(r[ii][1]), round(v[ii], 3))) print('-'*73) self._logging.info (f'`Fit` method from {self.__class__.__name__!r}' ' is triggered') if self.verbose >= 7 : print(f'Range {str(self.vesorder)!r} of resistivity data ' 'should be selected as the main sounding data. ') self.data_ = vesSelector( data = data, index_rhoa= self.vesorder, is_utm= True if str(self.projection).lower() =='utm' else False, epsg = self.epsg, utm_zone= self.utm_zone or '49R', xy_coords = self.xycoords, **fit_params ) #xxxx add sves_coordinates xxxxxxxx # since xycoords value should be controlled in vesSelector. # if they are valid then data will hold longitude # and latitude. Now ascertain again if # easting and northing are in the data using # # if xy_sves coordinates is not supplied # check whether the coordinates can be found # in the dataframe. if dataframe and coordinates # exist then get the coordinates and remove them from data self.xycoords, _, xynames = get_xy_coordinates ( self.data_, drop_xy = True , as_frame =False, raise_exception='mute') if self.xycoords is None: self.xycoords = ( np.nan, np.nan ) xynames = ('longitude', 'latitude') for name, value in zip ( xynames, self.xycoords ) : setattr ( self, name + '_', value ) #xxxx end add sves_coordinates xxxxxxxx if not _is_valid_ves( self.data_): raise VESError("Invalid VES data. Data must contain at least" " 'resistivity' and 'AB/2' position." ) self.max_depth_ = self.data_.AB.max() if self.fromlog10: self.resistivity_ = np.power( 10, self.resistivity_) if self.verbose > 7 : print("Sounding resistivity data should be converted to " "the concrete resistivity values (ohm.meters)" ) self.data_['resistivity'] = self.resistivity_ if self.search >= self.max_depth_ : raise VESError( " Process of the depth monitoring is aborted! The searching" f" point of param 'search'<{self.search}m> ' is expected to " f" be less than the maximum depth <{self.max_depth_}m>.") if self.verbose >= 3 : print("Pseudo-area should be computed from AB/2 ={str(self.search)}" f" to {self.max_depth_} meters. " ) r = ohmicArea( data = self.data_ , sum = False, search = self.search, objective = self.objective , typeofop = self.typeofop, ) self._logging.info(f'Populating {self.__class__.__name__!r} property' ' attributes.') oc, gc = r ohmS, self.err_, self.roots_ = list(oc) self.nareas_ = len(ohmS) self._logging.info(f'Setting the {self.nareas_} pseudo-areas calculated.') for ii in range(self.nareas_): self.__setattr__(f"area{ii+1}_", ohmS[ii]) self.roots_ = np.split(self.roots_, len(self.roots_)//2 ) if len( self.roots_) > 2 else [np.array(self.roots_)] if self.verbose >= 7 : prettyprinter(n= self.nareas_, r= self.roots_, v= ohmS) self.ohmic_area_= sum(ohmS) # sum the different spaces self.XY_ , self.XYfit_, self.XYarea_ = list(gc) self.AB_ = self.XY_[:, 0] self.resistivity_ = self.XY_[:, 1] self.fractured_zone_= self.XYarea_[:, 0] self.fractured_zone_resistivity_ = self.XYarea_[:, 1] if self.verbose > 7 : print("The Parameter numbers were successfully computed.") return self
[docs] def summary(self, keep_params: bool = False, return_table: bool =False, ) -> DataFrame | object : """ Summarize the most import features for prediction purpose. Parameters ------------- keep_params: bool, default=False, If `keep_params` is set to ``True``. Method should output only the main important params for prediction purpose. Otherwise, returns all main DC-resistivity attributes return_tables: bool, default=False, if ``True``, returns only the summarized table Returns -------- self or table_: :class:`~.VerticalSounding` or class:`pd.DataFrame` Returns DC- Sounding object or dataframe. """ self.inspect coords_xy = ('easting_', 'northing_') if hasattr ( self, 'easting_') else ('longitude_', 'latitude_') usefulparams = ( 'area', 'AB','MN', 'arrangememt','utm_zone', 'objective', 'rho0', 'h0', 'search', 'max_depth_', 'ohmic_area_', 'nareas_', * coords_xy) table_= pd.DataFrame ( dict( _make_param_table(self, usefulparams)), index=range(1) ) table_.area = self.area table_.set_index ('area', inplace =True) # rcols ={k: k[:-1] for k in usefulparams if k.endswith ('_')} table_.rename ( columns={k: k[:-1] for k in usefulparams if k.endswith ('_')}, inplace =True ) # table_.rename (columns= { # 'max_depth_':'max_depth', # 'ohmic_area_':'ohmic_area', # 'nareas_':'nareas' # }, # inplace =True) if keep_params: table_.reset_index(inplace =True ) table_.drop( [ el for el in list(table_.columns) if el !='ohmic_area'], axis='columns', inplace =True ) self.table_ = table_ return table_ if return_table else self
[docs] def plotOhmicArea (self, fbtw= False, **plot_kws ) : """ Plot the ohmic-area from selected fractured zone. :param fbtw: bool, default=False, If ``True``, filled the computed fractured zone. :param plot_kws: dict, Additional keywords arguments passed to :func:`~watex.utils.exmath.plotOhmicArea`. """ self.inspect return plotOhmicArea( xy= self.XY_, xyf = self.XYfit_ , xyarea= self.XYarea_, data = None , pre_computed = True, fbtw = fbtw , **plot_kws )
[docs] def invert( self, data: str | DataFrame , strategy=None, **kwd): """ Invert1D the |VES| data collected in the exporation area. Parameters ------------ data: Dataframe pandas contains the depth measurement AB from current electrodes, the potentials electrodes MN and the collected apparent resistivities. rho0: float - Value of the starting resistivity model. If ``None``, `rho0` should be the half minumm value of the apparent resistivity collected. Units is in Ω.m not log10(Ω.m) h0: float - Thickness in meter of the first layers in meters. If ``None``, it should be the minimum thickess as possible ``1.``m. strategy: str - Type of inversion scheme. The defaut is Hybrid Monte Carlo (HMC) known as ``HMCMC``. Another scheme is Bayesian neural network approach (``BNN``). kwd: dict - Additionnal keywords arguments from |VES| data operations. See :doc:`watex.utils.exmath.vesDataOperator` for futherdetails. .. |VES| replace: Vertical Electrical Sounding """ self.inspect # invert data #XXX TODO if strategy is not None: self.strategy = strategy invertVES(data= self.data_, h0 = self.h0 , rho0 = self.rho0, typeof = self.strategy , **kwd) return self
def __repr__(self): """ Pretty format for developers following the API... """ return repr_callable_obj(self) def __getattr__(self, name): rv = smart_strobj_recognition(name, self.__dict__, deep =True) appender = "" if rv is None else f'. Do you mean {rv!r}' if name =='table_': err_msg =(". Call 'summary' method to fetch attribute 'table_'") else: err_msg = f'{appender}{"" if rv is None else "?"}' raise AttributeError ( f'{self.__class__.__name__!r} object has no attribute {name!r}' f'{err_msg}' ) @property def inspect (self): """ Inspect object whether is fitted or not""" msg = ( "{obj.__class__.__name__} instance is not fitted yet." " Call 'fit' with appropriate arguments before using" " this method" ) if not hasattr (self, 'ohmic_area_'): raise NotFittedError(msg.format( obj=self) ) return 1
def _parse_dc_objs ( *data, method = 'ResistivityProfiling', ): """ Separate module objects objects if exists from the whole data valid data. Parameters ----------- data: list Collection of DC objects method: str, Method of selection. return_diff: bool, default=False, Retuns the remain objects which are not DCProfiling or DCSounding Returns -------- dc0, remain_data: Tuple of list A collection of selected DC objects from other objects objects """ dco=[] dco = list(filter ( lambda o : get_estimator_name ( o)== method, data ) ) # get the remain data which are not a # DCObjects remain_data = list(filter ( lambda o: get_estimator_name(o) != method, data ) ) return dco, remain_data def _parse_dc_args(self, dcmethod: str ,survey_names=None, **kws): """ parse dc arguments to fit the number of survey lines, populate and sanitize the attributes accordingly. :param dcmethod: :param kws: Additional keyword from :func:`watex.utils.coreutils.parseDCArgs`. It refers to the `station_delimiter` parameters. """ flag=0 if dcmethod =='ResistivityProfiling': sf , arg = self.stations , 'stations' flag=0 elif dcmethod =='VerticalSounding': sf, arg =self.search , 'search' flag=1 # write an error msg msg =''.join([ f"### Number of {arg!r} does not fit the number of" " data. Expect {0} but {1} {2} given." ]) if sf is None: sf= np.repeat ([45.] if flag else [None], len(survey_names)) elif sf is not None: if os.path.isfile (str(sf)): sf=parseDCArgs(sf, arg=arg, **kws) elif isinstance (sf, str): sf= [sf] elif ( isinstance(sf, (int , float)) and flag ): sf= np.repeat ([sf], len(survey_names)) if len(sf)!= len(survey_names): fmsg = msg.format(len(survey_names),len(sf), f"{'is' if len(sf)<2 else 'are'}") self._logging.error (fmsg) warnings.warn(fmsg) if self.verbose > 3: print("-->!Number of DC-resistivity data read sucessfully" f"= {len(self.survey_names_)}. Number of the given" " stations considered as a drilling points" f"={len(sf)}. Station must fit each survey lines." ) raise StationError (msg.format( len(survey_names), len(sf) , f"{'is' if len(sf) <=1 else 'are'}", )) if not flag: self.stations = sf elif flag: self.search = sf return survey_names def _summary (self, return_table = True): """ Isolated part of `summary` method of DC-resistivity method. Agregate the DC-Resistivity method parameters :param return_table: bool, default=True returns table of DC parameters at all sites if ``True`` and 'DC-Resistivity method' instanciated object otherwise. :returns: - table if `return_table` is ``True`` and `DC-Resistivity method` instanciated object otherwise. """ # check whether there is valid data if len( self.ids_)==0: warnings.warn("No DC objects to draw summary. Use <.isnotvalid_>" " attribute to get the list of non-valid data.") return tables =[] vids_ =[] for sl in self.ids_: try: t= getattr( getattr(self, sl), "table_") except AttributeError as er: msg =( "This probably occurs because {sl!r} has no parameters computed." f" It seems the data for {sl!r} seems invalid. Use attribute" " 'isnotvalid_' to check whether the site is valid or not." ) warnings.warn(str (er) + '.' + msg) continue tables.append (t ) vids_.append(sl) self.table_ = pd.concat( tables , axis =0 ) self.table_.index = vids_ return self.table_ if return_table else self def _make_param_table( self, uparams ): """ Make parameters table """ for k in uparams: if hasattr( getattr(self, k , np.nan ), '__len__') and len( getattr(self, k , np.nan ))==0: v = np.nan else: v = getattr(self, k , np.nan ) yield (f"{k[:-1] if k.endswith('_') else k }", v) def _geterpattr (attr , dl ): """ Get attribute from the each DC-resistivity object and collect into numpy array. If `stack` is ``True``, it will collect stacked data allong axis 1. :param attr: attribute name :param dl: list of erp object """ # np.warnings.filterwarnings( # 'ignore', category=np.VisibleDeprecationWarning) return np.array(list(map(lambda o : getattr(o, attr), dl )), dtype =object) def _validate_file_in (*data ): """ Validate DC-types - file ( F-type ), path (P-type) or dataframe (D-type). """ msg=("{0!r} is set as a priority DCtype. Expect DC data to have a" " consistent type. Only '{1}' data fit {0} while expecting" " '{2}'. Please check your data and use only a single DC-type:" " D-type (Dataframe), F-type (File object) or P-type (Pathlike).") for ii, o in enumerate ( ('D-type', 'P-type', 'F-type') ): if ii ==0: s = [ True if ( hasattr ( d , '__array__') and hasattr (d, 'columns')) else False for d in data ] if ii==1: s= [True if os.path.isdir ( str(d)) else False for d in data ] if ii==2: s = [True if os.path.isfile(str(d)) else False for d in data ] if len(set ( s))==2: # {'false', 'true'} raise DCError(msg.format(o, s.count(True), len(data))) # { true} or {'false } if len( set(s)) ==1 and s[0]: return o return def _readfromdcObjs(self, *data , dcmethod:str = "ResistivityProfiling", **fit_params) : """ Read multiple data with different kinds including the meta-DC-objects. Read metadata DC object as a set of :param data: list-a collection of DC-resistivity method objects :param dcmethod: str object of :class:`.ResistivityProfiling` or :class:`.VerticalSounding` objects. :returns: bool- whether an object is readable as a DC-resistivity profiling or sounding object or not.``False`` otherwise. """ if hasattr (dcmethod , '__module__'): dcmethod= get_estimator_name(dcmethod ) # separate DCobj from others files objs , others = _parse_dc_objs(*data , method = dcmethod) # make temp line/sites if objects are given name = 'line' if dcmethod =='ResistivityProfiling' else 'site' survey_names = [ name + "{kk +1}" for i in range(len(objs))] surv_names=None if len(others) !=0: others, surv_names = _readfrompath ( self, *others , dcmethod = dcmethod , **fit_params ) # # if D-type , rename survey line/or site # this will fit the number of site or line survey_names += [] if surv_names is None else list(surv_names) data0 = objs + list(others ) try: bname = "{:<8}".format( 'dc-erp' if dcmethod =='ResistivityProfiling' else 'dc-ves') pbar = data0 if not TQDM else tqdm.tqdm(data0 ,ascii=True, unit='B', desc =bname, ncols =77) except NameError: # force pbar to hold the data value # if trouble occurs pbar =data0 unwanted_snames =[] for kk, o in enumerate (pbar): if get_estimator_name(o) == dcmethod: if not hasattr ( o, 'table_'): o.summary(return_table =False ) self.data_.append( o) pbar.update(kk) if TQDM else '' continue # reset index kk by excluding the number of # DC objects. ss= kk - len(objs) # -> read the data and make dc Objs try: if dcmethod =='ResistivityProfiling': dcObj = ResistivityProfiling( station = self.stations[ss] , dipole= self.dipole, auto=True if self.stations[ss] is None else self.auto, utm_zone = self.utm_zone, force=self.force ) self.data_.append (dcObj.fit(o).summary( keep_params=self.keep_params)) self.stations[ss] = dcObj.sves_ elif dcmethod =='VerticalSounding': dcObj = VerticalSounding( search=self.search[ss], vesorder=self.vesorder, typeofop=self.typeofop, objective=self.objective, rho0=self.rho0, h0=self.h0, strategy=self.strategy, ) self.data_.append (dcObj.fit(o).summary( keep_params=self.keep_params)) except : self.isnotvalid_.append(o) # rather than keep the dataframe name, used the File names # that are anot successfull read. unwanted_snames.append(surv_names[ss]) pbar.update(kk) if TQDM else None # let's keep the valid survey names # and pop the invalid names # reconvert survey names into an array # survey_names= [ # e for e in survey_names if e not in unwanted_snames] survey_names = list(filter ( lambda s : s not in unwanted_snames, survey_names )) name = 'line' if dcmethod=='ResistivityProfiling' else 'site' self.survey_names_ = np.array( make_ids(self.data_, name , None, True)) if self.verbose > 0: #show stats print() show_stats (data , self.data_, obj = 'DC-ERP' if dcmethod =='ResistivityProfiling' else 'DC-VES' , lenl=79) if self.verbose > 3: print(" Number of file unsucceful read is:" f" {len(self.isnotvalid_)}") def _readfrompath (self, *data: List[str | DataFrame ] , dcmethod: object= "ResistivityProfiling", **kws ): """ Read data from a file, a path-like object or dataframe. It collects the list of |ERP| or |VES| files and create a DC -resistivity object from a DC -resistivity method. :param data: str or path-like object, :param kws: Additional keyword from :func:`watex.utils.coreutils.parseStations`. It refers to the `station_delimiter` parameters. """ self._logging.info (" {self.__class__.__name__!r} collects the " "resistivity objects ") # assert whether the method is implemented if dcmethod not in ( 'ResistivityProfiling', 'VerticalSounding'): raise NotImplementedError( f"Method {dcmethod!r} is not implemented") # is_df =False ddict = dict() regex = re.compile (r'[$& #@%^!]', flags=re.IGNORECASE) survey_names = None # initialize # if isinstance(data, (str, pd.DataFrame) ): # data = [data ] is_df = _validate_file_in( *data ) if is_df is None: raise DCError( "Unknow data type, Expect a path-like object, a" f" dataframe or a dc-object. Got {type(data).__name__!r}") if is_df =='P-type': data = [os.path.join( data[0], d ) for d in os.listdir(data[0])] elif is_df =='F-type': pass if is_df in ("P-type", "F-type"): if self.read_sheets: _, ex = os.path.splitext( data[0]) if ex != '.xlsx': raise TypeError ("Read multisheets expects an excel file " f" extension <'.xlsx'> not: {ex!r}") for d in data : try: ddict.update ( **pd.read_excel (d , sheet_name =None)) except : pass #collect stations names if len(ddict)==0 : raise ERPError ("Can'find the DC-resistitivity profiling data " ) survey_names = list(map( lambda o: regex.sub('_', o).lower(), ddict.keys())) if self.verbose > 3: print(f"Number of the collected data from stations are" f" : {len(survey_names)}") data = list(ddict.values ()) # make a survey id from collection object if survey_names is None: survey_names = list(map(lambda o :regex.sub( '_', os.path.basename(o)), data )) # remove the extension and keep files names survey_names = list( map(lambda o: o.split('.')[0], survey_names)) if is_df =='D-type': name = 'line' if dcmethod =='ResistivityProfiling' else 'site' survey_names = [ name + "{kk +1}" for i in range(len(data))] # populate and assert stations and search #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # if list of station is not given for each file # note that here station is station where one expect to # locate a drilling drilling i.e. sves survey_names = _parse_dc_args(self, dcmethod, survey_names, **kws) return data, survey_names