def test_VStack(par): """Dot-test and inversion for VStack operator """ np.random.seed(0) G1 = np.random.normal(0, 10, (par['ny'], par['nx'])).astype(par['dtype']) G2 = np.random.normal(0, 10, (par['ny'], par['nx'])).astype(par['dtype']) x = np.ones(par['nx']) + par['imag'] * np.ones(par['nx']) Vop = VStack([ MatrixMult(G1, dtype=par['dtype']), MatrixMult(G2, dtype=par['dtype']) ], dtype=par['dtype']) assert dottest(Vop, 2 * par['ny'], par['nx'], complexflag=0 if par['imag'] == 0 else 3) xlsqr = lsqr(Vop, Vop * x, damp=1e-20, iter_lim=300, show=0)[0] assert_array_almost_equal(x, xlsqr, decimal=4) # use numpy matrix directly in the definition of the operator V1op = VStack([G1, MatrixMult(G2, dtype=par['dtype'])], dtype=par['dtype']) assert dottest(V1op, 2 * par['ny'], par['nx'], complexflag=0 if par['imag'] == 0 else 3) # use scipy matrix directly in the definition of the operator G1 = sp_random(par['ny'], par['nx'], density=0.4).astype('float32') V2op = VStack([G1, MatrixMult(G2, dtype=par['dtype'])], dtype=par['dtype']) assert dottest(V2op, 2 * par['ny'], par['nx'], complexflag=0 if par['imag'] == 0 else 3)
def focusing_wrapper(direct,toff,g0VS,iava,Rop,R1op,Restrop,t): nr=direct.shape[0] nsava=iava.shape[0] nt=t.shape[0] dt=t[1]-t[0] # window directVS_off = direct - toff idirectVS_off = np.round(directVS_off/dt).astype(np.int) w = np.zeros((nr, nt)) wi = np.ones((nr, nt)) for ir in range(nr-1): w[ir, :idirectVS_off[ir]]=1 wi = wi - w w = np.hstack((np.fliplr(w), w[:, 1:])) wi = np.hstack((np.fliplr(wi), wi[:, 1:])) # smoothing nsmooth=10 if nsmooth>0: smooth=np.ones(nsmooth)/nsmooth w = filtfilt(smooth, 1, w) wi = filtfilt(smooth, 1, wi) # Input focusing function fd_plus = np.concatenate((np.fliplr(g0VS.T), np.zeros((nr, nt-1))), axis=-1) # operators Wop = Diagonal(w.flatten()) WSop = Diagonal(w[iava].flatten()) WiSop = Diagonal(wi[iava].flatten()) Mop = VStack([HStack([Restrop, -1*WSop*Rop]), HStack([-1*WSop*R1op, Restrop])])*BlockDiag([Wop, Wop]) Gop = VStack([HStack([Restrop, -1*Rop]), HStack([-1*R1op, Restrop])]) p0_minus = Rop*fd_plus.flatten() d = WSop*p0_minus p0_minus = p0_minus.reshape(nsava, 2*nt-1) d = np.concatenate((d.reshape(nsava, 2*nt-1), np.zeros((nsava, 2*nt-1)))) # solve f1 = lsqr(Mop, d.flatten(), iter_lim=10, show=False)[0] f1 = f1.reshape(2*nr, (2*nt-1)) f1_tot = f1 + np.concatenate((np.zeros((nr, 2*nt-1)), fd_plus)) g = BlockDiag([WiSop,WiSop])*Gop*f1_tot.flatten() g = g.reshape(2*nsava, (2*nt-1)) f1_minus, f1_plus = f1_tot[:nr], f1_tot[nr:] g_minus, g_plus = -g[:nsava], np.fliplr(g[nsava:]) return f1_minus, f1_plus, g_minus, g_plus, p0_minus
def test_eigs(par): """Eigenvalues and condition number estimate with ARPACK""" # explicit=True diag = np.arange(par["nx"], 0, -1) + par["imag"] * np.arange(par["nx"], 0, -1) Op = MatrixMult( np.vstack((np.diag(diag), np.zeros( (par["ny"] - par["nx"], par["nx"]))))) eigs = Op.eigs() assert_array_almost_equal(diag[:eigs.size], eigs, decimal=3) cond = Op.cond() assert_array_almost_equal(np.real(cond), par["nx"], decimal=3) # explicit=False Op = Diagonal(diag, dtype=par["dtype"]) if par["ny"] > par["nx"]: Op = VStack([Op, Zero(par["ny"] - par["nx"], par["nx"])]) eigs = Op.eigs() assert_array_almost_equal(diag[:eigs.size], eigs, decimal=3) # uselobpcg cannot be used for square non-symmetric complex matrices if np.iscomplex(Op): eigs1 = Op.eigs(uselobpcg=True) assert_array_almost_equal(eigs, eigs1, decimal=3) cond = Op.cond() assert_array_almost_equal(np.real(cond), par["nx"], decimal=3) if np.iscomplex(Op): cond1 = Op.cond(uselobpcg=True, niter=100) assert_array_almost_equal(np.real(cond), np.real(cond1), decimal=3)
def test_VStack_incosistent_columns(par): """Check error is raised if operators with different number of columns are passed to VStack """ G1 = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) G2 = np.random.normal(0, 10, (par["ny"], par["nx"] + 1)).astype(par["dtype"]) with pytest.raises(ValueError): VStack( [MatrixMult(G1, dtype=par["dtype"]), MatrixMult(G2, dtype=par["dtype"])], dtype=par["dtype"], )
def test_VStack_multiproc(par): """Single and multiprocess consistentcy for VStack operator""" np.random.seed(0) nproc = 2 G = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) x = np.ones(par["nx"]) + par["imag"] * np.ones(par["nx"]) y = np.ones(4 * par["ny"]) + par["imag"] * np.ones(4 * par["ny"]) Vop = VStack([MatrixMult(G, dtype=par["dtype"])] * 4, dtype=par["dtype"]) Vmultiop = VStack( [MatrixMult(G, dtype=par["dtype"])] * 4, nproc=nproc, dtype=par["dtype"] ) assert dottest( Vmultiop, 4 * par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 ) # forward assert_array_almost_equal(Vop * x, Vmultiop * x, decimal=4) # adjoint assert_array_almost_equal(Vop.H * y, Vmultiop.H * y, decimal=4) # close pool Vmultiop.pool.close()
def test_VStack(par): """Dot-test and inversion for VStack operator """ np.random.seed(10) G1 = np.random.normal(0, 10, (par['ny'], par['nx'])).astype(par['dtype']) G2 = np.random.normal(0, 10, (par['ny'], par['nx'])).astype(par['dtype']) x = np.ones(par['nx']) + par['imag']*np.ones(par['nx']) Vop = VStack([MatrixMult(G1, dtype=par['dtype']), MatrixMult(G2, dtype=par['dtype'])], dtype=par['dtype']) assert dottest(Vop, 2*par['ny'], par['nx'], complexflag=0 if par['imag'] == 0 else 3) xlsqr = lsqr(Vop, Vop * x, damp=1e-20, iter_lim=300, show=0)[0] assert_array_almost_equal(x, xlsqr, decimal=4)
def test_memoize_evals(par): """Inversion of problem with real model and complex data, using two equivalent approaches: 1. complex operator enforcing the output of adjoint to be real, 2. joint system of equations for real and complex parts """ rdtype = np.real(np.ones(1, dtype=par["dtype"])).dtype A = np.random.normal(0, 10, ( par["ny"], par["nx"])).astype(rdtype) + par["imag"] * np.random.normal( 0, 10, (par["ny"], par["nx"])).astype(rdtype) Aop = MatrixMult(A, dtype=par["dtype"]) x = np.ones(par["nx"], dtype=rdtype) y = Aop * x # Approach 1 Aop1 = Aop.toreal(forw=False, adj=True) xinv1 = Aop1.div(y) assert_array_almost_equal(x, xinv1) # Approach 2 Amop = MemoizeOperator(Aop, max_neval=10) Aop2 = VStack([Amop.toreal(), Amop.toimag()]) y2 = np.concatenate([np.real(y), np.imag(y)]) xinv2 = Aop2.div(y2) assert_array_almost_equal(x, xinv2)
def RegularizedOperator(Op, Regs, epsRs=(1, )): r"""Regularized operator. Creates a regularized operator given the operator ``Op`` and a list of regularization terms ``Regs``. Parameters ---------- Op : :obj:`pylops.LinearOperator` Operator to invert Regs : :obj:`tuple` or :obj:`list` Regularization operators epsRs : :obj:`tuple` or :obj:`list`, optional Regularization dampings Returns ------- OpReg : :obj:`pylops.LinearOperator` Regularized operator See Also -------- RegularizedInversion: Regularized inversion Notes ----- Create a regularized operator by augumenting the problem operator :math:`\mathbf{Op}`, by a set of regularization terms :math:`\mathbf{R_i}` and their damping factors and :math:`\epsilon_{{R}_i}`: .. math:: \begin{bmatrix} \mathbf{Op} \\ \epsilon_{R_1} \mathbf{R}_1 \\ ... \\ \epsilon_{R_N} \mathbf{R}_N \end{bmatrix} """ OpReg = VStack([Op] + [epsR * Reg for epsR, Reg in zip(epsRs, Regs)], dtype=Op.dtype) return OpReg
def test_VStack(par): """Dot-test and comparison with pylops for VStack operator """ np.random.seed(10) G1 = da.random.normal(0, 10, (par['ny'], par['nx'])).astype(par['dtype']) G2 = da.random.normal(0, 10, (par['ny'], par['nx'])).astype(par['dtype']) x = da.ones(par['nx']) + par['imag']*da.ones(par['nx']) dops = [dMatrixMult(G1, dtype=par['dtype']), dMatrixMult(G2, dtype=par['dtype'])] ops = [MatrixMult(G1.compute(), dtype=par['dtype']), MatrixMult(G2.compute(), dtype=par['dtype'])] dVop = dVStack(dops, compute=(True, True), dtype=par['dtype']) Vop = VStack(ops, dtype=par['dtype']) assert dottest(dVop, 2*par['ny'], par['nx'], chunks=(2*par['ny'], par['nx']), complexflag=0 if par['imag'] == 0 else 3) dy = dVop * x.ravel() y = Vop * x.ravel().compute() assert_array_almost_equal(dy, y, decimal=4)
def test_VStack_rlinear(par): """VStack operator applied to mix of R-linear and C-linear operators""" np.random.seed(0) if np.dtype(par["dtype"]).kind == "c": G = ( np.random.normal(0, 10, (par["ny"], par["nx"])) + 1j * np.random.normal(0, 10, (par["ny"], par["nx"])) ).astype(par["dtype"]) else: G = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) Rop = Real(dims=(par["nx"],), dtype=par["dtype"]) VSop = VStack([Rop, MatrixMult(G, dtype=par["dtype"])], dtype=par["dtype"]) assert VSop.clinear == False assert dottest( VSop, par["nx"] + par["ny"], par["nx"], complexflag=0 if par["imag"] == 0 else 3 ) # forward x = np.random.randn(par["nx"]) + par["imag"] * np.random.randn(par["nx"]) expected = np.concatenate([np.real(x), G @ x]) assert_array_almost_equal(expected, VSop * x, decimal=4)
def test_eigs(par): """Eigenvalues and condition number estimate with ARPACK """ # explicit=True diag = np.arange(par['nx'], 0, -1) +\ par['imag'] * np.arange(par['nx'], 0, -1) Op = MatrixMult(np.vstack((np.diag(diag), np.zeros((par['ny'] - par['nx'], par['nx']))))) eigs = Op.eigs() assert_array_almost_equal(diag[:eigs.size], eigs, decimal=3) cond = Op.cond() assert_array_almost_equal(np.real(cond), par['nx'], decimal=3) # explicit=False Op = Diagonal(diag, dtype=par['dtype']) if par['ny'] > par['nx']: Op = VStack([Op, Zero(par['ny'] - par['nx'], par['nx'])]) eigs = Op.eigs() assert_array_almost_equal(diag[:eigs.size], eigs, decimal=3) cond = Op.cond() assert_array_almost_equal(np.real(cond), par['nx'], decimal=3)
def focusing_wrapper(direct, toff, g0VS, iava1, Rop1, R1op1, Restrop1, iava2, Rop2, R1op2, Restrop2, t): nsmooth = 10 nr = direct.shape[0] nsava1 = iava1.shape[0] nsava2 = iava2.shape[0] nt = t.shape[0] dt = t[1] - t[0] # window directVS_off = direct - toff idirectVS_off = np.round(directVS_off / dt).astype(np.int) w = np.zeros((nr, nt)) wi = np.ones((nr, nt)) for ir in range(nr - 1): w[ir, :idirectVS_off[ir]] = 1 wi = wi - w w = np.hstack((np.fliplr(w), w[:, 1:])) wi = np.hstack((np.fliplr(wi), wi[:, 1:])) if nsmooth > 0: smooth = np.ones(nsmooth) / nsmooth w = filtfilt(smooth, 1, w) wi = filtfilt(smooth, 1, wi) # Input focusing function fd_plus = np.concatenate((np.fliplr(g0VS.T), np.zeros((nr, nt - 1))), axis=-1) # operators Wop = Diagonal(w.flatten()) WSop1 = Diagonal(w[iava1].flatten()) WSop2 = Diagonal(w[iava2].flatten()) WiSop1 = Diagonal(wi[iava1].flatten()) WiSop2 = Diagonal(wi[iava2].flatten()) Mop1 = VStack([ HStack([Restrop1, -1 * WSop1 * Rop1]), HStack([-1 * WSop1 * R1op1, Restrop1]) ]) * BlockDiag([Wop, Wop]) Mop2 = VStack([ HStack([Restrop2, -1 * WSop2 * Rop2]), HStack([-1 * WSop2 * R1op2, Restrop2]) ]) * BlockDiag([Wop, Wop]) Mop = VStack([ HStack([Mop1, Mop1, Zero(Mop1.shape[0], Mop1.shape[1])]), HStack([Mop2, Zero(Mop2.shape[0], Mop2.shape[1]), Mop2]) ]) Gop1 = VStack( [HStack([Restrop1, -1 * Rop1]), HStack([-1 * R1op1, Restrop1])]) Gop2 = VStack( [HStack([Restrop2, -1 * Rop2]), HStack([-1 * R1op2, Restrop2])]) d1 = WSop1 * Rop1 * fd_plus.flatten() d1 = np.concatenate( (d1.reshape(nsava1, 2 * nt - 1), np.zeros((nsava1, 2 * nt - 1)))) d2 = WSop2 * Rop2 * fd_plus.flatten() d2 = np.concatenate( (d2.reshape(nsava2, 2 * nt - 1), np.zeros((nsava2, 2 * nt - 1)))) d = np.concatenate((d1, d2)) # solve comb_f = lsqr(Mop, d.flatten(), iter_lim=10, show=False)[0] comb_f = comb_f.reshape(6 * nr, (2 * nt - 1)) comb_f_tot = comb_f + np.concatenate((np.zeros( (nr, 2 * nt - 1)), fd_plus, np.zeros((4 * nr, 2 * nt - 1)))) f1_1 = comb_f_tot[:2 * nr] + comb_f_tot[2 * nr:4 * nr] f1_2 = comb_f_tot[:2 * nr] + comb_f_tot[4 * nr:] g_1 = BlockDiag([WiSop1, WiSop1]) * Gop1 * f1_1.flatten() g_1 = g_1.reshape(2 * nsava1, (2 * nt - 1)) g_2 = BlockDiag([WiSop2, WiSop2]) * Gop2 * f1_2.flatten() g_2 = g_2.reshape(2 * nsava2, (2 * nt - 1)) f1_1_minus, f1_1_plus = f1_1[:nr], f1_1[nr:] f1_2_minus, f1_2_plus = f1_2[:nr], f1_2[nr:] g_1_minus, g_1_plus = -g_1[:nsava1], np.fliplr(g_1[nsava1:]) g_2_minus, g_2_plus = -g_2[:nsava2], np.fliplr(g_2[nsava2:]) return f1_1_minus, f1_1_plus, f1_2_minus, f1_2_plus, g_1_minus, g_1_plus, g_2_minus, g_2_plus
def Gradient(dims, sampling=1, edge=False, dtype='float64', kind='centered'): r"""Gradient. Apply gradient operator to a multi-dimensional array (at least 2 dimensions are required). Parameters ---------- dims : :obj:`tuple` Number of samples for each dimension. sampling : :obj:`tuple`, optional Sampling steps for each direction. edge : :obj:`bool`, optional Use reduced order derivative at edges (``True``) or ignore them (``False``). dtype : :obj:`str`, optional Type of elements in input array. kind : :obj:`str`, optional Derivative kind (``forward``, ``centered``, or ``backward``). Returns ------- l2op : :obj:`pylops.LinearOperator` Gradient linear operator Notes ----- The Gradient operator applies a first-order derivative to each dimension of a multi-dimensional array in forward mode. For simplicity, given a three dimensional array, the Gradient in forward mode using a centered stencil can be expressed as: .. math:: \mathbf{g}_{i, j, k} = (f_{i+1, j, k} - f_{i-1, j, k}) / d_1 \mathbf{i_1} + (f_{i, j+1, k} - f_{i, j-1, k}) / d_2 \mathbf{i_2} + (f_{i, j, k+1} - f_{i, j, k-1}) / d_3 \mathbf{i_3} which is discretized as follows: .. math:: \mathbf{g} = \begin{bmatrix} \mathbf{df_1} \\ \mathbf{df_2} \\ \mathbf{df_3} \end{bmatrix} In adjoint mode, the adjoints of the first derivatives along different axes are instead summed together. """ ndims = len(dims) if isinstance(sampling, (int, float)): sampling = [sampling] * ndims gop = VStack([FirstDerivative(np.prod(dims), dims=dims, dir=idir, sampling=sampling[idir], edge=edge, kind=kind, dtype=dtype) for idir in range(ndims)]) return gop
def redatuming_wrapper(toff, W, wav, iava, Rop, R1op, Restrop, Sparseop, vsx, vsz, x, z, z_current, nt, dt, nfft, nr, ds, dvsx): from scipy.signal import filtfilt nava = iava.shape[0] nvsx = vsx.shape[0] PUP = np.zeros(shape=(nava, nvsx, nt)) PDOWN = np.zeros(shape=(nava, nvsx, nt)) for ix in range(nvsx): s = '####### Point ' + str(ix + 1) + ' of ' + str( nvsx) + ' of current line (z = ' + str(z_current) + ', x = ' + str( vsx[ix]) + ')' print(s) # direct wave direct = np.loadtxt(path0 + 'Traveltimes/trav_x' + str(vsx[ix]) + '_z' + str(z_current) + '.dat', delimiter=',') f = 2 * np.pi * np.arange(nfft) / (dt * nfft) g0VS = np.zeros((nfft, nr), dtype=np.complex128) for it in range(len(W)): g0VS[it] = W[it] * f[it] * hankel2(0, f[it] * direct + 1e-10) / 4 g0VS = np.fft.irfft(g0VS, nfft, axis=0) / dt g0VS = np.real(g0VS[:nt]) nr = direct.shape[0] nsava = iava.shape[0] # window directVS_off = direct - toff idirectVS_off = np.round(directVS_off / dt).astype(np.int) w = np.zeros((nr, nt)) wi = np.ones((nr, nt)) for ir in range(nr - 1): w[ir, :idirectVS_off[ir]] = 1 wi = wi - w w = np.hstack((np.fliplr(w), w[:, 1:])) wi = np.hstack((np.fliplr(wi), wi[:, 1:])) # smoothing nsmooth = 10 if nsmooth > 0: smooth = np.ones(nsmooth) / nsmooth w = filtfilt(smooth, 1, w) wi = filtfilt(smooth, 1, wi) # Input focusing function fd_plus = np.concatenate((np.fliplr(g0VS.T), np.zeros((nr, nt - 1))), axis=-1) # create operators Wop = Diagonal(w.flatten()) WSop = Diagonal(w[iava].flatten()) WiSop = Diagonal(wi[iava].flatten()) Mop = VStack([ HStack([Restrop, -1 * WSop * Rop]), HStack([-1 * WSop * R1op, Restrop]) ]) * BlockDiag([Wop, Wop]) Mop_radon = Mop * Sparseop Gop = VStack( [HStack([Restrop, -1 * Rop]), HStack([-1 * R1op, Restrop])]) d = WSop * Rop * fd_plus.flatten() d = np.concatenate( (d.reshape(nsava, 2 * nt - 1), np.zeros((nsava, 2 * nt - 1)))) # solve with SPGL1 f = SPGL1(Mop_radon, d.flatten(), sigma=1e-5, iter_lim=35, opt_tol=0.05, dec_tol=0.05, verbosity=1)[0] # alternatively solve with FISTA #f = FISTA(Mop_radon, d.flatten(), eps=1e-1, niter=200, # alpha=2.129944e-04, eigsiter=4, eigstol=1e-3, # tol=1e-2, returninfo=False, show=True)[0] # alternatively solve with LSQR #f = lsqr(Mop_radon, d.flatten(), iter_lim=100, show=True)[0] f = Sparseop * f f = f.reshape(2 * nr, (2 * nt - 1)) f_tot = f + np.concatenate((np.zeros((nr, 2 * nt - 1)), fd_plus)) g_1 = BlockDiag([WiSop, WiSop]) * Gop * f_tot.flatten() g_1 = g_1.reshape(2 * nsava, (2 * nt - 1)) #f1_minus, f1_plus = f_tot[:nr], f_tot[nr:] g_minus, g_plus = -g_1[:nsava], np.fliplr(g_1[nsava:]) # PUP[:, ix, :] = g_minus[:, nt - 1:] PDOWN[:, ix, :] = g_plus[:, nt - 1:] # save redatumed wavefield (line-by-line) jt = 2 redatumed = MDD(PDOWN[:, :, ::jt], PUP[:, :, ::jt], dt=jt * dt, dr=dvsx, wav=wav[::jt], twosided=True, adjoint=False, psf=False, dtype='complex64', dottest=False, **dict(iter_lim=20, show=0)) np.savetxt(path_save + 'Line1_' + str(z_current) + '.dat', np.diag(redatumed[:, :, (nt + 1) // jt - 1]), delimiter=',') # calculate and save angle gathers (line-by-line) vel_sm = np.loadtxt(path0 + 'vel_sm.dat', delimiter=',') cp = vel_sm[find_closest(z_current, z), 751 // 2] irA = np.asarray([7, 15, 24, 35]) nalpha = 201 A = np.zeros((nalpha, len(irA))) for i in np.arange(0, len(irA)): ir = irA[i] anglegath, alpha = AngleGather(np.swapaxes(redatumed, 0, 2), nvsx, nalpha, dt * jt, ds, ir, cp) A[:, i] = anglegath np.savetxt(path_save + 'AngleGather1_' + str(z_current) + '.dat', A, delimiter=',')
def Block(ops, dtype=None): r"""Block operator. Create a block operator from N lists of M linear operators each. Parameters ---------- ops : :obj:`list` List of lists of operators to be combined in block fashion dtype : :obj:`str`, optional Type of elements in input array. Attributes ---------- shape : :obj:`tuple` Operator shape explicit : :obj:`bool` Operator contains a matrix that can be solved explicitly (``True``) or not (``False``) Notes ----- In mathematics, a block or a partitioned matrix is a matrix that is interpreted as being broken into sections called blocks or submatrices. Similarly a block operator is composed of N sets of M linear operators each such that its application in forward mode leads to .. math:: \begin{bmatrix} \mathbf{L_{1,1}} & \mathbf{L_{1,2}} & ... & \mathbf{L_{1,M}} \\ \mathbf{L_{2,1}} & \mathbf{L_{2,2}} & ... & \mathbf{L_{2,M}} \\ ... & ... & ... & ... \\ \mathbf{L_{N,1}} & \mathbf{L_{N,2}} & ... & \mathbf{L_{N,M}} \\ \end{bmatrix} \begin{bmatrix} \mathbf{x}_{1} \\ \mathbf{x}_{2} \\ ... \\ \mathbf{x}_{M} \end{bmatrix} = \begin{bmatrix} \mathbf{L_{1,1}} \mathbf{x}_{1} + \mathbf{L_{1,2}} \mathbf{x}_{2} + \mathbf{L_{1,M}} \mathbf{x}_{M} \\ \mathbf{L_{2,1}} \mathbf{x}_{1} + \mathbf{L_{2,2}} \mathbf{x}_{2} + \mathbf{L_{2,M}} \mathbf{x}_{M} \\ ... \\ \mathbf{L_{N,1}} \mathbf{x}_{1} + \mathbf{L_{N,2}} \mathbf{x}_{2} + \mathbf{L_{N,M}} \mathbf{x}_{M} \\ \end{bmatrix} while its application in adjoint mode leads to .. math:: \begin{bmatrix} \mathbf{L_{1,1}}^H & \mathbf{L_{2,1}}^H & ... & \mathbf{L_{N,1}}^H \\ \mathbf{L_{1,2}}^H & \mathbf{L_{2,2}}^H & ... & \mathbf{L_{N,2}}^H \\ ... & ... & ... & ... \\ \mathbf{L_{1,M}}^H & \mathbf{L_{2,M}}^H & ... & \mathbf{L_{N,M}}^H \\ \end{bmatrix} \begin{bmatrix} \mathbf{y}_{1} \\ \mathbf{y}_{2} \\ ... \\ \mathbf{y}_{N} \end{bmatrix} = \begin{bmatrix} \mathbf{L_{1,1}}^H \mathbf{y}_{1} + \mathbf{L_{2,1}}^H \mathbf{y}_{2} + \mathbf{L_{N,1}}^H \mathbf{y}_{N} \\ \mathbf{L_{1,2}}^H \mathbf{y}_{1} + \mathbf{L_{2,2}}^H \mathbf{y}_{2} + \mathbf{L_{N,2}}^H \mathbf{y}_{N} \\ ... \\ \mathbf{L_{1,M}}^H \mathbf{y}_{1} + \mathbf{L_{2,M}}^H \mathbf{y}_{2} + \mathbf{L_{N,M}}^H \mathbf{y}_{N} \\ \end{bmatrix} """ hblocks = [HStack(hblock, dtype=dtype) for hblock in ops] return VStack(hblocks, dtype=dtype)