Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fitReciprocalErrorModel IP data #725

Open
HenryWHR opened this issue May 23, 2024 · 7 comments
Open

fitReciprocalErrorModel IP data #725

HenryWHR opened this issue May 23, 2024 · 7 comments
Assignees

Comments

@HenryWHR
Copy link

Hi,

I am wondering if there is a way to fit normal/reciprocal error model for IP data. It works fine with DC data by
err = ert.fitReciprocalErrorModel(data, nBins=20, show=True, rel=False)

but have no idea how to do this with IP data in pygimli.

Thanks in advance for your kind reply!

Best regards,
Henry

@halbmy halbmy self-assigned this May 24, 2024
@halbmy
Copy link
Contributor

halbmy commented May 24, 2024

Very good point! It should of course also work for IP using

iperr=ert.fitReciprocalErrorModel(data, ip=True, ...)

using the approach of Flores Orozco et al. (2012), and

ert.reciprocalProcessing(data)

should do it automatically if IP data are present.

I personally don't have any IP dataset with reciprocals. Mostly I am measuring with multi-gradient array.

@HenryWHR
Copy link
Author

Hi Thomas,

Thank you very much for your quick reply! I attached one dataset where some N/R measurements are included. Unfortunately, neither option works for IP data on my laptop. The error says [fitReciprocalErrorModel() got an unexpected keyword argument 'ip']. With ert.reciprocalProcessing(data), the returned iperr are all zero although IP data are present.

Details

OS : Windows
CPU(s) : 14
Machine : AMD64
Architecture : 64bit
RAM : 31.7 GiB
Environment : IPython

Python 3.11.9 | packaged by conda-forge | (main, Apr 19 2024, 18:27:10) [MSC
v.1938 64 bit (AMD64)]

       pygimli : 1.5.1
        pgcore : 1.5.0
         numpy : 1.26.4
    matplotlib : 3.8.4
         scipy : 1.13.0
          tqdm : 4.66.4
       IPython : 8.20.0
       pyvista : 0.43.8

testdata.zip

@halbmy
Copy link
Contributor

halbmy commented May 31, 2024

Dear Henry, it was nice to meet you on the IP workshop and discuss the matter. Unlike apparent resistivity, IP data may not fully fulfil reciprocity as electromagnetic effects like capacitive (cable) coupling or inductive coupling through the subsurface depend on the measuring layout which can be different for normal and reverse measurement. So one might need to distinguish systematic errors from Gaussian noise. We can extend this discussion also in the IP community

@HenryWHR
Copy link
Author

HenryWHR commented Jun 1, 2024

Hi Thomas,

It was a great pleasure talking to you during the IP workshop. I agree that reciprocal error can be tricky for IP data, and we should therefore be careful with it when processing. As we discussed, I will look into the source code to implement this part (error model estimation) for IP data. I believe that it is useful in some cases and not too complicated to add to pygimli.

I suggest that we close this issue in pygimli for now.

@HenryWHR
Copy link
Author

HenryWHR commented Jul 8, 2024

Hi Thomas,

I have now implemented the normal/reciprocal error model estimation for IP data. I believe it might be useful sometimes. The modified code and a synthetic dataset for testing are attached below. It works fine on my laptop.

