Source code for pygimli.physics.ert.visualization

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""View ERT data."""

from math import pi
import numpy as np
from numpy import ma

import pygimli as pg
from pygimli.viewer.mpl.dataview import showValMapPatches
from pygimli.viewer.mpl import showDataContainerAsMatrix


[docs] def generateDataPDF(data, filename="data.pdf"): """Generate a multi-page pdf showing all data properties.""" if isinstance(data, str): filename = data.replace('.txt', '-data.pdf') data = pg.load(data) from matplotlib.backends.backend_pdf import PdfPages logToks = ["Uout(V)", "u", "i", "r", "rhoa"] with PdfPages(filename) as pdf: fig = pg.plt.figure() for tok in data.tokenList().split(): if data.haveData(tok): vals = data[tok] logScale = min(vals) > 0 and tok in logToks ax = fig.add_subplot() pg.show(data, vals, ax=ax, label=tok, logScale=logScale) fig.savefig(pdf, format='pdf') fig.clf()
[docs] def showERTData(data, vals=None, **kwargs): """Plot ERT data as pseudosection matrix (position over separation). Creates figure, axis and draw a pseudosection. Parameters ---------- data : :gimliapi:`BERT::DataContainerERT` **kwargs : * vals : array[nData] | str Values to be plotted. Default is data['rhoa']. Can be array or string whose data field is extracted. * axes : matplotlib.axes Axes to plot into. By default (None), a new figure with a single Axes is created. * x and y : str | list(str) forces using matrix plot (drawDataContainerAsMatrix) x, y define the electrode number on x and y axis can be strings ("a", "m", "mid", "sep") or lists of them ["a", "m"] * style : str predefined styles for choosing x and y arguments (x/y overrides) - "ab-mn" (default): any combination of current/potential electrodes - "a-m" : only a and m electrode (for unique dipole spacings like DD) - "a-mn" : a and combination of mn electrode (PD with different MN) - "ab-m" : a and combination of mn electrode - "sepa-m" : current dipole length with a and m (multi-gradient) - "a-sepm" : a and potential dipole length with m * switchxy : bool exchange x and y axes before plotting Returns ------- ax : matplotlib.axes axis containing the plots cb : matplotlib.colorbar colorbar instance """ # remove ax keyword globally (problems with colorbar) ax = kwargs.pop('ax', None) if ax is None: fig = pg.plt.figure() ax = None axTopo = None if 'showTopo' in kwargs: # can be removed? ax = fig.add_subplot(1, 1, 1) # axs = fig.subplots(2, 1, sharex=True) # # Remove horizontal space between axes # fig.subplots_adjust(hspace=0) # ax = axs[1] # axTopo = axs[0] else: ax = fig.add_subplot(1, 1, 1) pg.checkAndFixLocaleDecimal_point(verbose=False) if vals is None: vals = 'rhoa' if isinstance(vals, str): if data.haveData(vals): kwargs.setdefault('label', pg.utils.unit(vals)) vals = data(vals) else: pg.critical('field not in data container: ', vals) kwargs.setdefault('cMap', pg.utils.cMap('rhoa')) # better vals? kwargs.setdefault('label', pg.utils.unit('rhoa')) kwargs.setdefault('logScale', min(vals) > 0.0) sty = kwargs.pop("style", None) if isinstance(sty, str): sty = sty.lower() if sty == "a-m": kwargs.setdefault("y", "a") kwargs.setdefault("x", "m") elif sty == "a-mn": kwargs.setdefault("y", "a") kwargs.setdefault("x", ["m", "n"]) elif sty == "ab-m": kwargs.setdefault("y", ["a", "b"]) kwargs.setdefault("x", "m") elif sty == "sepa-m": data["ab"] = np.abs(data["b"] - data["a"]) kwargs.setdefault("y", ["ab", "a"]) kwargs.setdefault("x", "m") elif sty == "a-sepm": data["mn"] = np.abs(data["n"] - data["m"]) kwargs.setdefault("x", "a") kwargs.setdefault("y", ["mn", "m"]) elif sty is not None and sty != 0: kwargs.setdefault("y", ["a", "b"]) kwargs.setdefault("x", ["m", "n"]) if "x" in kwargs and "y" in kwargs: if kwargs["y"] == "mid": kwargs["y"] = (data["a"] + data["b"]) / 2 elif kwargs["y"] == "sep": kwargs["y"] = np.abs(data["a"] - data["b"]) if kwargs["x"] == "mid": kwargs["x"] = (data["m"] + data["n"]) / 2 elif kwargs["x"] == "sep": kwargs["x"] = np.abs(data["m"] - data["n"]) if kwargs.pop("switchxy", False): kwargs["x"], kwargs["y"] = kwargs["y"], kwargs["x"] ax, cbar = showDataContainerAsMatrix(data, v=vals, ax=ax, **kwargs) else: equidistant = kwargs.pop("equidistant", False) if not equidistant: try: ax, cbar = drawERTData(ax, data, vals=vals, **kwargs) except Exception: pg.warning('Something gone wrong while drawing data. ' 'Try fallback with equidistant electrodes.') equidistant = True if equidistant: d = pg.DataContainerERT(data) sc = data.sensorCount() d.setSensors(list(zip(range(sc), np.zeros(sc)))) ax, cbar = drawERTData(ax, d, vals=vals, **kwargs) # TODO here cbar handling like pg.show if 'xlabel' in kwargs: ax.set_xlabel(kwargs['xlabel']) if 'ylabel' in kwargs: ax.set_ylabel(kwargs['ylabel']) if 'showTopo' in kwargs: # if axTopo is not None: print(ax.get_position()) axTopo = pg.plt.axes([ax.get_position().x0, ax.get_position().y0, ax.get_position().x0+0.2, ax.get_position().y0+0.2]) x = pg.x(data) x *= (ax.get_xlim()[1] - ax.get_xlim()[0]) / (max(x)-min(x)) x += ax.get_xlim()[0] axTopo.plot(x, pg.z(data), '-o', markersize=4) axTopo.set_ylim(min(pg.z(data)), max(pg.z(data))) axTopo.set_aspect(1) pg.viewer.mpl.updateAxes(ax) return ax, cbar
[docs] def drawERTData(ax, data, vals=None, **kwargs): """Plot ERT data as pseudosection matrix (position over separation). Parameters ---------- data : DataContainerERT data container with sensorPositions and a/b/m/n fields vals : iterable of data.size() [data['rhoa']] vector containing the vals to show ax : mpl.axis axis to plot, if not given a new figure is created cMin/cMax : float minimum/maximum color vals logScale : bool logarithmic colour scale [min(A)>0] label : string colorbar label **kwargs: * dx : float x-width of individual rectangles * ind : integer iterable or IVector indices to limit display * circular : bool Plot in polar coordinates when plotting via patchValMap Returns ------- ax: The used Axes cbar: The used Colorbar or None """ if vals is None: vals = data['rhoa'] valid = data.get("valid").array().astype("bool") vals = ma.array(vals, mask=~valid) ind = kwargs.pop('ind', None) sw = kwargs.pop("switch", False) if ind is not None: vals = vals[ind] mid, sep = midconfERT(data, ind, switch=sw) else: mid, sep = midconfERT(data, circular=kwargs.get('circular', False), switch=sw) # var = kwargs.pop('var', 0) # not used anymore cbar = None dx = kwargs.pop('dx', np.median(np.diff(np.unique(mid))))*2 ax, cbar, ymap = showValMapPatches(vals, xVec=mid, yVec=sep, dx=dx, ax=ax, **kwargs) if kwargs.get('circular', False): sM = np.mean(data.sensors(), axis=0) a = np.array([np.arctan2(s[1]-sM[1], s[0]-sM[0]) for s in data.sensors()]) p = list(range(len(a))) # p.append(0) ax.plot(np.cos(a)[p], np.sin(a)[p], 'o', color='black') for i in range(len(a)): ax.text(1.15 * np.cos(a[i]), 1.15 * np.sin(a[i]), str(i+1), horizontalalignment='center', verticalalignment='center') ax.set_axis_off() ax.set_aspect(1) else: ytl = generateConfStr(np.sort([int(k) for k in ymap]), switch=sw) # if only DD1/WE1 in WB/SL data rename to WB/SL if 'DD1' in ytl and 'WB2' in ytl and 'DD2'not in ytl: ytl[ytl.index('DD1')] = 'WB1' if 'WA1' in ytl and 'SL2' in ytl and 'WA2'not in ytl: ytl[ytl.index('WA1')] = 'SL1' yt = ax.get_yticks() yt = np.unique(yt.clip(0, len(ytl)-1)) # if yt[0] == yt[1]: # yt = yt[1:] dyt = np.diff(yt) if len(dyt) > 1 and dyt[-1] < dyt[-2]: yt = yt[:-1] ax.set_yticks(yt) ax.set_yticklabels([ytl[int(yti)] for yti in yt]) return ax, cbar
def midconfERT(data, ind=None, rnum=1, circular=False, switch=False): """Return the midpoint and configuration key for ERT data. Return the midpoint and configuration key for ERT data. Parameters ---------- data : DataContainerERT data container with sensorPositions and a/b/m/n fields ind : [] Documentme rnum : [] Documentme circular : bool Return midpoint in degree (rad) instead if meter. Returns ------- mid : np.array of float representative midpoint (middle of MN, AM depending on array) conf : np.array of float configuration/array key consisting of 1) array type (Wenner-alpha/beta, Schlumberger, PP, PD, DD, MG) 00000: pole-pole 10000: pole-dipole or dipole-pole 30000: Wenner-alpha 40000: Schlumberger or Gradient 50000: dipole-dipole or Wenner-beta 2) potential dipole length (in electrode spacings) .XX..: dipole length 3) separation factor (current dipole length or (di)pole separation) ...XX: pole/dipole separation (PP,PD,DD,GR) or separation """ # xe = np.hstack((pg.x(data.sensorPositions()), np.nan)) # not used anymore x0 = data.sensorPosition(0).x() xe = pg.x(data.sensorPositions()) - x0 ux = pg.unique(xe) mI, mO, mT = 1, 100, 10000 if switch: mI, mO = mO, mI if len(ux) * 2 > data.sensorCount() and not circular: # 2D with topography case dx = np.array(pg.utils.diff(pg.utils.cumDist(data.sensorPositions()))) dxM = pg.mean(dx) if min(pg.y(data)) != max(pg.y(data)) or \ min(pg.z(data)) != max(pg.z(data)): # Topography case if (max(abs(dx-dxM)) < dxM*0.9): # if the maximum spacing < meanSpacing/2 we assume equidistant # spacing and no missing electrodes dx = np.ones(len(dx)) * dxM else: # topography with probably missing electrodes dx = np.floor(dx/np.round(dxM)) * dxM if max(dx) < 0.5: pg.debug("Detecting small distances, using mm accuracy") rnum = 3 xe = np.hstack((0., np.cumsum(np.round(dx, rnum)), np.nan)) de = np.median(np.diff(xe[:-1])).round(rnum) ne = np.round(xe/de) else: # 3D (without topo) case => take positions directly de = np.median(np.diff(ux)).round(1) ne = np.array(xe/de, dtype=int) # a, b, m, n = data['a'], data['b'], data['m'], data['n'] # check if xe[a]/a is better suited (has similar size) if circular: # for circle geometry center = np.mean(data.sensorPositions(), axis=0) r = data.sensors()[0].distance(center) s0 = data.sensors()[0]-center s1 = data.sensors()[1]-center p0 = np.arctan2(s0[1], s0[0]) p1 = np.arctan2(s1[1], s1[0]) if p1 > p0: # rotate left x = np.cos(np.linspace(0, 2*pi, data.sensorCount()+1)+p0)[:-1] * r y = np.sin(np.linspace(0, 2*pi, data.sensorCount()+1)+p0)[:-1] * r else: x = np.cos(np.linspace(2*pi, 0, data.sensorCount()+1)+p0)[:-1] * r y = np.sin(np.linspace(2*pi, 0, data.sensorCount()+1)+p0)[:-1] * r a = np.array([np.arctan2(y[i], x[i]) for i in data['a']]) b = np.array([np.arctan2(y[i], x[i]) for i in data['b']]) m = np.array([np.arctan2(y[i], x[i]) for i in data['m']]) n = np.array([np.arctan2(y[i], x[i]) for i in data['n']]) a = np.unwrap(a) % (np.pi*2) b = np.unwrap(b) % (np.pi*2) m = np.unwrap(m) % (np.pi*2) n = np.unwrap(n) % (np.pi*2) else: a = np.array([ne[int(i)] for i in data['a']]) b = np.array([ne[int(i)] for i in data['b']]) m = np.array([ne[int(i)] for i in data['m']]) n = np.array([ne[int(i)] for i in data['n']]) if ind is not None: a = a[ind] b = b[ind] m = m[ind] n = n[ind] anan = np.isnan(a) a[anan] = b[anan] b[anan] = np.nan ab, am, an = np.abs(a-b), np.abs(a-m), np.abs(a-n) bm, bn, mn = np.abs(b-m), np.abs(b-n), np.abs(m-n) if circular: for v in [ab, mn, bm, an]: v[v > pi] = 2*pi - v[v > pi] # 2-point (default) 00000 sep = np.abs(a-m) # * mI # does not make sense here mid = (a+m) / 2 # 3-point (PD, DP) (now only b==-1 or n==-<1, check also for a and m) imn = np.isfinite(n)*np.isnan(b) mid[imn] = (m[imn]+n[imn]) / 2 sep[imn] = np.minimum(am[imn], an[imn]) * mI + mT + mO * (mn[imn]-1) + \ (np.sign(a[imn]-m[imn])/2+0.5) * mT iab = np.isfinite(b)*np.isnan(n) mid[iab] = (a[iab]+b[iab]) / 2 # better 20000 or -10000? sep[iab] = np.minimum(am[iab], bm[iab]) * mI + mT + mO * (ab[iab]-1) + \ (np.sign(a[iab]-n[iab])/2+0.5) * mT # + 10000*(a-m) # 4-point alpha: 30000 (WE) or 4000 (SL) iabmn = np.isfinite(a) & np.isfinite(b) & np.isfinite(m) & np.isfinite(n) ialfa = np.copy(iabmn) ialfa[iabmn] = (ab[iabmn] >= mn[iabmn]+2) # old mnmid = (m[iabmn] + n[iabmn]) / 2 ialfa[iabmn] = np.sign((a[iabmn]-mnmid)*(b[iabmn]-mnmid)) < 0 mid[ialfa] = (m[ialfa] + n[ialfa]) / 2 spac = np.minimum(bn[ialfa], bm[ialfa]) abmn3 = np.round((3*mn[ialfa]-ab[ialfa])*mT)/mT sep[ialfa] = spac * mI + (mn[ialfa]-1) * mO * (abmn3 != 0) + \ 3*mT + (abmn3 < 0)*mT # gradient # 4-point beta ibeta = np.copy(iabmn) ibeta[iabmn] = (bm[iabmn] >= mn[iabmn]) & (~ialfa[iabmn]) if circular: # print(ab[ibeta]) ibeta = np.copy(iabmn) def _averageAngle(vs): sumsin = 0 sumcos = 0 for v in vs: sumsin += np.sin(v) sumcos += np.cos(v) return np.arctan2(sumsin, sumcos) abC = _averageAngle([a[ibeta], b[ibeta]]) mnC = _averageAngle([m[ibeta], n[ibeta]]) mid[ibeta] = _averageAngle([abC, mnC]) # special case when dipoles are completely opposite iOpp = abs(abs((mnC - abC)) - np.pi) < 1e-3 mid[iOpp] = _averageAngle([b[iOpp], m[iOpp]]) minAb = min(ab[ibeta]) sep[ibeta] = 5 * mT + (np.round(ab[ibeta]/minAb)) * mO + \ np.round(np.minimum(np.minimum(am[ibeta], an[ibeta]), np.minimum(bm[ibeta], bn[ibeta])) / minAb) * mI else: mid[ibeta] = (a[ibeta] + b[ibeta] + m[ibeta] + n[ibeta]) / 4 sep[ibeta] = 5 * mT + (ab[ibeta]-1) * mO + np.minimum( np.minimum(am[ibeta], an[ibeta]), np.minimum(bm[ibeta], bn[ibeta])) * mI # %% 4-point gamma # multiply with electrode distance and add first position if not circular: mid *= de mid += x0 return mid, sep def generateConfStr(yy, switch=False): """Generate configuration string to characterize array.""" mI, mO, mT = 1, 100, 10000 types = ['PP', 'PD', 'DP', 'WA', 'SL', 'DD'] # base types typ = np.round(yy//mT) if switch: dip = yy % mO # source-receiver distance spac = np.round(yy//mO) % mO # MN dipole length else: spac = yy % mO # source-receiver distance dip = np.round(yy//mO) % mO # MN dipole length # check if SL is actually GR (multi-gradient) # check if DD-n-n should be renamed rendd = (np.mean(spac / (dip+1)) < 2.1) keys = [] for s, d, t in zip(spac, dip, typ): key = types[t] if d > 0: if rendd and d+1 == s and t == 5: key = 'WB' else: key = key + str(d+1) + '-' key = key + "{:2d}".format(s) # str(s) keys.append(key) return keys