def UpDownComposition2D(nt, nr, dt, dr, rho, vel, nffts=(None, None), critical=100., ntaper=10, scaling=1., dtype='complex128'): r"""2D Up-down wavefield composition. Apply multi-component seismic wavefield composition from its up- and down-going constituents. This input model required by the operator should be created by flattening the concatenated separated wavefields of size :math:`\lbrack n_r \times n_t \rbrack` along the spatial axis. Similarly, the data is also a concatenation of flattened pressure and vertical particle velocity wavefields. Parameters ---------- nt : :obj:`int` Number of samples along the time axis nr : :obj:`int` Number of samples along the receiver axis dt : :obj:`float` Sampling along the time axis dr : :obj:`float` Sampling along the receiver array rho : :obj:`float` Density along the receiver array (must be constant) vel : :obj:`float` Velocity along the receiver array (must be constant) nffts : :obj:`tuple`, optional Number of samples along the wavenumber and frequency axes critical : :obj:`float`, optional Percentage of angles to retain in obliquity factor. For example, if ``critical=100`` only angles below the critical angle :math`\frac{f(k_x)}{vel}` will be retained ntaper : :obj:`float`, optional Number of samples of taper applied to obliquity factor around critical angle scaling : :obj:`float`, optional Scaling to apply to the operator (see Notes for more details) dtype : :obj:`str`, optional Type of elements in input array. Returns ------- UDop : :obj:`pylops.LinearOperator` Up-down wavefield composition operator See Also -------- WavefieldDecomposition: Wavefield decomposition Notes ----- Multi-component seismic data (:math:`p(x, t)` and :math:`v_z(x, t)`) can be synthesized in the frequency-wavenumber domain as the superposition of the up- and downgoing constituents of the pressure wavefield (:math:`p^-(x, t)` and :math:`p^+(x, t)`) as follows [1]_: .. math:: \begin{bmatrix} \mathbf{p}(k_x, \omega) \\ \mathbf{v_z}(k_x, \omega) \end{bmatrix} = \begin{bmatrix} 1 & 1 \\ \frac{k_z}{\omega \rho} & - \frac{k_z}{\omega \rho} \\ \end{bmatrix} \begin{bmatrix} \mathbf{p^+}(k_x, \omega) \\ \mathbf{p^-}(k_x, \omega) \end{bmatrix} which we can write in a compact matrix-vector notation as: .. math:: \begin{bmatrix} \mathbf{p} \\ s*\mathbf{v_z} \end{bmatrix} = \begin{bmatrix} \mathbf{F} & 0 \\ 0 & s*\mathbf{F} \end{bmatrix} \mathbf{W} \begin{bmatrix} \mathbf{F}^H & 0 \\ 0 & \mathbf{F}^H \end{bmatrix} \mathbf{p^{\pm}} where :math:`\mathbf{F}` is the 2-dimensional FFT (:class:`pylops.signalprocessing.FFT2`), :math:`\mathbf{W}` is a weighting matrix implemented via :class:`pylops.basicprocessing.Diagonal`, and :math:`s` is a scaling factor that is applied to both the particle velocity data and to the operator has shown above. Such a scaling is required to balance out the different dynamic range of pressure and particle velocity when solving the wavefield separation problem as an inverse problem. As the operator is effectively obtained by chaining basic PyLops operators the adjoint is automatically implemented for this operator. .. [1] Wapenaar, K. "Reciprocity properties of one-way propagators", Geophysics, vol. 63, pp. 1795-1798. 1998. """ nffts = (int(nffts[0]) if nffts[0] is not None else nr, int(nffts[1]) if nffts[1] is not None else nt) # create obliquity operator FFTop = FFT2D(dims=[nr, nt], nffts=nffts, sampling=[dr, dt], dtype=dtype) [Kx, F] = np.meshgrid(FFTop.f1, FFTop.f2, indexing='ij') k = F / vel Kz = np.sqrt((k**2 - Kx**2).astype(np.complex)) Kz[np.isnan(Kz)] = 0 OBL = Kz / (rho * np.abs(F)) OBL[F == 0] = 0 # cut off and taper OBL = _filter_obliquity(OBL, F, Kx, vel, critical, ntaper) OBLop = Diagonal(OBL.ravel(), dtype=dtype) # create up-down modelling operator UDop = (BlockDiag([FFTop.H, scaling*FFTop.H]) * \ Block([[Identity(nffts[0]*nffts[1], dtype=dtype), Identity(nffts[0]*nffts[1], dtype=dtype)], [OBLop, -OBLop]]) * \ BlockDiag([FFTop, FFTop])) return UDop
def WavefieldDecomposition(p, vz, nt, nr, dt, dr, rho, vel, nffts=(None, None), critical=100., ntaper=10, scaling=1., kind='inverse', restriction=None, sptransf=None, solver=lsqr, dottest=False, dtype='complex128', **kwargs_solver): r"""Up-down wavefield decomposition. Apply seismic wavefield decomposition from its multi-component (pressure and vertical particle velocity) data. Parameters ---------- p : :obj:`np.ndarray` Pressure data of of size :math:`\lbrack n_r \times n_t \rbrack` (or :math:`\lbrack n_{r,sub} \times n_t \rbrack` in case a ``restriction`` operator is provided, and :math:`n_{r,sub}` must agree with the size of the output of this an operator) vz : :obj:`np.ndarray` Vertical particle velocity data of size :math:`\lbrack n_r \times n_t \rbrack` (or :math:`\lbrack n_{r,sub} \times n_t \rbrack`) nt : :obj:`int` Number of samples along the time axis nr : :obj:`np.ndarray` Number of samples along the receiver axis of the separated pressure consituents dt : :obj:`float` Sampling along the time axis dr : :obj:`float` Sampling along the receiver array of the separated pressure consituents rho : :obj:`float` Density along the receiver array (must be constant) vel : :obj:`float` Velocity along the receiver array (must be constant) nffts : :obj:`tuple`, optional Number of samples along the wavenumber and frequency axes critical : :obj:`float`, optional Percentage of angles to retain in obliquity factor. For example, if ``critical=100`` only angles below the critical angle :math:`\frac{f(k_x)}{vel}` will be retained ntaper : :obj:`float`, optional Number of samples of taper applied to obliquity factor around critical angle kind : :obj:`str`, optional Type of separation: ``inverse`` (default) or ``analytical`` scaling : :obj:`float`, optional Scaling to apply to the particle velocity data at the restriction : :obj:`pylops.LinearOperator`, optional Restriction operator sptransf : :obj:`pylops.LinearOperator`, optional Sparsifying operator solver : :obj:`float`, optional Function handle of solver to be used if ``kind='inverse'`` dottest : :obj:`bool`, optional Apply dot-test dtype : :obj:`str`, optional Type of elements in input array. **kwargs_solver Arbitrary keyword arguments for chosen ``solver`` Returns ------- pup : :obj:`np.ndarray` Up-going wavefield pdown : :obj:`np.ndarray` Down-going wavefield Raises ------ KeyError If ``kind`` is neither ``analytical`` nor ``inverse`` Notes ----- Up- and down-going components of seismic data (:math:`p^-(x, t)` and :math:`p^+(x, t)`) can be estimated from multi-component data (:math:`p(x, t)` and :math:`v_z(x, t)`) by computing the following expression [1]_: .. math:: \begin{bmatrix} \mathbf{p^+}(k_x, \omega) \\ \mathbf{p^-}(k_x, \omega) \end{bmatrix} = \frac{1}{2} \begin{bmatrix} 1 & \frac{\omega \rho}{k_z} \\ 1 & - \frac{\omega \rho}{k_z} \\ \end{bmatrix} \begin{bmatrix} \mathbf{p}(k_x, \omega) \\ \mathbf{v_z}(k_x, \omega) \end{bmatrix} if ``kind='analytical'`` or alternatively by solving the equation in :func:`ptcpy.waveeqprocessing.UpDownComposition2D` as an inverse problem, if ``kind='inverse'``. The latter approach has several advantages as data regularization can be included as part of the separation process allowing the input data to be aliased. This is obtained by solving the following problem: .. math:: \begin{bmatrix} \mathbf{p} \\ s*\mathbf{v_z} \end{bmatrix} = \begin{bmatrix} \mathbf{R}\mathbf{F} & 0 \\ 0 & s*\mathbf{R}\mathbf{F} \end{bmatrix} \mathbf{W} \begin{bmatrix} \mathbf{F}^H \mathbf{S} & 0 \\ 0 & \mathbf{F}^H \mathbf{S} \end{bmatrix} \mathbf{p^{\pm}} where :math:`\mathbf{R}` is a :class:`ptcpy.basicoperators.Restriction` operator and :math:`\mathbf{S}` is sparsyfing transform operator (e.g., :class:`ptcpy.signalprocessing.Radon2D`). .. [1] Wapenaar, K. "Reciprocity properties of one-way propagators", Geophysics, vol. 63, pp. 1795-1798. 1998. """ ndims = p.ndim if ndims == 2: decomposition = _UpDownDecomposition2D_analytical composition = UpDownComposition2D if kind == 'analytical': FFTop, OBL = \ decomposition(nt, nr, dt, dr, rho, vel, nffts=nffts, critical=critical, ntaper=ntaper, dtype=dtype) VZ = FFTop * vz.ravel() VZ = VZ.reshape(nffts) # scaled Vz VZ_obl = OBL * VZ vz_obl = FFTop.H * VZ_obl.ravel() vz_obl = np.real(vz_obl.reshape(nr, nt)) # separation pup = (p - vz_obl) / 2 pdown = (p + vz_obl) / 2 elif kind == 'inverse': d = np.concatenate((p.ravel(), scaling * vz.ravel())) UDop = \ composition(nt, nr, dt, dr, rho, vel, nffts=nffts, critical=critical, ntaper=ntaper, scaling=scaling, dtype=dtype) if restriction is not None: UDop = restriction * UDop if sptransf is not None: UDop = UDop * BlockDiag([sptransf, sptransf]) UDop.dtype = np.real(np.ones(1, UDop.dtype)).dtype if dottest: Dottest(UDop, UDop.shape[0], UDop.shape[1], complexflag=2, verb=True) # separation by inversion dud = solver(UDop, d.ravel(), **kwargs_solver)[0] if sptransf is None: dud = np.real(dud) else: dud = BlockDiag([sptransf, sptransf]) * np.real(dud) dud = dud.reshape(2 * nr, nt) pdown, pup = dud[:nr], dud[nr:] else: raise KeyError('kind must be analytical or inverse') return pup, pdown
def UpDownComposition3D(nt, nr, dt, dr, rho, vel, nffts=(None, None, None), critical=100., ntaper=10, scaling=1., dtype='complex128'): r"""3D Up-down wavefield composition. Apply multi-component seismic wavefield composition from its up- and down-going constituents. The input model required by the operator should be created by flattening the separated wavefields of size :math:`\lbrack n_{r_y} \times n_{r_x} \times n_t \rbrack` concatenated along the first spatial axis. Similarly, the data is also a flattened concatenation of pressure and vertical particle velocity wavefields. Parameters ---------- nt : :obj:`int` Number of samples along the time axis nr : :obj:`tuple` Number of samples along the receiver axes dt : :obj:`float` Sampling along the time axis dr : :obj:`tuple` Samplings along the receiver array rho : :obj:`float` Density along the receiver array (must be constant) vel : :obj:`float` Velocity along the receiver array (must be constant) nffts : :obj:`tuple`, optional Number of samples along the wavenumbers and frequency axes (for the wavenumbers axes the same order as ``nr`` and ``dr`` must be followed) critical : :obj:`float`, optional Percentage of angles to retain in obliquity factor. For example, if ``critical=100`` only angles below the critical angle :math:`\sqrt{k_y^2 + k_x^2} < \frac{\omega}{vel}` will be retained ntaper : :obj:`float`, optional Number of samples of taper applied to obliquity factor around critical angle scaling : :obj:`float`, optional Scaling to apply to the operator (see Notes for more details) dtype : :obj:`str`, optional Type of elements in input array. Returns ------- UDop : :obj:`pylops.LinearOperator` Up-down wavefield composition operator See Also -------- UpDownComposition2D: 2D Wavefield composition WavefieldDecomposition: Wavefield decomposition Notes ----- Multi-component seismic data (:math:`p(y, x, t)` and :math:`v_z(y, x, t)`) can be synthesized in the frequency-wavenumber domain as the superposition of the up- and downgoing constituents of the pressure wavefield (:math:`p^-(y, x, t)` and :math:`p^+(y, x, t)`) as described :class:`pylops.waveeqprocessing.UpDownComposition2D`. Here the vertical wavenumber :math:`k_z` is defined as :math:`k_z=\sqrt{\omega^2/c^2 - k_y^2 - k_x^2}`. """ nffts = (int(nffts[0]) if nffts[0] is not None else nr[0], int(nffts[1]) if nffts[1] is not None else nr[1], int(nffts[2]) if nffts[2] is not None else nt) # create obliquity operator FFTop, OBLop = \ _obliquity3D(nt, nr, dt, dr, rho, vel, nffts=nffts, critical=critical, ntaper=ntaper, composition=True, dtype=dtype) # create up-down modelling operator UDop = (BlockDiag([FFTop.H, scaling * FFTop.H]) * \ Block([[Identity(nffts[0] * nffts[1] * nffts[2], dtype=dtype), Identity(nffts[0] * nffts[1] * nffts[2], dtype=dtype)], [OBLop, -OBLop]]) * \ BlockDiag([FFTop, FFTop])) return UDop
def Segment(y, cl, sigma, alpha, clsigmas=None, z=None, niter=10, x0=None, callback=None, show=False, kwargs_simplex=None): r"""Primal-dual algorithm for image segmentation Perform image segmentation over :math:`N_{cl}` classes using the general version of the first-order primal-dual algorithm [1]_. Parameters ---------- y : :obj:`np.ndarray` Image to segment (must have 2 or more dimensions) cl : :obj:`numpy.ndarray` Classes sigma : :obj:`float` Positive scalar weight of the misfit term alpha : :obj:`float` Positive scalar weight of the regularization term clsigmas : :obj:`numpy.ndarray`, optional Classes standard deviations z : :obj:`numpy.ndarray`, optional Additional vector niter : :obj:`int`, optional Number of iterations of iterative scheme x0 : :obj:`numpy.ndarray`, optional Initial vector callback : :obj:`callable`, optional Function with signature (``callback(x)``) to call after each iteration where ``x`` is the current model vector show : :obj:`bool`, optional Display iterations log kwargs_simplex : :obj:`dict`, optional Arbitrary keyword arguments for :py:func:`pyproximal.Simplex` operator Returns ------- x : :obj:`numpy.ndarray` Classes probabilities. This is a vector of size :math:`N_{dim} \times N_{cl}` whose columns contain the probability for each pixel to be in the class :math:`c_i` cl : :obj:`numpy.ndarray` Estimated classes. This is a vector of the same size of the input data ``y`` with the selected classes at each pixel. Notes ----- This solver performs image segmentation over :math:`N_{cl}` classes solving the following nonlinear minimization problem using the general version of the first-order primal-dual algorithm of [1]_: .. math:: \min_{\mathbf{x} \in X} \frac{\sigma}{2} \mathbf{x}^T \mathbf{f} + \mathbf{x}^T \mathbf{z} + \frac{\alpha}{2}||\nabla \mathbf{x}||_{2,1} where :math:`X=\{ \mathbf{x}: \sum_{i=1}^{N_{cl}} x_i = 1,\; x_i \geq 0 \}` is a simplex and :math:`\mathbf{f}=[\mathbf{f}_1, ..., \mathbf{f}_{N_{cl}}]^T` with :math:`\mathbf{f}_i = |\mathbf{y}-c_i|^2/\sigma_i`. Here :math:`\mathbf{c}=[c_1, ..., c_{N_{cl}}]^T` and :math:`\mathbf{\sigma}=[\sigma_1, ..., \sigma_{N_{cl}}]^T` are vectors representing the optimal mean and standard deviations for each class. .. [1] Chambolle, and A., Pock, "A first-order primal-dual algorithm for convex problems with applications to imaging", Journal of Mathematical Imaging and Vision, 40, 8pp. 120–145. 2011. """ kwargs_simplex = {} if kwargs_simplex is None else kwargs_simplex dims = y.shape ndims = len(dims) dimsprod = np.prod(np.array(dims)) ncl = len(cl) # Data (difference between image and center of classes) g = sigma / 2. * (y.reshape(1, dimsprod) - cl[:, np.newaxis])**2 if clsigmas is not None: g /= clsigmas[:, np.newaxis] g = g.ravel() # Gradient operator sampling = 1. Gop = Gradient(dims=dims, sampling=sampling, edge=False, kind='forward', dtype='float64') Gop = BlockDiag([Gop] * ncl) # Simplex and L1 proximal operators simp = Simplex(dimsprod * ncl, radius=1, dims=(ncl, dimsprod), axis=0, **kwargs_simplex) #l1 = L1(sigma=0.5 * alpha) l21 = VStack([L21(ndim=ndims, sigma=0.5 * alpha)] * ncl, nn=[ndims * dimsprod] * ncl) # Steps L = 8. / sampling**2 tau = 1. mu = 1. / (tau * L) # Inversion x = PrimalDual(simp, l21, Gop, tau=tau, mu=mu, z=g if z is None else g + z, theta=1., x0=np.zeros_like(g) if x0 is None else x0, niter=niter, callback=callback, show=show) x = x.reshape(ncl, dimsprod).T cl = np.argmax(x, axis=1) cl = cl.reshape(dims) return x, cl
def apply_multiplepoints(self, trav, G0=None, nfft=None, rtm=False, greens=False, dottest=False, usematmul=False, **kwargs_solver): r"""Marchenko redatuming for multiple points Solve the Marchenko redatuming inverse problem for multiple points given their direct arrival traveltime curves (``trav``) and waveforms (``G0``). Parameters ---------- trav : :obj:`numpy.ndarray` Traveltime of first arrival from subsurface points to surface receivers of size :math:`[n_r \times n_{vs}]` G0 : :obj:`numpy.ndarray`, optional Direct arrival in time domain of size :math:`[n_r \times n_{vs} \times n_t]` (if None, create arrival using ``trav``) nfft : :obj:`int`, optional Number of samples in fft when creating the analytical direct wave rtm : :obj:`bool`, optional Compute and return rtm redatuming greens : :obj:`bool`, optional Compute and return Green's functions dottest : :obj:`bool`, optional Apply dot-test usematmul : :obj:`bool`, optional Use :func:`numpy.matmul` (``True``) or for-loop with :func:`numpy.dot` (``False``) in :py:class:`pylops.signalprocessing.Fredholm1` operator. Refer to Fredholm1 documentation for details. **kwargs_solver Arbitrary keyword arguments for chosen solver (:py:func:`scipy.sparse.linalg.lsqr` and :py:func:`pylops.optimization.solver.cgls` are used as default for numpy and cupy `data`, respectively) Returns ---------- f1_inv_minus : :obj:`numpy.ndarray` Inverted upgoing focusing function of size :math:`[n_r \times n_{vs} \times n_t]` f1_inv_plus : :obj:`numpy.ndarray` Inverted downgoing focusing functionof size :math:`[n_r \times n_{vs} \times n_t]` p0_minus : :obj:`numpy.ndarray` Single-scattering standard redatuming upgoing Green's function of size :math:`[n_r \times n_{vs} \times n_t]` g_inv_minus : :obj:`numpy.ndarray` Inverted upgoing Green's function of size :math:`[n_r \times n_{vs} \times n_t]` g_inv_plus : :obj:`numpy.ndarray` Inverted downgoing Green's function of size :math:`[n_r \times n_{vs} \times n_t]` """ nvs = trav.shape[1] # Create window trav_off = trav - self.toff trav_off = np.round(trav_off / self.dt).astype(int) w = np.zeros((self.nr, nvs, self.nt), dtype=self.dtype) for ir in range(self.nr): for ivs in range(nvs): w[ir, ivs, :trav_off[ir, ivs]] = 1 w = np.concatenate((np.flip(w, axis=-1), w[:, :, 1:]), axis=-1) if self.nsmooth > 0: smooth = np.ones(self.nsmooth, dtype=self.dtype) / self.nsmooth w = filtfilt(smooth, 1, w) w = to_cupy_conditional(self.Rtwosided_fft, w) # Create operators Rop = MDC( self.Rtwosided_fft, self.nt2, nv=nvs, dt=self.dt, dr=self.dr, twosided=True, conj=False, fftengine=self.fftengine, transpose=False, prescaled=self.prescaled, usematmul=usematmul, dtype=self.dtype, ) R1op = MDC( self.Rtwosided_fft, self.nt2, nv=nvs, dt=self.dt, dr=self.dr, twosided=True, conj=True, fftengine=self.fftengine, transpose=False, prescaled=self.prescaled, usematmul=usematmul, dtype=self.dtype, ) Rollop = Roll( self.ns * nvs * self.nt2, dims=(self.nt2, self.ns, nvs), dir=0, shift=-1, dtype=self.dtype, ) Wop = Diagonal(w.transpose(2, 0, 1).ravel()) Iop = Identity(self.nr * nvs * self.nt2) Mop = Block([[Iop, -1 * Wop * Rop], [-1 * Wop * Rollop * R1op, Iop] ]) * BlockDiag([Wop, Wop]) Gop = Block([[Iop, -1 * Rop], [-1 * Rollop * R1op, Iop]]) if dottest: Dottest( Gop, 2 * self.nr * nvs * self.nt2, 2 * self.nr * nvs * self.nt2, raiseerror=True, verb=True, backend=get_module_name(self.ncp), ) if dottest: Dottest( Mop, 2 * self.ns * nvs * self.nt2, 2 * self.nr * nvs * self.nt2, raiseerror=True, verb=True, backend=get_module_name(self.ncp), ) # Create input focusing function if G0 is None: if self.wav is not None and nfft is not None: G0 = np.zeros((self.nr, nvs, self.nt), dtype=self.dtype) for ivs in range(nvs): G0[:, ivs] = (directwave( self.wav, trav[:, ivs], self.nt, self.dt, nfft=nfft, derivative=True, )).T G0 = to_cupy_conditional(self.Rtwosided_fft, G0) else: logging.error("wav and/or nfft are not provided. " "Provide either G0 or wav and nfft...") raise ValueError("wav and/or nfft are not provided. " "Provide either G0 or wav and nfft...") fd_plus = np.concatenate(( np.flip(G0, axis=-1).transpose(2, 0, 1), self.ncp.zeros((self.nt - 1, self.nr, nvs), dtype=self.dtype), )) # Run standard redatuming as benchmark if rtm: p0_minus = Rop * fd_plus.ravel() p0_minus = p0_minus.reshape(self.nt2, self.ns, nvs).transpose(1, 2, 0) # Create data and inverse focusing functions d = Wop * Rop * fd_plus.ravel() d = np.concatenate(( d.reshape(self.nt2, self.ns, nvs), self.ncp.zeros((self.nt2, self.ns, nvs), dtype=self.dtype), )) # Invert for focusing functions if self.ncp == np: f1_inv = lsqr(Mop, d.ravel(), **kwargs_solver)[0] else: f1_inv = cgls(Mop, d.ravel(), x0=self.ncp.zeros(2 * (2 * self.nt - 1) * self.nr * nvs, dtype=self.dtype), **kwargs_solver)[0] f1_inv = f1_inv.reshape(2 * self.nt2, self.nr, nvs) f1_inv_tot = f1_inv + np.concatenate((self.ncp.zeros( (self.nt2, self.nr, nvs), dtype=self.dtype), fd_plus)) f1_inv_minus = f1_inv_tot[:self.nt2].transpose(1, 2, 0) f1_inv_plus = f1_inv_tot[self.nt2:].transpose(1, 2, 0) if greens: # Create Green's functions g_inv = Gop * f1_inv_tot.ravel() g_inv = g_inv.reshape(2 * self.nt2, self.ns, nvs) g_inv_minus = -g_inv[:self.nt2].transpose(1, 2, 0) g_inv_plus = np.flip(g_inv[self.nt2:], axis=0).transpose(1, 2, 0) if rtm and greens: return f1_inv_minus, f1_inv_plus, p0_minus, g_inv_minus, g_inv_plus elif rtm: return f1_inv_minus, f1_inv_plus, p0_minus elif greens: return f1_inv_minus, f1_inv_plus, g_inv_minus, g_inv_plus else: return f1_inv_minus, f1_inv_plus
def apply_multiplepoints(self, trav, G0=None, nfft=None, rtm=False, greens=False, dottest=False, **kwargs_lsqr): r"""Marchenko redatuming for multiple points Solve the Marchenko redatuming inverse problem for multiple points given their direct arrival traveltime curves (``trav``) and waveforms (``G0``). Parameters ---------- trav : :obj:`numpy.ndarray` Traveltime of first arrival from subsurface points to surface receivers of size :math:`[n_r \times n_{vs}]` G0 : :obj:`numpy.ndarray`, optional Direct arrival in time domain of size :math:`[n_r \times n_{vs} \times n_t]` (if None, create arrival using ``trav``) nfft : :obj:`int`, optional Number of samples in fft when creating the analytical direct wave rtm : :obj:`bool`, optional Compute and return rtm redatuming greens : :obj:`bool`, optional Compute and return Green's functions dottest : :obj:`bool`, optional Apply dot-test **kwargs_lsqr Arbitrary keyword arguments for :py:func:`scipy.sparse.linalg.lsqr` solver Returns ---------- f1_inv_minus : :obj:`numpy.ndarray` Inverted upgoing focusing function of size :math:`[n_r \times n_{vs} \times n_t]` f1_inv_plus : :obj:`numpy.ndarray` Inverted downgoing focusing functionof size :math:`[n_r \times n_{vs} \times n_t]` p0_minus : :obj:`numpy.ndarray` Single-scattering standard redatuming upgoing Green's function of size :math:`[n_r \times n_{vs} \times n_t]` g_inv_minus : :obj:`numpy.ndarray` Inverted upgoing Green's function of size :math:`[n_r \times n_{vs} \times n_t]` g_inv_plus : :obj:`numpy.ndarray` Inverted downgoing Green's function of size :math:`[n_r \times n_{vs} \times n_t]` """ nvs = trav.shape[1] # Create window trav_off = trav - self.toff trav_off = np.round(trav_off / self.dt).astype(np.int) w = np.zeros((self.nr, nvs, self.nt)) for ir in range(self.nr): for ivs in range(nvs): w[ir, ivs, :trav_off[ir, ivs]] = 1 w = np.concatenate((np.flip(w, axis=-1), w[:, :, 1:]), axis=-1) if self.nsmooth > 0: smooth = np.ones(self.nsmooth) / self.nsmooth w = filtfilt(smooth, 1, w) # Create operators Rop = MDC(self.Rtwosided_fft, self.nt2, nv=nvs, dt=self.dt, dr=self.dr, twosided=True, conj=False, transpose=False, prescaled=self.prescaled, dtype=self.dtype) R1op = MDC(self.Rtwosided_fft, self.nt2, nv=nvs, dt=self.dt, dr=self.dr, twosided=True, conj=True, transpose=False, prescaled=self.prescaled, dtype=self.dtype) Rollop = Roll(self.ns * nvs * self.nt2, dims=(self.nt2, self.ns, nvs), dir=0, shift=-1, dtype=self.dtype) Wop = Diagonal(w.transpose(2, 0, 1).flatten()) Iop = Identity(self.nr * nvs * self.nt2) Mop = Block([[Iop, -1 * Wop * Rop], [-1 * Wop * Rollop * R1op, Iop] ]) * BlockDiag([Wop, Wop]) Gop = Block([[Iop, -1 * Rop], [-1 * Rollop * R1op, Iop]]) if dottest: Dottest(Gop, 2 * self.nr * nvs * self.nt2, 2 * self.nr * nvs * self.nt2, raiseerror=True, verb=True) if dottest: Dottest(Mop, 2 * self.ns * nvs * self.nt2, 2 * self.nr * nvs * self.nt2, raiseerror=True, verb=True) # Create input focusing function if G0 is None: if self.wav is not None and nfft is not None: G0 = np.zeros((self.nr, nvs, self.nt)) for ivs in range(nvs): G0[:, ivs] = (directwave(self.wav, trav[:, ivs], self.nt, self.dt, nfft=nfft, derivative=True)).T else: logging.error('wav and/or nfft are not provided. ' 'Provide either G0 or wav and nfft...') raise ValueError('wav and/or nfft are not provided. ' 'Provide either G0 or wav and nfft...') fd_plus = np.concatenate((np.flip(G0, axis=-1).transpose(2, 0, 1), np.zeros((self.nt - 1, self.nr, nvs)))) # Run standard redatuming as benchmark if rtm: p0_minus = Rop * fd_plus.flatten() p0_minus = p0_minus.reshape(self.nt2, self.ns, nvs).transpose(1, 2, 0) # Create data and inverse focusing functions d = Wop * Rop * fd_plus.flatten() d = np.concatenate((d.reshape(self.nt2, self.ns, nvs), np.zeros( (self.nt2, self.ns, nvs)))) # Invert for focusing functions f1_inv = lsqr(Mop, d.flatten(), **kwargs_lsqr)[0] f1_inv = f1_inv.reshape(2 * self.nt2, self.nr, nvs) f1_inv_tot = \ f1_inv + np.concatenate((np.zeros((self.nt2, self.nr, nvs)), fd_plus)) f1_inv_minus = f1_inv_tot[:self.nt2].transpose(1, 2, 0) f1_inv_plus = f1_inv_tot[self.nt2:].transpose(1, 2, 0) if greens: # Create Green's functions g_inv = Gop * f1_inv_tot.flatten() g_inv = np.real(g_inv) # cast to real as Gop is a complex operator g_inv = g_inv.reshape(2 * self.nt2, self.ns, nvs) g_inv_minus = -g_inv[:self.nt2].transpose(1, 2, 0) g_inv_plus = np.flip(g_inv[self.nt2:], axis=0).transpose(1, 2, 0) if rtm and greens: return f1_inv_minus, f1_inv_plus, p0_minus, g_inv_minus, g_inv_plus elif rtm: return f1_inv_minus, f1_inv_plus, p0_minus elif greens: return f1_inv_minus, f1_inv_plus, g_inv_minus, g_inv_plus else: return f1_inv_minus, f1_inv_plus
def apply_onepoint(self, trav, G0=None, nfft=None, rtm=False, greens=False, dottest=False, fast=True, **kwargs_lsqr): r"""Marchenko redatuming for one point Solve the Marchenko redatuming inverse problem for a single point given its direct arrival traveltime curve (``trav``) and waveform (``G0``). Parameters ---------- trav : :obj:`numpy.ndarray` Traveltime of first arrival from subsurface point to surface receivers of size :math:`[n_r \times 1]` G0 : :obj:`numpy.ndarray`, optional Direct arrival in time domain of size :math:`[n_r \times n_t]` (if None, create arrival using ``trav``) nfft : :obj:`int`, optional Number of samples in fft when creating the analytical direct wave rtm : :obj:`bool`, optional Compute and return rtm redatuming greens : :obj:`bool`, optional Compute and return Green's functions dottest : :obj:`bool`, optional Apply dot-test fast : :obj:`bool` Fast application of MDC when model has only one virtual source (``True``) or not (``False``) **kwargs_lsqr Arbitrary keyword arguments for :py:func:`scipy.sparse.linalg.lsqr` solver Returns ---------- f1_inv_minus : :obj:`numpy.ndarray` Inverted upgoing focusing function of size :math:`[n_r \times n_t]` f1_inv_plus : :obj:`numpy.ndarray` Inverted downgoing focusing function of size :math:`[n_r \times n_t]` p0_minus : :obj:`numpy.ndarray` Single-scattering standard redatuming upgoing Green's function of size :math:`[n_r \times n_t]` g_inv_minus : :obj:`numpy.ndarray` Inverted upgoing Green's function of size :math:`[n_r \times n_t]` g_inv_plus : :obj:`numpy.ndarray` Inverted downgoing Green's function of size :math:`[n_r \times n_t]` """ # Create window trav_off = trav - self.toff trav_off = np.round(trav_off / self.dt).astype(np.int) # window w = np.zeros((self.nr, self.nt)) for ir in range(self.nr): w[ir, :trav_off[ir]] = 1 w = np.hstack((np.fliplr(w), w[:, 1:])) if self.nsmooth > 0: smooth = np.ones(self.nsmooth) / self.nsmooth w = filtfilt(smooth, 1, w) # Create operators Rop = MDC(self.Rtwosided_fft, self.nt2, nv=1, dt=self.dt, dr=self.dr, twosided=True, fast=fast, dtype=self.dtype) R1op = MDC(self.R1twosided_fft, self.nt2, nv=1, dt=self.dt, dr=self.dr, twosided=True, fast=fast, dtype=self.dtype) Wop = Diagonal(w.flatten()) Iop = Identity(self.nr * (2 * self.nt - 1)) Mop = VStack( [HStack([Iop, -1 * Wop * Rop]), HStack([-1 * Wop * R1op, Iop])]) * BlockDiag([Wop, Wop]) Gop = VStack([HStack([Iop, -1 * Rop]), HStack([-1 * R1op, Iop])]) if dottest: assert Dottest(Gop, 2 * self.nr * self.nt2, 2 * self.nr * self.nt2, verb=True) if dottest: assert Dottest(Mop, 2 * self.nr * self.nt2, 2 * self.nr * self.nt2, verb=True) # Create input focusing function if G0 is None: if self.wav is not None and nfft is not None: G0 = (directwave(self.wav, trav, self.nt, self.dt, nfft=nfft)).T else: logging.error('wav and/or nfft are not provided. ' 'Provide either G0 or wav and nfft...') raise ValueError('wav and/or nfft are not provided. ' 'Provide either G0 or wav and nfft...') fd_plus = np.concatenate( (np.fliplr(G0), np.zeros((self.nr, self.nt - 1))), axis=-1) # Run standard redatuming as benchmark if rtm: p0_minus = Rop * fd_plus.flatten() p0_minus = p0_minus.reshape(self.nr, self.nt2) # Create data and inverse focusing functions d = Wop * Rop * fd_plus.flatten() d = np.concatenate( (d.reshape(self.nr, self.nt2), np.zeros((self.nr, self.nt2)))) f1_inv = lsqr(Mop, d.flatten(), **kwargs_lsqr)[0] f1_inv = f1_inv.reshape(2 * self.nr, self.nt2) f1_inv_tot = f1_inv + np.concatenate((np.zeros( (self.nr, self.nt2)), fd_plus)) f1_inv_minus, f1_inv_plus = f1_inv_tot[:self.nr], f1_inv_tot[self.nr:] if greens: # Create Green's functions g_inv = Gop * f1_inv_tot.flatten() g_inv = g_inv.reshape(2 * self.nr, (2 * self.nt - 1)) g_inv_minus, g_inv_plus = -g_inv[:self.nr], np.fliplr( g_inv[self.nr:]) if rtm and greens: return f1_inv_minus, f1_inv_plus, p0_minus, g_inv_minus, g_inv_plus elif rtm: return f1_inv_minus, f1_inv_plus, p0_minus elif greens: return f1_inv_minus, f1_inv_plus, g_inv_minus, g_inv_plus else: return f1_inv_minus, f1_inv_plus
def apply_onepoint(self, trav, G0=None, nfft=None, rtm=False, greens=False, dottest=False, fast=None, **kwargs_solver): r"""Marchenko redatuming for one point Solve the Marchenko redatuming inverse problem for a single point given its direct arrival traveltime curve (``trav``) and waveform (``G0``). Parameters ---------- trav : :obj:`numpy.ndarray` Traveltime of first arrival from subsurface point to surface receivers of size :math:`[n_r \times 1]` G0 : :obj:`numpy.ndarray`, optional Direct arrival in time domain of size :math:`[n_r \times n_t]` (if None, create arrival using ``trav``) nfft : :obj:`int`, optional Number of samples in fft when creating the analytical direct wave rtm : :obj:`bool`, optional Compute and return rtm redatuming greens : :obj:`bool`, optional Compute and return Green's functions dottest : :obj:`bool`, optional Apply dot-test fast : :obj:`bool` *Deprecated*, will be removed in v2.0.0 **kwargs_solver Arbitrary keyword arguments for chosen solver (:py:func:`scipy.sparse.linalg.lsqr` and :py:func:`pylops.optimization.solver.cgls` are used as default for numpy and cupy `data`, respectively) Returns ---------- f1_inv_minus : :obj:`numpy.ndarray` Inverted upgoing focusing function of size :math:`[n_r \times n_t]` f1_inv_plus : :obj:`numpy.ndarray` Inverted downgoing focusing function of size :math:`[n_r \times n_t]` p0_minus : :obj:`numpy.ndarray` Single-scattering standard redatuming upgoing Green's function of size :math:`[n_r \times n_t]` g_inv_minus : :obj:`numpy.ndarray` Inverted upgoing Green's function of size :math:`[n_r \times n_t]` g_inv_plus : :obj:`numpy.ndarray` Inverted downgoing Green's function of size :math:`[n_r \times n_t]` """ # Create window trav_off = trav - self.toff trav_off = np.round(trav_off / self.dt).astype(np.int) w = np.zeros((self.nr, self.nt), dtype=self.dtype) for ir in range(self.nr): w[ir, :trav_off[ir]] = 1 w = np.hstack((np.fliplr(w), w[:, 1:])) if self.nsmooth > 0: smooth = np.ones(self.nsmooth, dtype=self.dtype) / self.nsmooth w = filtfilt(smooth, 1, w) w = to_cupy_conditional(self.Rtwosided_fft, w) # Create operators Rop = MDC(self.Rtwosided_fft, self.nt2, nv=1, dt=self.dt, dr=self.dr, twosided=True, conj=False, transpose=False, saveGt=self.saveRt, prescaled=self.prescaled, dtype=self.dtype) R1op = MDC(self.Rtwosided_fft, self.nt2, nv=1, dt=self.dt, dr=self.dr, twosided=True, conj=True, transpose=False, saveGt=self.saveRt, prescaled=self.prescaled, dtype=self.dtype) Rollop = Roll(self.nt2 * self.ns, dims=(self.nt2, self.ns), dir=0, shift=-1, dtype=self.dtype) Wop = Diagonal(w.T.flatten()) Iop = Identity(self.nr * self.nt2) Mop = Block([[Iop, -1 * Wop * Rop], [-1 * Wop * Rollop * R1op, Iop] ]) * BlockDiag([Wop, Wop]) Gop = Block([[Iop, -1 * Rop], [-1 * Rollop * R1op, Iop]]) if dottest: Dottest(Gop, 2 * self.ns * self.nt2, 2 * self.nr * self.nt2, raiseerror=True, verb=True, backend=get_module_name(self.ncp)) if dottest: Dottest(Mop, 2 * self.ns * self.nt2, 2 * self.nr * self.nt2, raiseerror=True, verb=True, backend=get_module_name(self.ncp)) # Create input focusing function if G0 is None: if self.wav is not None and nfft is not None: G0 = (directwave(self.wav, trav, self.nt, self.dt, nfft=nfft, derivative=True)).T G0 = to_cupy_conditional(self.Rtwosided_fft, G0) else: logging.error('wav and/or nfft are not provided. ' 'Provide either G0 or wav and nfft...') raise ValueError('wav and/or nfft are not provided. ' 'Provide either G0 or wav and nfft...') fd_plus = np.concatenate((np.fliplr(G0).T, self.ncp.zeros((self.nt - 1, self.nr), dtype=self.dtype))) # Run standard redatuming as benchmark if rtm: p0_minus = Rop * fd_plus.flatten() p0_minus = p0_minus.reshape(self.nt2, self.ns).T # Create data and inverse focusing functions d = Wop * Rop * fd_plus.flatten() d = np.concatenate((d.reshape(self.nt2, self.ns), self.ncp.zeros((self.nt2, self.ns), self.dtype))) # Invert for focusing functions if self.ncp == np: f1_inv = lsqr(Mop, d.flatten(), **kwargs_solver)[0] else: f1_inv = cgls(Mop, d.flatten(), x0=self.ncp.zeros(2 * (2 * self.nt - 1) * self.nr, dtype=self.dtype), **kwargs_solver)[0] f1_inv = f1_inv.reshape(2 * self.nt2, self.nr) f1_inv_tot = f1_inv + np.concatenate((self.ncp.zeros( (self.nt2, self.nr), dtype=self.dtype), fd_plus)) f1_inv_minus = f1_inv_tot[:self.nt2].T f1_inv_plus = f1_inv_tot[self.nt2:].T if greens: # Create Green's functions g_inv = Gop * f1_inv_tot.flatten() g_inv = g_inv.reshape(2 * self.nt2, self.ns) g_inv_minus, g_inv_plus = -g_inv[:self.nt2].T, \ np.fliplr(g_inv[self.nt2:].T) if rtm and greens: return f1_inv_minus, f1_inv_plus, p0_minus, g_inv_minus, g_inv_plus elif rtm: return f1_inv_minus, f1_inv_plus, p0_minus elif greens: return f1_inv_minus, f1_inv_plus, g_inv_minus, g_inv_plus else: return f1_inv_minus, f1_inv_plus
def WavefieldDecomposition(p, vz, nt, nr, dt, dr, rho, vel, nffts=(None, None, None), critical=100.0, ntaper=10, scaling=1.0, kind="inverse", restriction=None, sptransf=None, solver=lsqr, dottest=False, dtype="complex128", **kwargs_solver): r"""Up-down wavefield decomposition. Apply seismic wavefield decomposition from multi-component (pressure and vertical particle velocity) data. This process is also generally referred to as data-based deghosting. Parameters ---------- p : :obj:`np.ndarray` Pressure data of size :math:`\lbrack n_{r_x} \,(\times n_{r_y}) \times n_t \rbrack` (or :math:`\lbrack n_{r_{x,\text{sub}}} \,(\times n_{r_{y,\text{sub}}}) \times n_t \rbrack` in case a ``restriction`` operator is provided. Note that :math:`n_{r_{x,\text{sub}}}` (and :math:`n_{r_{y,\text{sub}}}`) must agree with the size of the output of this operator.) vz : :obj:`np.ndarray` Vertical particle velocity data of same size as pressure data nt : :obj:`int` Number of samples along the time axis nr : :obj:`int` or :obj:`tuple` Number of samples along the receiver axis (or axes) dt : :obj:`float` Sampling along the time axis dr : :obj:`float` or :obj:`tuple` Sampling along the receiver array (or axes) rho : :obj:`float` Density :math:`\rho` along the receiver array (must be constant) vel : :obj:`float` Velocity :math:`c` along the receiver array (must be constant) nffts : :obj:`tuple`, optional Number of samples along the wavenumber and frequency axes critical : :obj:`float`, optional Percentage of angles to retain in obliquity factor. For example, if ``critical=100`` only angles below the critical angle :math:`\frac{f(k_x)}{c}` will be retained ntaper : :obj:`float`, optional Number of samples of taper applied to obliquity factor around critical angle kind : :obj:`str`, optional Type of separation: ``inverse`` (default) or ``analytical`` scaling : :obj:`float`, optional Scaling to apply to the operator (see Notes of :func:`pylops.waveeqprocessing.wavedecomposition.UpDownComposition2D` for more details) restriction : :obj:`pylops.LinearOperator`, optional Restriction operator sptransf : :obj:`pylops.LinearOperator`, optional Sparsifying operator solver : :obj:`float`, optional Function handle of solver to be used if ``kind='inverse'`` dottest : :obj:`bool`, optional Apply dot-test dtype : :obj:`str`, optional Type of elements in input array. **kwargs_solver Arbitrary keyword arguments for chosen ``solver`` Returns ------- pup : :obj:`np.ndarray` Up-going wavefield pdown : :obj:`np.ndarray` Down-going wavefield Raises ------ KeyError If ``kind`` is neither ``analytical`` nor ``inverse`` Notes ----- Up- and down-going components of seismic data :math:`p^-(x, t)` and :math:`p^+(x, t)` can be estimated from multi-component data :math:`p(x, t)` and :math:`v_z(x, t)` by computing the following expression [1]_: .. math:: \begin{bmatrix} \hat{p}^+ \\ \hat{p}^- \end{bmatrix}(k_x, \omega) = \frac{1}{2} \begin{bmatrix} 1 & \frac{\omega \rho}{k_z} \\ 1 & - \frac{\omega \rho}{k_z} \\ \end{bmatrix} \begin{bmatrix} \hat{p} \\ \hat{v}_z \end{bmatrix}(k_x, \omega) if ``kind='analytical'`` or alternatively by solving the equation in :func:`ptcpy.waveeqprocessing.UpDownComposition2D` as an inverse problem, if ``kind='inverse'``. The latter approach has several advantages as data regularization can be included as part of the separation process allowing the input data to be aliased. This is obtained by solving the following problem: .. math:: \begin{bmatrix} \mathbf{p} \\ s\mathbf{v_z} \end{bmatrix} = \begin{bmatrix} \mathbf{R}\mathbf{F} & 0 \\ 0 & s\mathbf{R}\mathbf{F} \end{bmatrix} \mathbf{W} \begin{bmatrix} \mathbf{F}^H \mathbf{S} & 0 \\ 0 & \mathbf{F}^H \mathbf{S} \end{bmatrix} \mathbf{p^{\pm}} where :math:`\mathbf{R}` is a :class:`ptcpy.basicoperators.Restriction` operator and :math:`\mathbf{S}` is sparsyfing transform operator (e.g., :class:`ptcpy.signalprocessing.Radon2D`). .. [1] Wapenaar, K. "Reciprocity properties of one-way propagators", Geophysics, vol. 63, pp. 1795-1798. 1998. """ ncp = get_array_module(p) backend = get_module_name(ncp) ndims = p.ndim if ndims == 2: dims = (nr, nt) dims2 = (2 * nr, nt) nr2 = nr decomposition = _obliquity2D composition = UpDownComposition2D else: dims = (nr[0], nr[1], nt) dims2 = (2 * nr[0], nr[1], nt) nr2 = nr[0] decomposition = _obliquity3D composition = UpDownComposition3D if kind == "analytical": FFTop, OBLop = decomposition( nt, nr, dt, dr, rho, vel, nffts=nffts, critical=critical, ntaper=ntaper, composition=False, backend=backend, dtype=dtype, ) VZ = FFTop * vz.ravel() # scaled Vz VZ_obl = OBLop * VZ vz_obl = FFTop.H * VZ_obl vz_obl = ncp.real(vz_obl.reshape(dims)) # separation pup = (p - vz_obl) / 2 pdown = (p + vz_obl) / 2 elif kind == "inverse": d = ncp.concatenate((p.ravel(), scaling * vz.ravel())) UDop = composition( nt, nr, dt, dr, rho, vel, nffts=nffts, critical=critical, ntaper=ntaper, scaling=scaling, backend=backend, dtype=dtype, ) if restriction is not None: UDop = restriction * UDop if sptransf is not None: UDop = UDop * BlockDiag([sptransf, sptransf]) UDop.dtype = ncp.real(ncp.ones(1, UDop.dtype)).dtype if dottest: Dottest( UDop, UDop.shape[0], UDop.shape[1], complexflag=2, backend=backend, verb=True, ) # separation by inversion dud = solver(UDop, d.ravel(), **kwargs_solver)[0] if sptransf is None: dud = ncp.real(dud) else: dud = BlockDiag([sptransf, sptransf]) * ncp.real(dud) dud = dud.reshape(dims2) pdown, pup = dud[:nr2], dud[nr2:] else: raise KeyError("kind must be analytical or inverse") return pup, pdown
def UpDownComposition2D( nt, nr, dt, dr, rho, vel, nffts=(None, None), critical=100.0, ntaper=10, scaling=1.0, backend="numpy", dtype="complex128", ): r"""2D Up-down wavefield composition. Apply multi-component seismic wavefield composition from its up- and down-going constituents. The input model required by the operator should be created by flattening the separated wavefields of size :math:`\lbrack n_r \times n_t \rbrack` concatenated along the spatial axis. Similarly, the data is also a flattened concatenation of pressure and vertical particle velocity wavefields. Parameters ---------- nt : :obj:`int` Number of samples along the time axis nr : :obj:`int` Number of samples along the receiver axis dt : :obj:`float` Sampling along the time axis dr : :obj:`float` Sampling along the receiver array rho : :obj:`float` Density :math:`\rho` along the receiver array (must be constant) vel : :obj:`float` Velocity :math:`c` along the receiver array (must be constant) nffts : :obj:`tuple`, optional Number of samples along the wavenumber and frequency axes critical : :obj:`float`, optional Percentage of angles to retain in obliquity factor. For example, if ``critical=100`` only angles below the critical angle :math:`|k_x| < \frac{f(k_x)}{c}` will be retained will be retained ntaper : :obj:`float`, optional Number of samples of taper applied to obliquity factor around critical angle scaling : :obj:`float`, optional Scaling to apply to the operator (see Notes for more details) backend : :obj:`str`, optional Backend used for creation of obliquity factor operator (``numpy`` or ``cupy``) dtype : :obj:`str`, optional Type of elements in input array. Returns ------- UDop : :obj:`pylops.LinearOperator` Up-down wavefield composition operator See Also -------- UpDownComposition3D: 3D Wavefield composition WavefieldDecomposition: Wavefield decomposition Notes ----- Multi-component seismic data :math:`p(x, t)` and :math:`v_z(x, t)` can be synthesized in the frequency-wavenumber domain as the superposition of the up- and downgoing constituents of the pressure wavefield (:math:`p^-(x, t)` and :math:`p^+(x, t)`) as follows [1]_: .. math:: \begin{bmatrix} \hat{p} \\ \hat{v_z} \end{bmatrix}(k_x, \omega) = \begin{bmatrix} 1 & 1 \\ \frac{k_z}{\omega \rho} & - \frac{k_z}{\omega \rho} \\ \end{bmatrix} \begin{bmatrix} \hat{p^+} \\ \hat{p^-} \end{bmatrix}(k_x, \omega) where the vertical wavenumber :math:`k_z` is defined as :math:`k_z=\sqrt{\frac{\omega^2}{c^2} - k_x^2}`. We can write the entire composition process in a compact matrix-vector notation as follows: .. math:: \begin{bmatrix} \mathbf{p} \\ s\mathbf{v_z} \end{bmatrix} = \begin{bmatrix} \mathbf{F} & 0 \\ 0 & s\mathbf{F} \end{bmatrix} \begin{bmatrix} \mathbf{I} & \mathbf{I} \\ \mathbf{W}^+ & \mathbf{W}^- \end{bmatrix} \begin{bmatrix} \mathbf{F}^H & 0 \\ 0 & \mathbf{F}^H \end{bmatrix} \mathbf{p^{\pm}} where :math:`\mathbf{F}` is the 2-dimensional FFT (:class:`pylops.signalprocessing.FFT2`), :math:`\mathbf{W}^\pm` are weighting matrices which contain the scalings :math:`\pm \frac{k_z}{\omega \rho}` implemented via :class:`pylops.basicprocessing.Diagonal`, and :math:`s` is a scaling factor that is applied to both the particle velocity data and to the operator has shown above. Such a scaling is required to balance out the different dynamic range of pressure and particle velocity when solving the wavefield separation problem as an inverse problem. As the operator is effectively obtained by chaining basic PyLops operators the adjoint is automatically implemented for this operator. .. [1] Wapenaar, K. "Reciprocity properties of one-way propagators", Geophysics, vol. 63, pp. 1795-1798. 1998. """ nffts = ( int(nffts[0]) if nffts[0] is not None else nr, int(nffts[1]) if nffts[1] is not None else nt, ) # create obliquity operator FFTop, OBLop, = _obliquity2D( nt, nr, dt, dr, rho, vel, nffts=nffts, critical=critical, ntaper=ntaper, composition=True, backend=backend, dtype=dtype, ) # create up-down modelling operator UDop = (BlockDiag([FFTop.H, scaling * FFTop.H]) * Block([ [ Identity(nffts[0] * nffts[1], dtype=dtype), Identity(nffts[0] * nffts[1], dtype=dtype), ], [OBLop, -OBLop], ]) * BlockDiag([FFTop, FFTop])) return UDop
def solve(self, f: np.ndarray): self.k = 1 if len(np.shape(f)) != 2: raise ValueError( "The TGV-Algorithm only implemnted for 2D images. Please give input shaped (m, n)" ) (primal_n, primal_m) = np.shape(f) grad = Gradient(dims=(primal_n, primal_m), dtype='float64', edge=True, kind="backward") grad_v = BlockDiag( [grad, grad] ) # symmetric dxdy <-> dydx not necessary (expensive) but easy and functional p, q = 0, 0 v = v_bar = np.zeros(2 * primal_n * primal_m) u = u_bar = f.ravel() # Projections proj_p = IndicatorL2((primal_n, primal_m), upper_bound=self.alpha[0]) proj_q = IndicatorL2((2 * primal_n, primal_m), upper_bound=self.alpha[1]) if self.mode == 'tv': dataterm = DatanormL2(image_size=f.shape, data=f.ravel(), prox_param=self.tau, lam=self.lam) else: dataterm = DatanormL2Bregman(image_size=f.shape, data=f.ravel(), prox_param=self.tau, lam=self.lam) dataterm.pk = self.pk dataterm.bregman_weight_alpha = self.alpha[0] sens = 100 while (self.tol < sens or self.k == 1) and (self.k <= self.max_iter): p = proj_p.prox(p + self.sigma * (grad * u_bar - v_bar)) q = proj_q.prox(q + self.sigma * (grad_v * v_bar)) #self.adjoint_div(v_bar, 1) u_old = u v_old = v u = dataterm.prox(u - self.tau * grad.H * p) u_bar = 2 * u - u_old v = v + self.tau * (p - grad_v.H * q) v_bar = 2 * v - v_old #self.update_sensivity(u, u_old, v, v_old, grad) # test if self.k % 300 == 0: u_gap = u - u_old v_gap = v - v_old #sens = 1/2*( # np.linalg.norm(u_gap - self.tau*grad.H*proj_p.prox(p+self.sigma*(grad*u_gap - v_gap)), 2)/ # np.linalg.norm(u, 2) + # np.linalg.norm(v_gap-proj_p.prox(p+self.sigma*(grad*u_gap - v_gap))-grad_v.H*proj_q.prox(q+ self.sigma*(grad_v*v_gap)), 2)/ # np.linalg.norm(v, 2)) # not best sens sens = 1 / 2 * ( np.linalg.norm(u_gap - self.tau * grad.H * v_gap, 2) / np.linalg.norm(u, 2) + np.linalg.norm(v_gap - self.sigma * (grad * u_gap), 2) / np.linalg.norm(v, 2)) print(sens) self.k += 1 return np.reshape(u, (primal_n, primal_m))