def fitReciprocalErrorModel(data, nBins=None, show=False, rel=False, ip=False):
    """Fit an error by statistical normal-reciprocal analysis.

    Identify normal reciprocal pairs and fit error model to it

    Parameters
    ----------
    data : pg.DataContainerERT
        input data
    nBins : int
        number of bins to subdivide data (4 < data.size()//30 < 30)
    rel : bool [False]
        fit relative instead of absolute errors
    show : bool [False]
        generate plot of reciprocals, mean/std bins and error model

    Returns
    -------
    ab : [float, float]
        indices of a+b*R (rel=False) or a+b/R (rec=True)
    """
    R = getResistance(data)
    
    iF, iB = reciprocalIndices(data, True)
    n30 = len(iF) // 30
    nBins = nBins or np.maximum(np.minimum(n30, 30), 4)
    RR = np.abs(R[iF] + R[iB]) / 2
    sInd = np.argsort(RR)
    RR = RR[sInd]
    dR = (R[iF] - R[iB])[sInd]
    inds = np.linspace(0, len(RR), nBins+1, dtype=int)
    stdR = np.zeros(nBins)
    meanR = np.zeros(nBins)
    for b in range(nBins):
        ii = range(inds[b], inds[b+1])
        stdR[b] = np.std(dR[ii])
        meanR[b] = np.mean(RR[ii])

    G = np.ones([len(meanR), 2]) # a*x+b
    w = np.reshape(np.isfinite(meanR), [-1, 1])
    meanR[np.isnan(meanR)] = 1e-16
    stdR[np.isnan(stdR)] = 0
    if rel:
        G[:, 1] = 1 / meanR
        ab, *_ = np.linalg.lstsq(w*G, stdR/meanR, rcond=None)
    else:
        G[:, 1] = meanR
        ab, *_ = np.linalg.lstsq(w*G, stdR, rcond=None)
        
        
    if ip:
        m = data['ip']
        dm = (m[iF] - m[iB])[sInd]
        stdm = np.zeros(nBins)
        for b in range(nBins):
            ii = range(inds[b], inds[b+1])
            stdm[b] = np.std(dm[ii])
    
        # fitting the inverse power law model (Flores Orozco et al., 2012) in log-space (i.e. linear fitting)
        logstdm = np.log(stdm)
        logmR = np.log(meanR)
        G_ip = np.ones([len(logmR), 2]) # a*x+b
        w_ip = np.reshape(np.isfinite(logmR), [-1, 1])
        # logmR[np.isnan(logmR)] = 1e-16
        # logstdm[np.isnan(logstdm)] = 0
        G_ip[:, 1] = logmR
        abip, *_ = np.linalg.lstsq(w_ip*G_ip, logstdm, rcond=None)
        abip[0] = np.exp(abip[0]) # convert to the parameter in power law model

    if show:
        x = np.logspace(np.log10(min(meanR)), np.log10(max(meanR)), 30)
        _, ax = pg.plt.subplots()
        if rel:
            ax.semilogx(RR, dR/RR, '.', label='data')  # /RR
            ii = meanR > 1e-12
            x = x[x > 1e-12]
            eModel = ab[0] + ab[1] / x
            ax.plot(meanR[ii], stdR[ii]/meanR[ii], '*', label='std dev') # /meanR
            ax.set_title(r'$\delta$R/R={:.6f}+{:.6f}/|R|'.format(*ab))
            ax.set_ylabel(r'$\delta$R/R')
        else:
            eModel = ab[0] + ab[1] * x
            ax.semilogx(RR, dR, '.', label='data')  # /RR
            ax.plot(meanR, stdR, '*', label='std dev') # /meanR
            ax.set_title(r'$\delta$R={:.6f}+{:.6f}*|R|'.format(*ab))
            ax.set_ylabel(r'$\delta$R ($\Omega$)')

        ax.plot(x, eModel, '-', label='error model') # /x
        ax.grid(which='both')
        ax.set_xlabel(r'R ($\Omega$)')
        ax.legend()
        if ip:
            _, ax = pg.plt.subplots()
            eModel_ip = abip[0] * (x**abip[1])
            ax.semilogx(RR, dm, '.', label='data')  
            ax.plot(meanR, stdm, '*', label='std dev') 
            ax.set_title(r'$\delta\phi$={:.6f}*|R|^{:.6f}'.format(*abip))
            ax.set_ylabel(r'$\delta\phi$ (mrad)')
            ax.plot(x, eModel_ip, '-', label='error model')
            ax.grid(which='both')
            ax.set_xlabel(r'R ($\Omega$)')
            ax.legend()
            return ab, abip, ax
        else:
            return ab, ax
    else:
        return ab, abip

testdata.zip

@halbmy
Copy link
Contributor

halbmy commented Jul 9, 2024

Many thanks for this effort!

I also checked the code with your testfile. With rel=False I got a resistance error of -3milliOhms plus +7% but with rel=True I got 3.7 milliOhms plus 7% which really makes sense. For the IP error I got $d\Phi=1.4|R|^(-0.5)$ which is very similar to the values in the upper right plot of Fig. 7 in Adrians paper ($1.92 |R|^(-0.61)$).
image

The error model is between 0.3 and 2.5 mrad (note the symlog scale) which makes sense to me.
We need to check with other files how robust this procedure is. Anyone with data is welcome to test and report.

@halbmy
Copy link
Contributor

halbmy commented Jul 9, 2024

I just don't like the four possible different output arguments (ab only, ab/ax, ab/abip, ab/abip/ax) and that the first axes is lost in case of IP. I would rather redirect ip=True (or ip=anyarray) to do IP reciprocal analysis only so that it can also be used to do it spectrally and the output is the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants