def test_printstartfinish(capsys): t0 = utils.printstartfinish(0) assert isinstance(t0, float) out, _ = capsys.readouterr() assert out == "" t0 = utils.printstartfinish(3) assert isinstance(t0, float) out, _ = capsys.readouterr() assert ":: empymod START :: v" in out utils.printstartfinish(0, t0) out, _ = capsys.readouterr() assert out == "" utils.printstartfinish(3, t0) out, _ = capsys.readouterr() assert out[:27] == "\n:: empymod END; runtime = " utils.printstartfinish(3, t0, 13) out, _ = capsys.readouterr() assert out[-19:] == "13 kernel call(s)\n\n"
def dipole(src, rec, depth, res, freqtime, aniso=None, eperm=None, mperm=None, verb=2): r"""Return the electromagnetic field due to a dipole source. This is a modified version of :func:`empymod.model.dipole`. It returns the separated contributions of TM--, TM-+, TM+-, TM++, TMdirect, TE--, TE-+, TE+-, TE++, and TEdirect. Parameters ---------- src, rec : list of floats or arrays Source and receiver coordinates (m): [x, y, z]. The x- and y-coordinates can be arrays, z is a single value. The x- and y-coordinates must have the same dimension. Sources or receivers placed on a layer interface are considered in the upper layer. Sources and receivers must be in the same layer. depth : list Absolute layer interfaces z (m); #depth = #res - 1 (excluding +/- infinity). res : array_like Horizontal resistivities rho_h (Ohm.m); #res = #depth + 1. freqtime : float Frequency f (Hz). (The name `freqtime` is kept for consistency with :func:`empymod.model.dipole`. Only one frequency at once. aniso : array_like, optional Anisotropies lambda = sqrt(rho_v/rho_h) (-); #aniso = #res. Defaults to ones. eperm : array_like, optional Relative electric permittivities epsilon (-); #eperm = #res. Default is ones. mperm : array_like, optional Relative magnetic permeabilities mu (-); #mperm = #res. Default is ones. verb : {0, 1, 2, 3, 4}, optional Level of verbosity, default is 2: - 0: Print nothing. - 1: Print warnings. - 2: Print additional runtime and kernel calls - 3: Print additional start/stop, condensed parameter information. - 4: Print additional full parameter information Returns ------- TM, TE : list of ndarrays, (nfreq, nrec, nsrc) Frequency-domain EM field [V/m], separated into TM = [TM--, TM-+, TM+-, TM++, TMdirect] and TE = [TE--, TE-+, TE+-, TE++, TEdirect]. However, source and receiver are normalised. So the source strength is 1 A and its length is 1 m. Therefore the electric field could also be written as [V/(A.m2)]. The shape of EM is (nfreq, nrec, nsrc). However, single dimensions are removed. """ # === 1. LET'S START ============ t0 = printstartfinish(verb) # === 2. CHECK INPUT ============ # Check layer parameters model = check_model(depth, res, aniso, eperm, eperm, mperm, mperm, False, verb) depth, res, aniso, epermH, epermV, mpermH, mpermV, _ = model # Check frequency => get etaH, etaV, zetaH, and zetaV frequency = check_frequency(freqtime, res, aniso, epermH, epermV, mpermH, mpermV, verb) freq, etaH, etaV, zetaH, zetaV = frequency # Check src and rec src, nsrc = check_dipole(src, 'src', verb) rec, nrec = check_dipole(rec, 'rec', verb) # Get offsets off, ang = get_off_ang(src, rec, nsrc, nrec, verb) # Get layer number in which src and rec reside (lsrc/lrec) lsrc, zsrc = get_layer_nr(src, depth) lrec, zrec = get_layer_nr(rec, depth) # Check limitations of this routine compared to the standard `dipole` if lsrc != lrec: # src and rec in same layer raise ValueError("src and rec must be in the same layer; " f"<lsrc>/<lrec> provided: {lsrc}/{lrec}.") if depth.size < 2: # at least two layers raise ValueError("model must have more than one layer; " f"<depth> provided: {_strvar(depth[1:])}.") if freq.size > 1: # only 1 frequency raise ValueError("only one frequency permitted; " f"<freqtime> provided: {_strvar(freqtime)}.") # === 3. EM-FIELD CALCULATION ============ # This part is a simplification of: # - model.fem() # - transform.dlf() # - kernel.wavenumber() # DLF filter we use filt = key_201_2012() # 3.1. COMPUTE REQUIRED LAMBDAS for given hankel-filter-base lambd = filt.base / off[:, None] # 3.2. CALL THE KERNEL PTM, PTE = greenfct(zsrc, zrec, lsrc, lrec, depth, etaH, etaV, zetaH, zetaV, lambd) # 3.3. CARRY OUT THE HANKEL TRANSFORM WITH DLF ang_fact = angle_factor(ang, 11, False, False) zm_ang_fact = (ang_fact[:, np.newaxis] - 1) / 2 zp_ang_fact = (ang_fact[:, np.newaxis] + 1) / 2 fact = 4 * np.pi * off # TE [uu, ud, du, dd, df] for i, val in enumerate(PTE): PTE[i] = (ang_fact * np.dot(-val, filt.j1) / off + np.dot(zm_ang_fact * val * lambd, filt.j0)) / fact # TM [uu, ud, du, dd, df] for i, val in enumerate(PTM): PTM[i] = (ang_fact * np.dot(-val, filt.j1) / off + np.dot(zp_ang_fact * val * lambd, filt.j0)) / fact # 3.4. Remove non-physical contributions # (Note: The T*dd corrections differ slightly from the equations given in # the accompanying pdf, due to the way the direct field is accounted for # in the book.) # General parameters Gam = np.sqrt((zetaH * etaH)[:, None, :, None]) # Gam for lambd=0 iGam = Gam[:, :, lsrc, 0] lgam = np.sqrt(zetaH[:, lsrc] * etaH[:, lsrc]) ddepth = np.r_[depth, np.inf] ds = ddepth[lsrc + 1] - ddepth[lsrc] def get_rp_rm(z_eta): r"""Return Rp, Rm.""" # Get Rp/Rm for lambd=0 Rp, Rm = reflections(depth, z_eta, Gam, lrec, lsrc) # Depending on model Rp/Rm have 3 or 4 dimensions. Last two are # wavenumbers and layers btw src and rec, which both are 1. if Rp.ndim == 4: Rp = np.squeeze(Rp, axis=3) if Rm.ndim == 4: Rm = np.squeeze(Rm, axis=3) Rp = np.squeeze(Rp, axis=2) Rm = np.squeeze(Rm, axis=2) # Calculate reverberation M and general factor npfct Ms = 1 - Rp * Rm * np.exp(-2 * iGam * ds) npfct = ang_fact * zetaH[:, lsrc] / (fact * off * lgam * Ms) return Rp, Rm, npfct # TE modes TE[uu, ud, du, dd] Rp, Rm, npfct = get_rp_rm(zetaH) PTE[0] += npfct * Rp * Rm * np.exp(-lgam * (2 * ds - zrec + zsrc)) PTE[1] += npfct * Rp * np.exp(-lgam * (2 * ddepth[lrec + 1] - zrec - zsrc)) PTE[2] += npfct * Rm * np.exp(-lgam * (zrec + zsrc)) PTE[3] += npfct * Rp * Rm * np.exp(-lgam * (2 * ds + zrec - zsrc)) # TM modes TM[uu, ud, du, dd] Rp, Rm, npfct = get_rp_rm(etaH) PTM[0] -= npfct * Rp * Rm * np.exp(-lgam * (2 * ds - zrec + zsrc)) PTM[1] += npfct * Rp * np.exp(-lgam * (2 * ddepth[lrec + 1] - zrec - zsrc)) PTM[2] += npfct * Rm * np.exp(-lgam * (zrec + zsrc)) PTM[3] -= npfct * Rp * Rm * np.exp(-lgam * (2 * ds + zrec - zsrc)) # 3.5 Reshape for number of sources for i, val in enumerate(PTE): PTE[i] = np.squeeze(val.reshape((-1, nrec, nsrc), order='F')) for i, val in enumerate(PTM): PTM[i] = np.squeeze(val.reshape((-1, nrec, nsrc), order='F')) # === 4. FINISHED ============ printstartfinish(verb, t0) # return [TMuu, TMud, TMdu, TMdd, TMdf], [TEuu, TEud, TEdu, TEdd, TEdf] return PTM, PTE
def design(n, spacing, shift, fI, fC=False, r=None, r_def=(1, 1, 2), reim=None, cvar='amp', error=0.01, name=None, full_output=False, finish=False, save=True, path='filters', verb=2, plot=1): r"""Digital linear filter (DLF) design This routine can be used to design digital linear filters for the Hankel or Fourier transform, or for any linear transform ([Ghos70]_). For this included or provided theoretical transform pairs can be used. Alternatively, one can use the EM modeller empymod to use the responses to an arbitrary 1D model as numerical transform pair. This filter designing tool uses the direct matrix inversion method as described in [Kong07]_ and is based on scripts by [Key12]_. The tool is an add-on to the electromagnetic modeller empymod [Wert17]_. Fruitful discussions with Evert Slob and Kerry Key improved the add-on substantially. Example notebooks of its usage can be found in the documentation-gallery, https://empymod.readthedocs.io/en/stable/examples Parameters ---------- n : int Filter length. spacing: float or tuple (start, stop, num) Spacing between filter points. If tuple, it corresponds to the input for np.linspace with endpoint=True. shift: float or tuple (start, stop, num) Shift of base from zero. If tuple, it corresponds to the input for np.linspace with endpoint=True. fI, fC : transform pairs Theoretical or numerical transform pair(s) for the inversion (I) and for the check of goodness (fC). fC is optional. If not provided, fI is used for both fI and fC. r : array, optional Right-hand side evaluation points for the check of goodness (fC). Defaults to r = np.logspace(0, 5, 1000), which are a lot of evaluation points, and depending on the transform pair way too long r's. r_def : tuple (add_left, add_right, factor), optional Definition of the right-hand side evaluation points r of the inversion. r is derived from the base values, default is (1, 1, 2). - rmin = log10(1/max(base)) - add_left - rmax = log10(1/min(base)) + add_right - r = logspace(rmin, rmax, factor*n) reim : np.real or np.imag, optional Which part of complex transform pairs is used for the inversion. Defaults to np.real. cvar : string {'amp', 'r'}, optional If 'amp', the inversion minimizes the amplitude. If 'r', the inversion maximizes the right-hand side evaluation point r. Defaults is 'amp'. error : float, optional Up to which relative error the transformation is considered good in the evaluation of the goodness. Default is 0.01 (1 %). name : str, optional Name of the filter. Defaults to dlf_+str(n). full_output : bool, optional If True, returns best filter and output from scipy.optimize.brute; else only filter. Default is False. finish : None, True, or callable, optional If callable, it is passed through to scipy.optimize.brute: minimization function to find minimize best result from brute-force approach. Default is None. You can simply provide True in order to use scipy.optimize.fmin_powell(). Set this to None if you are only interested in the actually provided spacing/shift-values. save : bool, optional If True, best filter is saved to plain text files in ./filters/. Can be loaded with fdesign.load_filter(name). If full, the inversion output is stored too. You can add '.gz' to `name`, which will then save the full inversion output in a compressed file instead of plain text. path : string, optional Absolute or relative path where output will be saved if `save=True`. Default is 'filters'. verb : {0, 1, 2}, optional Level of verbosity, default is 2: - 0: Print nothing. - 1: Print warnings. - 2: Print additional time, progress, and result plot : {0, 1, 2, 3}, optional Level of plot-verbosity, default is 1: - 0: Plot nothing. - 1: Plot brute-force result - 2: Plot additional theoretical transform pairs, and best inv. - 3: Plot additional inversion result (can result in lots of plots depending on spacing and shift) If you are using a notebook, use %matplotlib notebook to have all inversion results appear in the same plot. Returns ------- filter : empymod.filter.DigitalFilter instance Best filter for the input parameters. full : tuple Output from scipy.optimize.brute with full_output=True. (Returned when `full_output` is True.) """ # === 1. LET'S START ============ t0 = printstartfinish(verb) # Check plot with matplotlib (soft dependency) if plot > 0 and not plt: plot = 0 if verb > 0: print(plt_msg) # Ensure fI, fC are lists def check_f(f): if hasattr(f, 'name'): # put into list if single tp f = [f, ] else: # ensure list (works for lists, tuples, arrays) f = list(f) return f if not fC: # copy fI if fC not provided fC = dc(fI) fI = check_f(fI) if fI[0].name == 'j2': raise ValueError("j2 (jointly j0 and j1) is only implemented for " "fC, not for fI!") fC = check_f(fC) # Check default input values if finish and not callable(finish): finish = fmin_powell if name is None: name = 'dlf_'+str(n) if r is None: r = np.logspace(0, 5, 1000) if reim not in [np.real, np.imag]: reim = np.real # Get spacing and shift slices, cast r ispacing = _ls2ar(spacing, 'spacing') ishift = _ls2ar(shift, 'shift') r = np.atleast_1d(r) # Initialize log-dict to keep track in brute-force minimization-function. log = {'cnt1': -1, # Counter 'cnt2': -1, # %-counter; v Total number of iterations v 'totnr': np.arange(*ispacing).size*np.arange(*ishift).size, 'time': t0, # Timer 'warn-r': 0} # Warning for short r # === 2. THEORETICAL MODEL rhs ============ # Calculate rhs for i, f in enumerate(fC): fC[i].rhs = f.rhs(r) # Plot if plot > 1: _call_qc_transform_pairs(n, ispacing, ishift, fI, fC, r, r_def, reim) # === 3. RUN BRUTE FORCE OVER THE GRID ============ full = brute(_get_min_val, (ispacing, ishift), full_output=True, args=(n, fI, fC, r, r_def, error, reim, cvar, verb, plot, log), finish=finish) # Add cvar-information to full: 0 for 'amp', 1 for 'r' if cvar == 'r': full += (1, ) else: full += (0, ) # Finish output from brute/fmin; depending if finish or not if verb > 1: print("") if callable(finish): print("") # Get best filter (full[0] contains spacing/shift of the best result). dlf = _calculate_filter(n, full[0][0], full[0][1], fI, r_def, reim, name) # If verbose, print result if verb > 1: print_result(dlf, full) # === 4. FINISHED ============ printstartfinish(verb, t0) # If plot, show result if plot > 0: print("* QC: Overview of brute-force inversion:") plot_result(dlf, full, False) if plot > 1: print("* QC: Inversion result of best filter (minimum amplitude):") _get_min_val(full[0], n, fI, fC, r, r_def, error, reim, cvar, 0, plot+1, log) # Save if desired if save: if full_output: save_filter(name, dlf, full, path=path) else: save_filter(name, dlf, path=path) # Output, depending on full_output if full_output: return dlf, full else: return dlf