class BaseIPSimulation2D(BaseDCSimulation2D): eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") _data_type = properties.StringChoice( "IP data type", default="volt", choices=["volt", "apparent_chargeability"], ) data_type = deprecate_property( _data_type, "data_type", new_name="receiver.data_type", removal_version="0.17.0", future_warn=True, ) fieldsPair = Fields2D _Jmatrix = None _f = None # the DC fields _sign = 1.0 _pred = None _scale = None gtgdiag = None def fields(self, m): if self.verbose: print(">> Compute DC fields") if self._f is None: # re-uses the DC simulation's fields method self._f = super().fields(None) if self._scale is None: scale = Data(self.survey, np.full(self.survey.nD, self._sign)) f = self.fields_to_space(self._f) # loop through receievers to check if they need to set the _dc_voltage for src in self.survey.source_list: for rx in src.receiver_list: if (rx.data_type == "apparent_chargeability" or self._data_type == "apparent_chargeability"): scale[src, rx] = self._sign / rx.eval(src, self.mesh, f) self._scale = scale.dobs self._pred = self.forward(m, f=self._f) return self._f def dpred(self, m=None, f=None): """ Predicted data. .. math:: d_\\text{pred} = Pf(m) """ # return self.Jvec(m, m, f=f) if f is None: f = self.fields(m) return self._pred def getJtJdiag(self, m, W=None): if self.gtgdiag is None: J = self.getJ(m) if W is None: W = self._scale**2 else: W = (self._scale * W.diagonal())**2 self.gtgdiag = np.einsum("i,ij,ij->j", W, J, J) return self.gtgdiag def Jvec(self, m, v, f=None): return self._scale * super().Jvec(m, v, f) def forward(self, m, f=None): return self.Jvec(m, m, f=f) def Jtvec(self, m, v, f=None): return super().Jtvec(m, v * self._scale, f) @property def deleteTheseOnModelUpdate(self): toDelete = [] return toDelete @property def MeSigmaDerivMat(self): """ Derivative of MeSigma with respect to the model """ if getattr(self, "_MeSigmaDerivMat", None) is None: dsigma_dlogsigma = sdiag(self.sigma) * self.etaDeriv self._MeSigmaDerivMat = (self.mesh.getEdgeInnerProductDeriv( np.ones(self.mesh.nC))(np.ones(self.mesh.nE)) * dsigma_dlogsigma) return self._MeSigmaDerivMat # TODO: This should take a vector def MeSigmaDeriv(self, u, v, adjoint=False): """ Derivative of MeSigma with respect to the model times a vector (u) """ if self.storeInnerProduct: if adjoint: return self.MeSigmaDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MeSigmaDerivMat * v) else: dsigma_dlogsigma = sdiag(self.sigma) * self.etaDeriv if adjoint: return dsigma_dlogsigma.T * ( self.mesh.getEdgeInnerProductDeriv(self.sigma)(u).T * v) else: return self.mesh.getEdgeInnerProductDeriv( self.sigma)(u) * (dsigma_dlogsigma * v) @property def MccRhoiDerivMat(self): """ Derivative of MccRho with respect to the model """ if getattr(self, "_MccRhoiDerivMat", None) is None: rho = self.rho vol = self.mesh.vol drho_dlogrho = sdiag(rho) * self.etaDeriv self._MccRhoiDerivMat = sdiag(vol * (-1.0 / rho**2)) * drho_dlogrho return self._MccRhoiDerivMat def MccRhoiDeriv(self, u, v, adjoint=False): """ Derivative of :code:`MccRhoi` with respect to the model. """ if len(self.rho.shape) > 1: if self.rho.shape[1] > self.mesh.dim: raise NotImplementedError( "Full anisotropy is not implemented for MccRhoiDeriv.") if self.storeInnerProduct: if adjoint: return self.MccRhoiDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MccRhoiDerivMat * v) else: vol = self.mesh.vol rho = self.rho drho_dlogrho = sdiag(rho) * self.etaDeriv if adjoint: return drho_dlogrho.T * (sdiag(u * vol * (-1.0 / rho**2)) * v) else: return sdiag(u * vol * (-1.0 / rho**2)) * (drho_dlogrho * v) @property def MnSigmaDerivMat(self): """ Derivative of MnSigma with respect to the model """ if getattr(self, "_MnSigmaDerivMat", None) is None: sigma = self.sigma vol = self.mesh.vol dsigma_dlogsigma = sdiag(sigma) * self.etaDeriv self._MnSigmaDerivMat = self.mesh.aveN2CC.T * sdiag( vol) * dsigma_dlogsigma return self._MnSigmaDerivMat def MnSigmaDeriv(self, u, v, adjoint=False): """ Derivative of MnSigma with respect to the model times a vector (u) """ if self.storeInnerProduct: if adjoint: return self.MnSigmaDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MnSigmaDerivMat * v) else: sigma = self.sigma vol = self.mesh.vol dsigma_dlogsigma = sdiag(sigma) * self.etaDeriv if adjoint: return dsigma_dlogsigma.T * (vol * (self.mesh.aveN2CC * (u * v))) else: dsig_dm_v = dsigma_dlogsigma * v return u * (self.mesh.aveN2CC.T * (vol * dsig_dm_v))
class BaseRx(SimPEG.Survey.BaseTimeRx): locs = None rxType = None data_type = properties.StringChoice( "Type of DC-IP survey", required=True, default="volt", choices=["volt", "apparent_resistivity", "apparent_chargeability"]) data_type = 'volt' knownRxTypes = { 'phi': ['phi', None], 'ex': ['e', 'x'], 'ey': ['e', 'y'], 'ez': ['e', 'z'], 'jx': ['j', 'x'], 'jy': ['j', 'y'], 'jz': ['j', 'z'], } def __init__(self, locs, times, rxType, **kwargs): SimPEG.Survey.BaseTimeRx.__init__(self, locs, times, rxType, **kwargs) @property def dc_voltage(self): return self._dc_voltage @property def projField(self): """Field Type projection (e.g. e b ...)""" return self.knownRxTypes[self.rxType][0] def projGLoc(self, f): """Grid Location projection (e.g. Ex Fy ...)""" comp = self.knownRxTypes[self.rxType][1] if comp is not None: return f._GLoc(self.rxType) + comp return f._GLoc(self.rxType) def getTimeP(self, timesall): """ Returns the time projection matrix. .. note:: This is not stored in memory, but is created on demand. """ time_inds = np.in1d(timesall, self.times) return time_inds def eval(self, src, mesh, f): P = self.getP(mesh, self.projGLoc(f)) return P * f[src, self.projField] def evalDeriv(self, src, mesh, f, v, adjoint=False): P = self.getP(mesh, self.projGLoc(f)) if not adjoint: return P * v elif adjoint: return P.T * v
class BaseRx(survey.BaseRx): """ Frequency domain receiver base class :param numpy.ndarray locations: receiver locations (ie. :code:`np.r_[x,y,z]`) :param string orientation: receiver orientation 'x', 'y' or 'z' :param string component: real or imaginary component 'real' or 'imag' """ orientation = properties.StringChoice( "orientation of the receiver. Must currently be 'x', 'y', 'z'", ["x", "y", "z"]) component = properties.StringChoice( "component of the field (real or imag)", { "real": ["re", "in-phase", "in phase"], "imag": ["imaginary", "im", "out-of-phase", "out of phase"], }, ) projComp = deprecate_property( orientation, "projComp", new_name="orientation", removal_version="0.16.0", future_warn=True, ) def __init__(self, locations, orientation=None, component=None, **kwargs): proj = kwargs.pop("projComp", None) if proj is not None: self.projComp = proj else: self.orientation = orientation self.component = component super(BaseRx, self).__init__(locations, **kwargs) def projGLoc(self, f): """Grid Location projection (e.g. Ex Fy ...)""" return f._GLoc(self.projField) + self.orientation def eval(self, src, mesh, f): """ Project fields to receivers to get data. :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: FDEM source :param discretize.base.BaseMesh mesh: mesh used :param Fields f: fields object :rtype: numpy.ndarray :return: fields projected to recievers """ P = self.getP(mesh, self.projGLoc(f)) f_part_complex = f[src, self.projField] f_part = getattr(f_part_complex, self.component) # real or imag component return P * f_part def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): """ Derivative of projected fields with respect to the inversion model times a vector. :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: FDEM source :param discretize.base.BaseMesh mesh: mesh used :param Fields f: fields object :param numpy.ndarray v: vector to multiply :rtype: numpy.ndarray :return: fields projected to recievers """ df_dmFun = getattr(f, "_{0}Deriv".format(self.projField), None) assert v is not None, "v must be provided to compute the deriv or adjoint" P = self.getP(mesh, self.projGLoc(f)) if not adjoint: assert ( du_dm_v is not None ), "du_dm_v must be provided to evaluate the receiver deriv" df_dm_v = df_dmFun(src, du_dm_v, v, adjoint=False) Pv_complex = P * df_dm_v Pv = getattr(Pv_complex, self.component) return Pv elif adjoint: PTv_real = P.T * v if self.component == "imag": PTv = 1j * PTv_real elif self.component == "real": PTv = PTv_real.astype(complex) else: raise NotImplementedError("must be real or imag") df_duT, df_dmT = df_dmFun(src, None, PTv, adjoint=True) if self.component == "imag": # conjugate df_duT *= -1 df_dmT *= -1 return df_duT, df_dmT
class Simulation3DIntegral(BasePFSimulation): """ magnetic simulation in integral form. """ chi, chiMap, chiDeriv = props.Invertible( "Magnetic Susceptibility (SI)", default=1.0 ) modelType = properties.StringChoice( "Type of magnetization model", choices=["susceptibility", "vector"], default="susceptibility", ) is_amplitude_data = properties.Boolean( "Whether the supplied data is amplitude data", default=False ) def __init__(self, mesh, **kwargs): super().__init__(mesh, **kwargs) self._G = None self._M = None self._gtg_diagonal = None self.modelMap = self.chiMap setKwargs(self, **kwargs) @property def M(self): """ M: ndarray Magnetization matrix """ if getattr(self, "_M", None) is None: if self.modelType == "vector": self._M = sp.identity(self.nC) * self.survey.source_field.parameters[0] else: mag = mat_utils.dip_azimuth2cartesian( np.ones(self.nC) * self.survey.source_field.parameters[1], np.ones(self.nC) * self.survey.source_field.parameters[2], ) self._M = sp.vstack( ( sdiag(mag[:, 0] * self.survey.source_field.parameters[0]), sdiag(mag[:, 1] * self.survey.source_field.parameters[0]), sdiag(mag[:, 2] * self.survey.source_field.parameters[0]), ) ) return self._M @M.setter def M(self, M): """ Create magnetization matrix from unit vector orientation :parameter M: array (3*nC,) or (nC, 3) """ if self.modelType == "vector": self._M = sdiag(mkvc(M) * self.survey.source_field.parameters[0]) else: M = M.reshape((-1, 3)) self._M = sp.vstack( ( sdiag(M[:, 0] * self.survey.source_field.parameters[0]), sdiag(M[:, 1] * self.survey.source_field.parameters[0]), sdiag(M[:, 2] * self.survey.source_field.parameters[0]), ) ) def fields(self, model): model = self.chiMap * model if self.store_sensitivities == "forward_only": self.model = model fields = mkvc(self.linear_operator()) else: fields = np.asarray(self.G @ model.astype(np.float32)) if self.is_amplitude_data: fields = self.compute_amplitude(fields) return fields @property def G(self): if getattr(self, "_G", None) is None: self._G = self.linear_operator() return self._G @property def nD(self): """ Number of data """ self._nD = self.survey.receiver_locations.shape[0] return self._nD @property def tmi_projection(self): if getattr(self, "_tmi_projection", None) is None: # Convert from north to cartesian self._tmi_projection = mat_utils.dip_azimuth2cartesian( self.survey.source_field.parameters[1], self.survey.source_field.parameters[2], ) return self._tmi_projection def getJtJdiag(self, m, W=None): """ Return the diagonal of JtJ """ self.model = m if W is None: W = np.ones(self.nD) else: W = W.diagonal() ** 2 if getattr(self, "_gtg_diagonal", None) is None: diag = np.zeros(self.G.shape[1]) if not self.is_amplitude_data: for i in range(len(W)): diag += W[i] * (self.G[i] * self.G[i]) else: fieldDeriv = self.fieldDeriv Gx = self.G[::3] Gy = self.G[1::3] Gz = self.G[2::3] for i in range(len(W)): row = ( fieldDeriv[0, i] * Gx[i] + fieldDeriv[1, i] * Gy[i] + fieldDeriv[2, i] * Gz[i] ) diag += W[i] * (row * row) self._gtg_diagonal = diag else: diag = self._gtg_diagonal return mkvc((sdiag(np.sqrt(diag)) @ self.chiDeriv).power(2).sum(axis=0)) def Jvec(self, m, v, f=None): if self.chi is None: self.model = np.zeros(self.G.shape[1]) dmu_dm_v = self.chiDeriv @ v Jvec = self.G @ dmu_dm_v.astype(np.float32) if self.is_amplitude_data: Jvec = Jvec.reshape((-1, 3)).T fieldDeriv_Jvec = self.fieldDeriv * Jvec return fieldDeriv_Jvec[0] + fieldDeriv_Jvec[1] + fieldDeriv_Jvec[2] else: return Jvec def Jtvec(self, m, v, f=None): if self.chi is None: self.model = np.zeros(self.G.shape[1]) if self.is_amplitude_data: v = (self.fieldDeriv * v).T.reshape(-1) Jtvec = self.G.T @ v.astype(np.float32) return np.asarray(self.chiDeriv.T @ Jtvec) @property def fieldDeriv(self): if self.chi is None: self.model = np.zeros(self.G.shape[1]) if getattr(self, "_fieldDeriv", None) is None: fields = np.asarray(self.G.dot((self.chiMap @ self.chi).astype(np.float32))) b_xyz = self.normalized_fields(fields) self._fieldDeriv = b_xyz return self._fieldDeriv @classmethod def normalized_fields(cls, fields): """ Return the normalized B fields """ # Get field amplitude amp = cls.compute_amplitude(fields.astype(np.float64)) return fields.reshape((3, -1), order="F") / amp[None, :] @classmethod def compute_amplitude(cls, b_xyz): """ Compute amplitude of the magnetic field """ amplitude = np.linalg.norm(b_xyz.reshape((3, -1), order="F"), axis=0) return amplitude def evaluate_integral(self, receiver_location, components): """ Load in the active nodes of a tensor mesh and computes the magnetic forward relation between a cuboid and a given observation location outside the Earth [obsx, obsy, obsz] INPUT: receiver_location: [obsx, obsy, obsz] nC x 3 Array components: list[str] List of magnetic components chosen from: 'bx', 'by', 'bz', 'bxx', 'bxy', 'bxz', 'byy', 'byz', 'bzz' OUTPUT: Tx = [Txx Txy Txz] Ty = [Tyx Tyy Tyz] Tz = [Tzx Tzy Tzz] """ # TODO: This should probably be converted to C eps = 1e-8 # add a small value to the locations to avoid /0 rows = {component: np.zeros(3 * self.Xn.shape[0]) for component in components} # number of cells in mesh nC = self.Xn.shape[0] # comp. pos. differences for tne, bsw nodes dz2 = self.Zn[:, 1] - receiver_location[2] + eps dz1 = self.Zn[:, 0] - receiver_location[2] + eps dy2 = self.Yn[:, 1] - receiver_location[1] + eps dy1 = self.Yn[:, 0] - receiver_location[1] + eps dx2 = self.Xn[:, 1] - receiver_location[0] + eps dx1 = self.Xn[:, 0] - receiver_location[0] + eps # comp. squared diff dx2dx2 = dx2 ** 2.0 dx1dx1 = dx1 ** 2.0 dy2dy2 = dy2 ** 2.0 dy1dy1 = dy1 ** 2.0 dz2dz2 = dz2 ** 2.0 dz1dz1 = dz1 ** 2.0 # 2D radius component squared of corner nodes R1 = dy2dy2 + dx2dx2 R2 = dy2dy2 + dx1dx1 R3 = dy1dy1 + dx2dx2 R4 = dy1dy1 + dx1dx1 # radius to each cell node r1 = np.sqrt(dz2dz2 + R2) + eps r2 = np.sqrt(dz2dz2 + R1) + eps r3 = np.sqrt(dz1dz1 + R1) + eps r4 = np.sqrt(dz1dz1 + R2) + eps r5 = np.sqrt(dz2dz2 + R3) + eps r6 = np.sqrt(dz2dz2 + R4) + eps r7 = np.sqrt(dz1dz1 + R4) + eps r8 = np.sqrt(dz1dz1 + R3) + eps # compactify argument calculations arg1_ = dx1 + dy2 + r1 arg1 = dy2 + dz2 + r1 arg2 = dx1 + dz2 + r1 arg3 = dx1 + r1 arg4 = dy2 + r1 arg5 = dz2 + r1 arg6_ = dx2 + dy2 + r2 arg6 = dy2 + dz2 + r2 arg7 = dx2 + dz2 + r2 arg8 = dx2 + r2 arg9 = dy2 + r2 arg10 = dz2 + r2 arg11_ = dx2 + dy2 + r3 arg11 = dy2 + dz1 + r3 arg12 = dx2 + dz1 + r3 arg13 = dx2 + r3 arg14 = dy2 + r3 arg15 = dz1 + r3 arg16_ = dx1 + dy2 + r4 arg16 = dy2 + dz1 + r4 arg17 = dx1 + dz1 + r4 arg18 = dx1 + r4 arg19 = dy2 + r4 arg20 = dz1 + r4 arg21_ = dx2 + dy1 + r5 arg21 = dy1 + dz2 + r5 arg22 = dx2 + dz2 + r5 arg23 = dx2 + r5 arg24 = dy1 + r5 arg25 = dz2 + r5 arg26_ = dx1 + dy1 + r6 arg26 = dy1 + dz2 + r6 arg27 = dx1 + dz2 + r6 arg28 = dx1 + r6 arg29 = dy1 + r6 arg30 = dz2 + r6 arg31_ = dx1 + dy1 + r7 arg31 = dy1 + dz1 + r7 arg32 = dx1 + dz1 + r7 arg33 = dx1 + r7 arg34 = dy1 + r7 arg35 = dz1 + r7 arg36_ = dx2 + dy1 + r8 arg36 = dy1 + dz1 + r8 arg37 = dx2 + dz1 + r8 arg38 = dx2 + r8 arg39 = dy1 + r8 arg40 = dz1 + r8 if ("bxx" in components) or ("bzz" in components): rows["bxx"] = np.zeros((1, 3 * nC)) rows["bxx"][0, 0:nC] = 2 * ( ((dx1 ** 2 - r1 * arg1) / (r1 * arg1 ** 2 + dx1 ** 2 * r1 + eps)) - ((dx2 ** 2 - r2 * arg6) / (r2 * arg6 ** 2 + dx2 ** 2 * r2 + eps)) + ((dx2 ** 2 - r3 * arg11) / (r3 * arg11 ** 2 + dx2 ** 2 * r3 + eps)) - ((dx1 ** 2 - r4 * arg16) / (r4 * arg16 ** 2 + dx1 ** 2 * r4 + eps)) + ((dx2 ** 2 - r5 * arg21) / (r5 * arg21 ** 2 + dx2 ** 2 * r5 + eps)) - ((dx1 ** 2 - r6 * arg26) / (r6 * arg26 ** 2 + dx1 ** 2 * r6 + eps)) + ((dx1 ** 2 - r7 * arg31) / (r7 * arg31 ** 2 + dx1 ** 2 * r7 + eps)) - ((dx2 ** 2 - r8 * arg36) / (r8 * arg36 ** 2 + dx2 ** 2 * r8 + eps)) ) rows["bxx"][0, nC : 2 * nC] = ( dx2 / (r5 * arg25 + eps) - dx2 / (r2 * arg10 + eps) + dx2 / (r3 * arg15 + eps) - dx2 / (r8 * arg40 + eps) + dx1 / (r1 * arg5 + eps) - dx1 / (r6 * arg30 + eps) + dx1 / (r7 * arg35 + eps) - dx1 / (r4 * arg20 + eps) ) rows["bxx"][0, 2 * nC :] = ( dx1 / (r1 * arg4 + eps) - dx2 / (r2 * arg9 + eps) + dx2 / (r3 * arg14 + eps) - dx1 / (r4 * arg19 + eps) + dx2 / (r5 * arg24 + eps) - dx1 / (r6 * arg29 + eps) + dx1 / (r7 * arg34 + eps) - dx2 / (r8 * arg39 + eps) ) rows["bxx"] /= 4 * np.pi rows["bxx"] *= self.M if ("byy" in components) or ("bzz" in components): rows["byy"] = np.zeros((1, 3 * nC)) rows["byy"][0, 0:nC] = ( dy2 / (r3 * arg15 + eps) - dy2 / (r2 * arg10 + eps) + dy1 / (r5 * arg25 + eps) - dy1 / (r8 * arg40 + eps) + dy2 / (r1 * arg5 + eps) - dy2 / (r4 * arg20 + eps) + dy1 / (r7 * arg35 + eps) - dy1 / (r6 * arg30 + eps) ) rows["byy"][0, nC : 2 * nC] = 2 * ( ((dy2 ** 2 - r1 * arg2) / (r1 * arg2 ** 2 + dy2 ** 2 * r1 + eps)) - ((dy2 ** 2 - r2 * arg7) / (r2 * arg7 ** 2 + dy2 ** 2 * r2 + eps)) + ((dy2 ** 2 - r3 * arg12) / (r3 * arg12 ** 2 + dy2 ** 2 * r3 + eps)) - ((dy2 ** 2 - r4 * arg17) / (r4 * arg17 ** 2 + dy2 ** 2 * r4 + eps)) + ((dy1 ** 2 - r5 * arg22) / (r5 * arg22 ** 2 + dy1 ** 2 * r5 + eps)) - ((dy1 ** 2 - r6 * arg27) / (r6 * arg27 ** 2 + dy1 ** 2 * r6 + eps)) + ((dy1 ** 2 - r7 * arg32) / (r7 * arg32 ** 2 + dy1 ** 2 * r7 + eps)) - ((dy1 ** 2 - r8 * arg37) / (r8 * arg37 ** 2 + dy1 ** 2 * r8 + eps)) ) rows["byy"][0, 2 * nC :] = ( dy2 / (r1 * arg3 + eps) - dy2 / (r2 * arg8 + eps) + dy2 / (r3 * arg13 + eps) - dy2 / (r4 * arg18 + eps) + dy1 / (r5 * arg23 + eps) - dy1 / (r6 * arg28 + eps) + dy1 / (r7 * arg33 + eps) - dy1 / (r8 * arg38 + eps) ) rows["byy"] /= 4 * np.pi rows["byy"] *= self.M if "bzz" in components: rows["bzz"] = -rows["bxx"] - rows["byy"] if "bxy" in components: rows["bxy"] = np.zeros((1, 3 * nC)) rows["bxy"][0, 0:nC] = 2 * ( ((dx1 * arg4) / (r1 * arg1 ** 2 + (dx1 ** 2) * r1 + eps)) - ((dx2 * arg9) / (r2 * arg6 ** 2 + (dx2 ** 2) * r2 + eps)) + ((dx2 * arg14) / (r3 * arg11 ** 2 + (dx2 ** 2) * r3 + eps)) - ((dx1 * arg19) / (r4 * arg16 ** 2 + (dx1 ** 2) * r4 + eps)) + ((dx2 * arg24) / (r5 * arg21 ** 2 + (dx2 ** 2) * r5 + eps)) - ((dx1 * arg29) / (r6 * arg26 ** 2 + (dx1 ** 2) * r6 + eps)) + ((dx1 * arg34) / (r7 * arg31 ** 2 + (dx1 ** 2) * r7 + eps)) - ((dx2 * arg39) / (r8 * arg36 ** 2 + (dx2 ** 2) * r8 + eps)) ) rows["bxy"][0, nC : 2 * nC] = ( dy2 / (r1 * arg5 + eps) - dy2 / (r2 * arg10 + eps) + dy2 / (r3 * arg15 + eps) - dy2 / (r4 * arg20 + eps) + dy1 / (r5 * arg25 + eps) - dy1 / (r6 * arg30 + eps) + dy1 / (r7 * arg35 + eps) - dy1 / (r8 * arg40 + eps) ) rows["bxy"][0, 2 * nC :] = ( 1 / r1 - 1 / r2 + 1 / r3 - 1 / r4 + 1 / r5 - 1 / r6 + 1 / r7 - 1 / r8 ) rows["bxy"] /= 4 * np.pi rows["bxy"] *= self.M if "bxz" in components: rows["bxz"] = np.zeros((1, 3 * nC)) rows["bxz"][0, 0:nC] = 2 * ( ((dx1 * arg5) / (r1 * (arg1 ** 2) + (dx1 ** 2) * r1 + eps)) - ((dx2 * arg10) / (r2 * (arg6 ** 2) + (dx2 ** 2) * r2 + eps)) + ((dx2 * arg15) / (r3 * (arg11 ** 2) + (dx2 ** 2) * r3 + eps)) - ((dx1 * arg20) / (r4 * (arg16 ** 2) + (dx1 ** 2) * r4 + eps)) + ((dx2 * arg25) / (r5 * (arg21 ** 2) + (dx2 ** 2) * r5 + eps)) - ((dx1 * arg30) / (r6 * (arg26 ** 2) + (dx1 ** 2) * r6 + eps)) + ((dx1 * arg35) / (r7 * (arg31 ** 2) + (dx1 ** 2) * r7 + eps)) - ((dx2 * arg40) / (r8 * (arg36 ** 2) + (dx2 ** 2) * r8 + eps)) ) rows["bxz"][0, nC : 2 * nC] = ( 1 / r1 - 1 / r2 + 1 / r3 - 1 / r4 + 1 / r5 - 1 / r6 + 1 / r7 - 1 / r8 ) rows["bxz"][0, 2 * nC :] = ( dz2 / (r1 * arg4 + eps) - dz2 / (r2 * arg9 + eps) + dz1 / (r3 * arg14 + eps) - dz1 / (r4 * arg19 + eps) + dz2 / (r5 * arg24 + eps) - dz2 / (r6 * arg29 + eps) + dz1 / (r7 * arg34 + eps) - dz1 / (r8 * arg39 + eps) ) rows["bxz"] /= 4 * np.pi rows["bxz"] *= self.M if "byz" in components: rows["byz"] = np.zeros((1, 3 * nC)) rows["byz"][0, 0:nC] = ( 1 / r3 - 1 / r2 + 1 / r5 - 1 / r8 + 1 / r1 - 1 / r4 + 1 / r7 - 1 / r6 ) rows["byz"][0, nC : 2 * nC] = 2 * ( (((dy2 * arg5) / (r1 * (arg2 ** 2) + (dy2 ** 2) * r1 + eps))) - (((dy2 * arg10) / (r2 * (arg7 ** 2) + (dy2 ** 2) * r2 + eps))) + (((dy2 * arg15) / (r3 * (arg12 ** 2) + (dy2 ** 2) * r3 + eps))) - (((dy2 * arg20) / (r4 * (arg17 ** 2) + (dy2 ** 2) * r4 + eps))) + (((dy1 * arg25) / (r5 * (arg22 ** 2) + (dy1 ** 2) * r5 + eps))) - (((dy1 * arg30) / (r6 * (arg27 ** 2) + (dy1 ** 2) * r6 + eps))) + (((dy1 * arg35) / (r7 * (arg32 ** 2) + (dy1 ** 2) * r7 + eps))) - (((dy1 * arg40) / (r8 * (arg37 ** 2) + (dy1 ** 2) * r8 + eps))) ) rows["byz"][0, 2 * nC :] = ( dz2 / (r1 * arg3 + eps) - dz2 / (r2 * arg8 + eps) + dz1 / (r3 * arg13 + eps) - dz1 / (r4 * arg18 + eps) + dz2 / (r5 * arg23 + eps) - dz2 / (r6 * arg28 + eps) + dz1 / (r7 * arg33 + eps) - dz1 / (r8 * arg38 + eps) ) rows["byz"] /= 4 * np.pi rows["byz"] *= self.M if ("bx" in components) or ("tmi" in components): rows["bx"] = np.zeros((1, 3 * nC)) rows["bx"][0, 0:nC] = ( (-2 * np.arctan2(dx1, arg1 + eps)) - (-2 * np.arctan2(dx2, arg6 + eps)) + (-2 * np.arctan2(dx2, arg11 + eps)) - (-2 * np.arctan2(dx1, arg16 + eps)) + (-2 * np.arctan2(dx2, arg21 + eps)) - (-2 * np.arctan2(dx1, arg26 + eps)) + (-2 * np.arctan2(dx1, arg31 + eps)) - (-2 * np.arctan2(dx2, arg36 + eps)) ) rows["bx"][0, nC : 2 * nC] = ( np.log(arg5) - np.log(arg10) + np.log(arg15) - np.log(arg20) + np.log(arg25) - np.log(arg30) + np.log(arg35) - np.log(arg40) ) rows["bx"][0, 2 * nC :] = ( (np.log(arg4) - np.log(arg9)) + (np.log(arg14) - np.log(arg19)) + (np.log(arg24) - np.log(arg29)) + (np.log(arg34) - np.log(arg39)) ) rows["bx"] /= -4 * np.pi rows["bx"] *= self.M if ("by" in components) or ("tmi" in components): rows["by"] = np.zeros((1, 3 * nC)) rows["by"][0, 0:nC] = ( np.log(arg5) - np.log(arg10) + np.log(arg15) - np.log(arg20) + np.log(arg25) - np.log(arg30) + np.log(arg35) - np.log(arg40) ) rows["by"][0, nC : 2 * nC] = ( (-2 * np.arctan2(dy2, arg2 + eps)) - (-2 * np.arctan2(dy2, arg7 + eps)) + (-2 * np.arctan2(dy2, arg12 + eps)) - (-2 * np.arctan2(dy2, arg17 + eps)) + (-2 * np.arctan2(dy1, arg22 + eps)) - (-2 * np.arctan2(dy1, arg27 + eps)) + (-2 * np.arctan2(dy1, arg32 + eps)) - (-2 * np.arctan2(dy1, arg37 + eps)) ) rows["by"][0, 2 * nC :] = ( (np.log(arg3) - np.log(arg8)) + (np.log(arg13) - np.log(arg18)) + (np.log(arg23) - np.log(arg28)) + (np.log(arg33) - np.log(arg38)) ) rows["by"] /= -4 * np.pi rows["by"] *= self.M if ("bz" in components) or ("tmi" in components): rows["bz"] = np.zeros((1, 3 * nC)) rows["bz"][0, 0:nC] = ( np.log(arg4) - np.log(arg9) + np.log(arg14) - np.log(arg19) + np.log(arg24) - np.log(arg29) + np.log(arg34) - np.log(arg39) ) rows["bz"][0, nC : 2 * nC] = ( (np.log(arg3) - np.log(arg8)) + (np.log(arg13) - np.log(arg18)) + (np.log(arg23) - np.log(arg28)) + (np.log(arg33) - np.log(arg38)) ) rows["bz"][0, 2 * nC :] = ( (-2 * np.arctan2(dz2, arg1_ + eps)) - (-2 * np.arctan2(dz2, arg6_ + eps)) + (-2 * np.arctan2(dz1, arg11_ + eps)) - (-2 * np.arctan2(dz1, arg16_ + eps)) + (-2 * np.arctan2(dz2, arg21_ + eps)) - (-2 * np.arctan2(dz2, arg26_ + eps)) + (-2 * np.arctan2(dz1, arg31_ + eps)) - (-2 * np.arctan2(dz1, arg36_ + eps)) ) rows["bz"] /= -4 * np.pi rows["bz"] *= self.M if "tmi" in components: rows["tmi"] = np.dot( self.tmi_projection, np.r_[rows["bx"], rows["by"], rows["bz"]] ) return np.vstack([rows[component] for component in components]) @property def deleteTheseOnModelUpdate(self): deletes = super().deleteTheseOnModelUpdate if self.is_amplitude_data: deletes += ["_gtg_diagonal"] return deletes @property def coordinate_system(self): raise AttributeError( "The coordinate_system property has been removed. " "Instead make use of `SimPEG.maps.SphericalSystem`." )
class IO(properties.HasProperties): """ """ # Survey survey_layout = properties.StringChoice( "Survey geometry of DC surveys", default="SURFACE", choices=["SURFACE", "BOREHOLE", "GENERAL"]) survey_type = properties.StringChoice( "DC-IP Survey type", default="dipole-dipole", choices=["dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"]) dimension = properties.Integer("Dimension of electrode locations", default=2, required=True) a_locations = properties.Array( "locations of the positive (+) current electrodes", required=True, shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) b_locations = properties.Array( "locations of the negative (-) current electrodes", required=True, shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) m_locations = properties.Array( "locations of the positive (+) potential electrodes", required=True, shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) n_locations = properties.Array( "locations of the negative (-) potential electrodes", required=True, shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) electrode_locations = properties.Array( "unique locations of a, b, m, n electrodes", required=True, shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) # Data data_dc_type = properties.StringChoice("Type of DC-IP survey", required=True, default="volt", choices=[ "volt", "apparent_resistivity", "apparent_conductivity", ]) data_dc = properties.Array( "Measured DC data", shape=('*', ), dtype=float # data are floats ) data_ip_type = properties.StringChoice("Type of DC-IP survey", required=True, default="volt", choices=[ "volt", "apparent_chargeability", ]) data_ip = properties.Array( "Measured IP data", shape=('*', ), dtype=float # data are floats ) data_sip_type = properties.StringChoice("Type of DC-IP survey", required=True, default="volt", choices=[ "volt", "apparent_chargeability", ]) data_sip = properties.Array( "Measured Spectral IP data", shape=('*', '*'), dtype=float # data are floats ) times_ip = properties.Array( "Time channels of measured Spectral IP voltages (s)", required=True, shape=('*', ), dtype=float # data are floats ) G = properties.Array( "Geometric factor of DC-IP survey", shape=('*', ), dtype=float # data are floats ) grids = properties.Array( "Spatial grids for plotting pseudo-section", shape=('*', '*'), dtype=float # data are floats ) space_type = properties.StringChoice( "Assumption to compute apparent resistivity", default="half-space", choices=["half-space", "whole-space"]) line_inds = properties.Array( "Line indices", required=True, shape=('*', ), dtype=int # data are floats ) sort_inds = properties.Array( "Sorting indices from ABMN", required=True, shape=('*', ), dtype=int # data are floats ) # Related to Physics and Discretization mesh = properties.Instance("Mesh for discretization", BaseMesh, required=True) dx = properties.Float( "Length of corecell in x-direction", required=True, ) dy = properties.Float("Length of corecell in y-direction", required=True) dy = properties.Float("Length of corecell in z-direction", required=True) npad_x = properties.Integer("The number of padding cells x-direction", required=True, default=5) npad_y = properties.Integer("The number of padding cells y-direction", required=True, default=5) npad_z = properties.Integer("The number of padding cells z-direction", required=True, default=5) pad_rate_x = properties.Float( "Expansion rate of padding cells in x-direction", required=True, default=1.3) pad_rate_y = properties.Float( "Expansion rate of padding cells in y-direction", required=True, default=1.3) pad_rate_z = properties.Float( "Expansion rate of padding cells in z-direction", required=True, default=1.3) ncell_per_dipole = properties.Integer( "The number of cells between dipole electrodes", required=True, default=4) # For synthetic surveys x0 = None lineLength = None a = None n_spacing = None n_data = None def __init__(self, **kwargs): super(IO, self).__init__(**kwargs) warnings.warn( "code under construction - API might change in the future") # Properties @property def voltages(self): """ Votages (V) """ if self.data_dc_type.lower() == "volt": return self.data_dc elif self.data_dc_type.lower() == "apparent_resistivity": return self.data_dc * self.G elif self.data_dc_type.lower() == "apparent_conductivity": return self.apparent_conductivity / (self.data_dc * self.G) else: raise NotImplementedError() @property def apparent_resistivity(self): """ Apparent Resistivity (Ohm-m) """ if self.data_dc_type.lower() == "apparent_resistivity": return self.data_dc elif self.data_dc_type.lower() == "volt": return self.data_dc / self.G elif self.data_dc_type.lower() == "apparent_conductivity": return 1. / self.data_dc else: print(self.data_dc_type.lower()) raise NotImplementedError() @property def apparent_conductivity(self): """ Apparent Conductivity (S/m) """ if self.data_dc_type.lower() == "apparent_conductivity": return self.data_dc elif self.data_dc_type.lower() == "apparent_resistivity": return 1. / self.data_dc elif self.data_dc_type.lower() == "volt": return 1. / self.data_dc * self.G # For IP @property def voltages_ip(self): """ IP votages (V) """ if self.data_ip_type.lower() == "volt": return self.data_ip elif self.data_ip_type.lower() == "apparent_chargeability": if self.voltages is None: raise Exception( "DC voltages must be set to compute IP voltages") return self.data_ip * self.voltages else: raise NotImplementedError() # For SIP @property def voltages_sip(self): """ IP votages (V) """ if self.data_sip_type.lower() == "volt": return self.data_sip elif self.data_sip_type.lower() == "apparent_chargeability": if self.voltages is None: raise Exception( "DC voltages must be set to compute IP voltages") return Utils.sdiag(self.voltages) * self.data_sip else: raise NotImplementedError() @property def apparent_chargeability(self): """ Apparent Conductivity (S/m) """ if self.data_ip_type.lower() == "apparent_chargeability": return self.data_ip elif self.data_ip_type.lower() == "volt": if self.voltages is None: raise Exception( "DC voltages must be set to compute Apparent Chargeability" ) return self.data_ip / self.voltages else: raise NotImplementedError() # For SIP @property def apparent_chargeability_sip(self): """ Apparent Conductivity (S/m) """ if self.data_sip_type.lower() == "apparent_chargeability": return self.data_sip elif self.data_sip_type.lower() == "volt": if self.voltages is None: raise Exception( "DC voltages must be set to compute Apparent Chargeability" ) return Utils.sdiag(1. / self.voltages) * self.data_sip else: raise NotImplementedError() def geometric_factor(self, survey): """ Compute geometric factor, G, using locational informaition in survey object """ geometric_factor = SimPEG.EM.Static.Utils.StaticUtils.geometric_factor G = geometric_factor(survey, survey_type=self.survey_type, space_type=self.space_type) return G def from_ambn_locations_to_survey(self, a_locations, b_locations, m_locations, n_locations, survey_type=None, data_dc=None, data_ip=None, data_sip=None, data_dc_type="volt", data_ip_type="volt", data_sip_type="volt", fname=None, dimension=2, line_inds=None, times_ip=None): """ read A, B, M, N electrode location and data (V or apparent_resistivity) """ self.a_locations = a_locations.copy() self.b_locations = b_locations.copy() self.m_locations = m_locations.copy() self.n_locations = n_locations.copy() self.survey_type = survey_type self.dimension = dimension self.data_dc_type = data_dc_type self.data_ip_type = data_ip_type self.data_sip_type = data_sip_type if times_ip is not None: self.times_ip = times_ip uniqSrc = Utils.uniqueRows(np.c_[self.a_locations, self.b_locations]) uniqElec = SimPEG.Utils.uniqueRows( np.vstack((self.a_locations, self.b_locations, self.m_locations, self.n_locations))) self.electrode_locations = uniqElec[0] nSrc = uniqSrc[0].shape[0] ndata = self.a_locations.shape[0] if self.survey_layout == "SURFACE": # 2D locations srcLists = [] sort_inds = [] for iSrc in range(nSrc): inds = uniqSrc[2] == iSrc sort_inds.append(np.arange(ndata)[inds]) locsM = self.m_locations[inds, :] locsN = self.n_locations[inds, :] if dimension == 2: if survey_type in ['dipole-dipole', 'pole-dipole']: rx = Rx.Dipole_ky(locsM, locsN) elif survey_type in ['dipole-pole', 'pole-pole']: rx = Rx.Pole_ky(locsM) elif dimension == 3: if survey_type in ['dipole-dipole', 'pole-dipole']: rx = Rx.Dipole(locsM, locsN) elif survey_type in ['dipole-pole', 'pole-pole']: rx = Rx.Pole(locsM) else: raise NotImplementedError() if dimension == 2: locA = uniqSrc[0][iSrc, :2] locB = uniqSrc[0][iSrc, 2:] elif dimension == 3: locA = uniqSrc[0][iSrc, :3] locB = uniqSrc[0][iSrc, 3:] if survey_type in ['dipole-dipole', 'dipole-pole']: src = Src.Dipole([rx], locA, locB) elif survey_type in ['pole-dipole', 'pole-pole']: src = Src.Pole([rx], locA) srcLists.append(src) self.sort_inds = np.hstack(sort_inds) if dimension == 2: survey = Survey_ky(srcLists) elif dimension == 3: survey = Survey(srcLists) else: raise NotImplementedError() self.a_locations = self.a_locations[self.sort_inds, :] self.b_locations = self.b_locations[self.sort_inds, :] self.m_locations = self.m_locations[self.sort_inds, :] self.n_locations = self.n_locations[self.sort_inds, :] self.G = self.geometric_factor(survey) if data_dc is not None: self.data_dc = data_dc[self.sort_inds] if data_ip is not None: self.data_ip = data_ip[self.sort_inds] if data_sip is not None: self.data_sip = data_sip[self.sort_inds, :] if line_inds is not None: self.line_inds = line_inds[self.sort_inds] # Here we ignore ... z-locations self.n_data = survey.nD midABx = (self.a_locations[:, 0] + self.b_locations[:, 0]) * 0.5 midMNx = (self.m_locations[:, 0] + self.n_locations[:, 0]) * 0.5 if dimension == 2: z = abs(midABx - midMNx) * 1. / 3. x = (midABx + midMNx) * 0.5 self.grids = np.c_[x, z] elif dimension == 3: midABy = (self.a_locations[:, 1] + self.b_locations[:, 1]) * 0.5 midMNy = (self.m_locations[:, 1] + self.n_locations[:, 1]) * 0.5 z = np.sqrt((midABx - midMNx)**2 + (midABy - midMNy)**2) * 1. / 3. x = (midABx + midMNx) * 0.5 y = (midABy + midMNy) * 0.5 self.grids = np.c_[x, y, z] else: raise Exception() else: raise NotImplementedError() return survey def set_mesh(self, topo=None, dx=None, dy=None, dz=None, n_spacing=None, corezlength=None, npad_x=7, npad_y=7, npad_z=7, pad_rate_x=1.3, pad_rate_y=1.3, pad_rate_z=1.3, ncell_per_dipole=4, mesh_type='TensorMesh', dimension=2, method='nearest'): """ Set up a mesh for a given DC survey """ if mesh_type == 'TreeMesh': raise NotImplementedError() # 2D or 3D mesh if dimension in [2, 3]: if dimension == 2: z_ind = 1 else: z_ind = 2 a = abs(np.diff(np.sort(self.electrode_locations[:, 0]))).min() lineLength = abs(self.electrode_locations[:, 0].max() - self.electrode_locations[:, 0].min()) dx_ideal = a / ncell_per_dipole if dx is None: dx = dx_ideal warnings.warn( "dx is set to {} m (samllest electrode spacing ({}) / {})". format(dx, a, ncell_per_dipole)) if dz is None: dz = dx * 0.5 warnings.warn("dz ({} m) is set to dx ({} m) / {}".format( dz, dx, 2)) x0 = self.electrode_locations[:, 0].min() if topo is None: locs = np.sort(self.electrode_locations, axis=0) else: locs = np.vstack((topo, self.electrode_locations)) if dx > dx_ideal: # warnings.warn( # "Input dx ({}) is greater than expected \n We recommend using {:0.1e} m cells, that is, {} cells per {0.1e} m dipole length".format(dx, dx_ideal, ncell_per_dipole, a) # ) pass self.dx = dx self.dz = dz self.npad_x = npad_x self.npad_z = npad_z self.pad_rate_x = pad_rate_x self.pad_rate_z = pad_rate_z self.ncell_per_dipole = ncell_per_dipole zmax = locs[:, z_ind].max() zmin = locs[:, z_ind].min() # 3 cells each for buffer corexlength = lineLength + dx * 6 if corezlength is None: corezlength = self.grids[:, z_ind].max() ncx = np.round(corexlength / dx) ncz = np.round(corezlength / dz) hx = [(dx, npad_x, -pad_rate_x), (dx, ncx), (dx, npad_x, pad_rate_x)] hz = [(dz, npad_z, -pad_rate_z), (dz, ncz)] x0_mesh = -( (dx * pad_rate_x**(np.arange(npad_x) + 1)).sum() + dx * 3 - x0) z0_mesh = -((dz * pad_rate_z** (np.arange(npad_z) + 1)).sum() + dz * ncz) + zmax # For 2D mesh if dimension == 2: h = [hx, hz] x0_for_mesh = [x0_mesh, z0_mesh] self.xyzlim = np.vstack( (np.r_[x0, x0 + lineLength], np.r_[zmax - corezlength, zmax])) fill_value = "extrapolate" # For 3D mesh else: if dy is None: raise Exception("You must input dy (m)") self.dy = dy self.npad_y = npad_y self.pad_rate_y = pad_rate_y ylocs = np.unique(self.electrode_locations[:, 1]) ymin, ymax = ylocs.min(), ylocs.max() # 3 cells each for buffer in y-direction coreylength = ymax - ymin + dy * 6 ncy = np.round(coreylength / dy) hy = [(dy, npad_y, -pad_rate_y), (dy, ncy), (dy, npad_y, pad_rate_y)] y0 = ylocs.min() - dy / 2. y0_mesh = -((dy * pad_rate_y**(np.arange(npad_y) + 1)).sum() + dy * 3 - y0) h = [hx, hy, hz] x0_for_mesh = [x0_mesh, y0_mesh, z0_mesh] self.xyzlim = np.vstack( (np.r_[x0, x0 + lineLength], np.r_[ymin - dy * 3, ymax + dy * 3], np.r_[zmax - corezlength, zmax])) fill_value = np.nan mesh = Mesh.TensorMesh(h, x0=x0_for_mesh) actind = Utils.surface2ind_topo(mesh, locs, method=method, fill_value=fill_value) else: raise NotImplementedError() return mesh, actind def plotPseudoSection( self, data_type="apparent_resistivity", data=None, dataloc=True, aspect_ratio=2, scale="log", cmap="viridis", ncontour=10, ax=None, figname=None, clim=None, label=None, iline=0, ): """ Plot 2D pseudo-section for DC-IP data """ matplotlib.rcParams['font.size'] = 12 if ax is None: fig = plt.figure(figsize=(10, 5)) ax = plt.subplot(111) if self.dimension == 2: inds = np.ones(self.n_data, dtype=bool) grids = self.grids.copy() elif self.dimension == 3: inds = self.line_inds == iline grids = self.grids[inds, :][:, [0, 2]] else: raise NotImplementedError() if data_type == "apparent_resistivity": if data is None: val = self.apparent_resistivity[inds] else: val = data.copy()[inds] label = "Apparent Res. ($\Omega$m)" elif data_type == "volt": if data is None: val = self.voltages[inds] else: val = data.copy()[inds] label = "Voltage (V)" elif data_type == "apparent_conductivity": if data is None: val = self.apparent_conductivity[inds] else: val = data.copy()[inds] label = "Apparent Cond. (S/m)" elif data_type == "apparent_chargeability": if data is not None: val = data.copy()[inds] else: val = self.apparent_chargeability.copy()[inds] * 1e3 label = "Apparent Charg. (mV/V)" elif data_type == "volt_ip": if data is not None: val = data.copy()[inds] else: val = self.voltages_ip.copy()[inds] * 1e3 label = "Secondary voltage. (mV)" else: print(data_type) raise NotImplementedError() if scale == "log": fmt = "10$^{%.1f}$" elif scale == "linear": fmt = "%.1e" else: raise NotImplementedError() out = Utils.plot2Ddata(grids, val, contourOpts={'cmap': cmap}, ax=ax, dataloc=dataloc, scale=scale, ncontour=ncontour, clim=clim) ax.invert_yaxis() ax.set_xlabel("x (m)") ax.set_yticklabels([]) ax.set_ylabel("n-spacing") cb = plt.colorbar(out[0], fraction=0.01, format=fmt, ax=ax) cb.set_label(label) cb.set_ticks(out[0].levels) ax.set_aspect(aspect_ratio) plt.tight_layout() if figname is not None: fig.savefig(figname, dpi=200)
class Simulation2DNodal(BaseDCSimulation2D): """ 2.5D nodal DC problem """ _solutionType = "phiSolution" _formulation = "EB" # CC potentials means J is on faces fieldsPair = Fields2DNodal fieldsPair_fwd = Fields3DNodal _gradT = None bc_type = properties.StringChoice( "Type of boundary condition to use for simulation. Note that Robin and Mixed " "are equivalent.", choices=["Neumann", "Robin", "Mixed"], default="Robin", ) def __init__(self, mesh, **kwargs): BaseDCSimulation2D.__init__(self, mesh, **kwargs) self.solver_opts["is_symmetric"] = True self.solver_opts["is_positive_definite"] = True def getA(self, ky): """ Make the A matrix for the cell centered DC resistivity problem A = D MfRhoI G """ # To handle Mixed boundary condition self.setBC(ky=ky) MeSigma = self.MeSigma MnSigma = self.MnSigma Grad = self.mesh.nodalGrad if self._gradT is None: self._gradT = Grad.T.tocsr() # cache the .tocsr() GradT = self._gradT A = GradT * MeSigma * Grad + ky**2 * MnSigma if self.bc_type != "Neumann": try: A = A + sdiag(self._AvgBC[ky] @ self.sigma) except ValueError as err: if len(self.sigma) != len(self.mesh): raise NotImplementedError( "Anisotropic conductivity is not supported for Robin boundary " "conditions, please use 'Neumann'.") else: raise err return A def getADeriv(self, ky, u, v, adjoint=False): Grad = self.mesh.nodalGrad if adjoint: out = self.MeSigmaDeriv( Grad * u.flatten(), Grad * v, adjoint=adjoint ) + ky**2 * self.MnSigmaDeriv(u.flatten(), v, adjoint=adjoint) else: out = Grad.T * self.MeSigmaDeriv( Grad * u.flatten(), v, adjoint=adjoint ) + ky**2 * self.MnSigmaDeriv(u.flatten(), v, adjoint=adjoint) if self.bc_type != "Neumann" and self.sigmaMap is not None: if getattr(self, "_MBC_sigma", None) is None: self._MBC_sigma = {} if ky not in self._MBC_sigma: self._MBC_sigma[ky] = self._AvgBC[ky] @ self.sigmaDeriv if not isinstance(u, Zero): u = u.flatten() if v.ndim > 1: u = u[:, None] if not adjoint: out += u * (self._MBC_sigma[ky] @ v) else: out += self._MBC_sigma[ky].T @ (u * v) return out def getRHS(self, ky): """ RHS for the DC problem q """ RHS = self.getSourceTerm(ky) return RHS def getRHSDeriv(self, ky, src, v, adjoint=False): """ Derivative of the right hand side with respect to the model """ # TODO: add qDeriv for RHS depending on m # qDeriv = src.evalDeriv(self, ky, adjoint=adjoint) # return qDeriv return Zero() def setBC(self, ky=None): if self.bc_type == "Dirichlet": # do nothing raise ValueError( "Dirichlet conditions are not supported in the Nodal formulation" ) elif self.bc_type == "Neumann": if self.verbose: print( "Homogeneous Neumann is the natural BC for this Nodal discretization." ) return else: if getattr(self, "_AvgBC", None) is None: self._AvgBC = {} if ky in self._AvgBC: return mesh = self.mesh # calculate alpha, beta, gamma at the boundary faces boundary_faces = mesh.boundary_faces boundary_normals = mesh.boundary_face_outward_normals n_bf = len(boundary_faces) alpha = np.zeros(n_bf) # assume a source point at the middle of the top of the mesh middle = np.median(mesh.nodes, axis=0) top_v = np.max(mesh.nodes[:, -1]) source_point = np.r_[middle[:-1], top_v] r_vec = boundary_faces - source_point r = np.linalg.norm(r_vec, axis=-1) r_hat = r_vec / r[:, None] r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) # determine faces that are on the sides and bottom of the mesh... if mesh._meshType.lower() == "tree": not_top = boundary_faces[:, -1] != top_v else: # mesh faces are ordered, faces_x, faces_y, faces_z so... is_b = make_boundary_bool(mesh.shape_faces_y) is_t = np.zeros(mesh.shape_faces_y, dtype=bool, order="F") is_t[:, -1] = True is_t = is_t.reshape(-1, order="F")[is_b] not_top = np.zeros(boundary_faces.shape[0], dtype=bool) not_top[-len(is_t):] = ~is_t # use the exponentiall scaled modified bessel function of second kind, # (the division will cancel out the scaling) # This is more stable for large values of ky * r # actual ratio is k1/k0... alpha[not_top] = (ky * k1e(ky * r) / k0e(ky * r) * r_dot_n)[not_top] P_bf = self.mesh.project_face_to_boundary_face AvgN2Fb = P_bf @ self.mesh.average_node_to_face AvgCC2Fb = P_bf @ self.mesh.average_cell_to_face AvgCC2Fb = sdiag(alpha * (P_bf @ self.mesh.face_areas)) @ AvgCC2Fb self._AvgBC[ky] = AvgN2Fb.T @ AvgCC2Fb
class Point1DImpedance(BaseRx): """ Natural source 1D impedance receiver class :param string component: real or imaginary component 'real' or 'imag' """ component = properties.StringChoice( "component of the field (real or imag)", { "real": ["re", "in-phase", "in phase"], "imag": ["imaginary", "im", "out-of-phase", "out of phase"], }, ) orientation = "yx" def __init__(self, locs, component="real"): self.component = component BaseRx.__init__(self, locs) @property def mesh(self): return self._mesh @mesh.setter def mesh(self, value): if value is getattr(self, "_mesh", None): pass else: self._mesh = value # Utility for convienece def _sDiag(self, t): return sdiag(mkvc(t, 2)) @property def src(self): return self._src @src.setter def src(self, value): self._src = value @property def f(self): return self._f @f.setter def f(self, value): self._f = value @property def Pex(self): if getattr(self, "_Pex", None) is None: self._Pex = self._mesh.getInterpolationMat(self.locations[:, -1], "Fx") return self._Pex @property def Pbx(self): if getattr(self, "_Pbx", None) is None: self._Pbx = self._mesh.getInterpolationMat(self.locations[:, -1], "Ex") return self._Pbx @property def _ex(self): return self.Pex * mkvc(self.f[self.src, "e_1d"], 2) @property def _hx(self): return self.Pbx * mkvc(self.f[self.src, "b_1d"], 2) / mu_0 def _ex_u(self, v): return self.Pex * self.f._eDeriv_u(self.src, v) def _hx_u(self, v): return self.Pbx * self.f._bDeriv_u(self.src, v) / mu_0 def _aex_u(self, v): return self.f._eDeriv_u(self.src, self.Pex.T * v, adjoint=True) def _ahx_u(self, v): return self.f._bDeriv_u(self.src, self.Pbx.T * v, adjoint=True) / mu_0 @property def _Hd(self): return self._sDiag(1.0 / self._hx) def eval(self, src, mesh, f, return_complex=False): """ Project the fields to natural source data. :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: NSEM source :param discretize.TensorMesh mesh: Mesh defining the topology of the problem :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM f: NSEM fields object of the source :param bool (optional) return_complex: Flag for return the complex evaluation :rtype: numpy.ndarray :return: Evaluated data for the receiver """ # NOTE: Maybe set this as a property self.src = src self.mesh = mesh self.f = f rx_eval_complex = -self._Hd * self._ex # Return the full impedance if return_complex: return rx_eval_complex return getattr(rx_eval_complex, self.component) def evalDeriv(self, src, mesh, f, v, adjoint=False): """method evalDeriv The derivative of the projection wrt u :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: NSEM source :param discretize.TensorMesh mesh: Mesh defining the topology of the problem :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM f: NSEM fields object of the source :param numpy.ndarray v: vector of size (nU,) (adjoint=False) and size (nD,) (adjoint=True) :rtype: numpy.ndarray :return: Calculated derivative (nD,) (adjoint=False) and (nP,2) (adjoint=True) for both polarizations """ self.src = src self.mesh = mesh self.f = f if adjoint: Z1d = self.eval(src, mesh, f, True) def aZ_N_uV(x): return -self._aex_u(x) def aZ_D_uV(x): return self._ahx_u(x) rx_deriv = aZ_N_uV(self._Hd.T * v) - aZ_D_uV( self._sDiag(Z1d).T * self._Hd.T * v) if self.component == "imag": rx_deriv_component = 1j * rx_deriv elif self.component == "real": rx_deriv_component = rx_deriv.astype(complex) else: Z1d = self.eval(src, mesh, f, True) Z_N_uV = -self._ex_u(v) Z_D_uV = self._hx_u(v) # Evaluate rx_deriv = self._Hd * (Z_N_uV - self._sDiag(Z1d) * Z_D_uV) rx_deriv_component = np.array(getattr(rx_deriv, self.component)) return rx_deriv_component
class BaseEM1DSurvey(Survey.BaseSurvey, properties.HasProperties): """ Base EM1D Survey """ frequency = properties.Array("Frequency (Hz)", dtype=float) rx_location = properties.Array("Receiver location (x, y, z)", dtype=float) src_location = properties.Array("Source location (x, y, z)", dtype=float) src_path = properties.Array( "Source path (xi, yi, zi), i=0,...N", dtype=float ) src_type = properties.StringChoice( "Source type", default="VMD", choices=[ "VMD", "CircularLoop", "piecewise_segment" ] ) offset = properties.Array("Src-Rx offsets", dtype=float) rx_type = properties.StringChoice( "Source location", default="Hz", choices=["Hz", "ppm", "Bz", "dBzdt"] ) field_type = properties.StringChoice( "Field type", default="secondary", choices=["total", "secondary"] ) depth = properties.Array("Depth of the layers", dtype=float) topo = properties.Array("Topography (x, y, z)", dtype=float) I = properties.Float("Src loop current", default=1.) a = properties.Float("Src loop radius", default=1.) half_switch = properties.Bool("Switch for half-space", default=False) def __init__(self, **kwargs): Survey.BaseSurvey.__init__(self, **kwargs) @property def h(self): """ Source height """ return self.src_location[2]-self.topo[2] @property def z(self): """ Receiver height """ return self.rx_location[2]-self.topo[2] @property def dz(self): """ Source height - Rx height """ return self.z - self.h @property def n_layer(self): """ Srource height """ if self.half_switch is False: return self.depth.size elif self.half_switch is True: return int(1) @property def n_frequency(self): """ # of frequency """ return int(self.frequency.size) @property def src_paths_on_x(self): """ # of frequency """ if getattr(self, '_src_paths_on_x', None) is None: offset = np.unique(self.offset) if offset.size != 1: raise Exception( "For the sourth paths, only single offset works!" ) xy_rot, xy_obs_rot, angle = rotate_to_x_axis( np.flipud(xy), np.r_[offset, 0.] ) return self._src_paths @Utils.requires('prob') def dpred(self, m, f=None): """ Computes predicted data. Here we do not store predicted data because projection (`d = P(f)`) is cheap. """ if f is None: f = self.prob.fields(m) return Utils.mkvc(self.projectFields(f))
class EM1DSurveyTD(BaseEM1DSurvey): """docstring for EM1DSurveyTD""" time = properties.Array( "Time channels (s) at current off-time", dtype=float ) wave_type = properties.StringChoice( "Source location", default="stepoff", choices=["stepoff", "general"] ) moment_type = properties.StringChoice( "Source moment type", default="single", choices=["single", "dual"] ) n_pulse = properties.Integer( "The number of pulses", ) base_frequency = properties.Float( "Base frequency (Hz)" ) time_input_currents = properties.Array( "Time for input currents", dtype=float ) input_currents = properties.Array( "Input currents", dtype=float ) use_lowpass_filter = properties.Bool( "Switch for low pass filter", default=False ) high_cut_frequency = properties.Float( "High cut frequency for low pass filter (Hz)", default=210*1e3 ) # Predicted data _pred = None # ------------- For dual moment ------------- # time_dual_moment = properties.Array( "Off-time channels (s) for the dual moment", dtype=float ) time_input_currents_dual_moment = properties.Array( "Time for input currents (dual moment)", dtype=float ) input_currents_dual_moment = properties.Array( "Input currents (dual moment)", dtype=float ) base_frequency_dual_moment = properties.Float( "Base frequency for the dual moment (Hz)" ) def __init__(self, **kwargs): BaseEM1DSurvey.__init__(self, **kwargs) if self.time is None: raise Exception("time is required!") # Use Sin filter for frequency to time transform self.fftfilt = filters.key_81_CosSin_2009() self.set_frequency() if self.src_type == "VMD": if self.offset is None: raise Exception("offset is required!") if self.offset.size == 1: self.offset = self.offset * np.ones(self.n_frequency) @property def time_int(self): """ Time channels (s) for interpolation" """ if getattr(self, '_time_int', None) is None: if self.moment_type == "single": time = self.time pulse_period = self.pulse_period period = self.period # Dual moment else: time = np.unique(np.r_[self.time, self.time_dual_moment]) pulse_period = np.maximum( self.pulse_period, self.pulse_period_dual_moment ) period = np.maximum(self.period, self.period_dual_moment) tmin = time[time>0.].min() if self.n_pulse == 1: tmax = time.max() + pulse_period elif self.n_pulse == 2: tmax = time.max() + pulse_period + period/2. else: raise NotImplementedError("n_pulse must be either 1 or 2") n_time = int((np.log10(tmax)-np.log10(tmin))*10+1) self._time_int = np.logspace( np.log10(tmin), np.log10(tmax), n_time ) # print (tmin, tmax) return self._time_int @property def n_time(self): return int(self.time.size) @property def period(self): return 1./self.base_frequency @property def pulse_period(self): Tp = ( self.time_input_currents.max() - self.time_input_currents.min() ) return Tp # ------------- For dual moment ------------- # @property def n_time_dual_moment(self): return int(self.time_dual_moment.size) @property def period_dual_moment(self): return 1./self.base_frequency_dual_moment @property def pulse_period_dual_moment(self): Tp = ( self.time_input_currents_dual_moment.max() - self.time_input_currents_dual_moment.min() ) return Tp @property def nD(self): """ # of data """ if self.moment_type == "single": return self.n_time else: return self.n_time + self.n_time_dual_moment @property def lowpass_filter(self): """ Low pass filter values """ if getattr(self, '_lowpass_filter', None) is None: # self._lowpass_filter = butterworth_type_filter( # self.frequency, self.high_cut_frequency # ) self._lowpass_filter = (1+1j*(self.frequency/self.high_cut_frequency))**-1 self._lowpass_filter *= (1+1j*(self.frequency/3e5))**-0.99 # For actual butterworth filter # filter_frequency, values = butter_lowpass_filter( # self.high_cut_frequency # ) # lowpass_func = interp1d( # filter_frequency, values, fill_value='extrapolate' # ) # self._lowpass_filter = lowpass_func(self.frequency) return self._lowpass_filter def set_frequency(self, pts_per_dec=-1): """ Compute Frequency reqired for frequency to time transform """ if self.wave_type == "general": _, frequency, ft, ftarg = check_time( self.time_int, -1, 'dlf', {'pts_per_dec': pts_per_dec, 'dlf': self.fftfilt}, 0 ) elif self.wave_type == "stepoff": _, frequency, ft, ftarg = check_time( self.time, -1, 'dlf', {'pts_per_dec': pts_per_dec, 'dlf': self.fftfilt}, 0, ) else: raise Exception("wave_type must be either general or stepoff") self.frequency = frequency self.ftarg = ftarg def projectFields(self, u): """ Transform frequency domain responses to time domain responses """ # Compute frequency domain reponses right at filter coefficient values # Src waveform: Step-off if self.use_lowpass_filter: factor = self.lowpass_filter.copy() else: factor = np.ones_like(self.frequency, dtype=complex) if self.rx_type == 'Bz': factor *= 1./(2j*np.pi*self.frequency) if self.wave_type == 'stepoff': # Compute EM responses if u.size == self.n_frequency: resp, _ = fourier_dlf( u.flatten()*factor, self.time, self.frequency, self.ftarg ) # Compute EM sensitivities else: resp = np.zeros( (self.n_time, self.n_layer), dtype=np.float64, order='F') # ) # TODO: remove for loop for i in range(self.n_layer): resp_i, _ = fourier_dlf( u[:, i]*factor, self.time, self.frequency, self.ftarg ) resp[:, i] = resp_i # Evaluate piecewise linear input current waveforms # Using Fittermann's approach (19XX) with Gaussian Quadrature elif self.wave_type == 'general': # Compute EM responses if u.size == self.n_frequency: resp_int, _ = fourier_dlf( u.flatten()*factor, self.time_int, self.frequency, self.ftarg ) # step_func = interp1d( # self.time_int, resp_int # ) step_func = iuSpline( np.log10(self.time_int), resp_int ) resp = piecewise_pulse_fast( step_func, self.time, self.time_input_currents, self.input_currents, self.period, n_pulse=self.n_pulse ) # Compute response for the dual moment if self.moment_type == "dual": resp_dual_moment = piecewise_pulse_fast( step_func, self.time_dual_moment, self.time_input_currents_dual_moment, self.input_currents_dual_moment, self.period_dual_moment, n_pulse=self.n_pulse ) # concatenate dual moment response # so, ordering is the first moment data # then the second moment data. resp = np.r_[resp, resp_dual_moment] # Compute EM sensitivities else: if self.moment_type == "single": resp = np.zeros( (self.n_time, self.n_layer), dtype=np.float64, order='F' ) else: # For dual moment resp = np.zeros( (self.n_time+self.n_time_dual_moment, self.n_layer), dtype=np.float64, order='F') # TODO: remove for loop (?) for i in range(self.n_layer): resp_int_i, _ = fourier_dlf( u[:, i]*factor, self.time_int, self.frequency, self.ftarg ) # step_func = interp1d( # self.time_int, resp_int_i # ) step_func = iuSpline( np.log10(self.time_int), resp_int_i ) resp_i = piecewise_pulse_fast( step_func, self.time, self.time_input_currents, self.input_currents, self.period, n_pulse=self.n_pulse ) if self.moment_type == "single": resp[:, i] = resp_i else: resp_dual_moment_i = piecewise_pulse_fast( step_func, self.time_dual_moment, self.time_input_currents_dual_moment, self.input_currents_dual_moment, self.period_dual_moment, n_pulse=self.n_pulse ) resp[:, i] = np.r_[resp_i, resp_dual_moment_i] return resp * (-2.0/np.pi) * mu_0 @Utils.requires('prob') def dpred(self, m, f=None): """ Computes predicted data. Predicted data (`_pred`) are computed and stored when self.prob.fields(m) is called. """ if f is None: f = self.prob.fields(m) return self._pred
class BaseRx(BaseSimPEGRx): """ Base DC receiver """ orientation = properties.StringChoice( "orientation of the receiver. Must currently be 'x', 'y', 'z'", ["x", "y", "z"] ) projField = properties.StringChoice( "field to be projected in the calculation of the data", choices=["phi", "e", "j"], default="phi", ) _geometric_factor = {} def __init__(self, locations=None, **kwargs): super(BaseRx, self).__init__(**kwargs) if locations is not None: self.locations = locations # @property # def projField(self): # """Field Type projection (e.g. e b ...)""" # return self.knownRxTypes[self.rxType][0] data_type = properties.StringChoice( "Type of DC-IP survey", required=True, default="volt", choices=["volt", "apparent_resistivity", "apparent_chargeability"], ) # data_type = 'volt' # knownRxTypes = { # 'phi': ['phi', None], # 'ex': ['e', 'x'], # 'ey': ['e', 'y'], # 'ez': ['e', 'z'], # 'jx': ['j', 'x'], # 'jy': ['j', 'y'], # 'jz': ['j', 'z'], # } @property def geometric_factor(self): return self._geometric_factor def projGLoc(self, f): """Grid Location projection (e.g. Ex Fy ...)""" # field = self.knownRxTypes[self.rxType][0] # orientation = self.knownRxTypes[self.rxType][1] if self.orientation is not None: return f._GLoc(self.projField) + self.orientation return f._GLoc(self.projField) def eval(self, src, mesh, f): P = self.getP(mesh, self.projGLoc(f)) proj_f = self.projField if proj_f == "phi": proj_f = "phiSolution" v = P * f[src, proj_f] if self.data_type == "apparent_resistivity": try: if mesh.dim == 2: return v / self.geometric_factor[src][:, None] return v / self.geometric_factor[src] except KeyError: raise KeyError( "Receiver geometric factor has not been set, please execute " "survey.set_geometric_factor()" ) return v def evalDeriv(self, src, mesh, f, v=None, adjoint=False): P = self.getP(mesh, self.projGLoc(f)) factor = None if self.data_type == "apparent_resistivity": factor = 1.0 / self.geometric_factor[src] if v is None: if factor is not None: P = sdiag(factor) @ P if adjoint: return P.T return P if not adjoint: v = P @ v if factor is not None: v = factor * v return v elif adjoint: if factor is not None: v = factor * v return P.T @ v
class EM1DSurveyFD(BaseEM1DSurvey): """ Freqency-domain EM1D survey """ # Nfreq = None switch_real_imag = properties.StringChoice( "Switch for real and imaginary part of the data", default="all", choices=["all", "real", "imag"] ) def __init__(self, **kwargs): BaseEM1DSurvey.__init__(self, **kwargs) if self.src_type == "VMD": if self.offset is None: raise Exception("offset is required!") if self.offset.size == 1: self.offset = self.offset * np.ones(self.n_frequency) @property def nD(self): """ # of data """ if self.switch_real_imag == "all": return int(self.frequency.size * 2) elif ( self.switch_real_imag == "imag" or self.switch_real_imag == "real" ): return int(self.n_frequency) @property def hz_primary(self): # Assumes HCP only at the moment if self.src_type == 'VMD': return -1./(4*np.pi*self.offset**3) elif self.src_type == 'CircularLoop': return self.I/(2*self.a) * np.ones_like(self.frequency) else: raise NotImplementedError() def projectFields(self, u): """ Decompose frequency domain EM responses as real and imaginary components """ ureal = (u.real).copy() uimag = (u.imag).copy() if self.rx_type == 'Hz': factor = 1. elif self.rx_type == 'ppm': factor = 1./self.hz_primary * 1e6 if self.switch_real_imag == 'all': ureal = (u.real).copy() uimag = (u.imag).copy() if ureal.ndim == 1 or 0: resp = np.r_[ureal*factor, uimag*factor] elif ureal.ndim == 2: if np.isscalar(factor): resp = np.vstack( (factor*ureal, factor*uimag) ) else: resp = np.vstack( (Utils.sdiag(factor)*ureal, Utils.sdiag(factor)*uimag) ) else: raise NotImplementedError() elif self.switch_real_imag == 'real': resp = (u.real).copy() elif self.switch_real_imag == 'imag': resp = (u.imag).copy() else: raise NotImplementedError() return resp
class Conversation(BaseConversation): """This represents a full conversation result.""" type = properties.StringChoice( 'The type of conversation.', choices=['email', 'chat', 'phone', 'spam'], required=True, ) folder_id = properties.Integer( 'ID of the Mailbox Folder to which this conversation resides.', required=True, ) is_draft = properties.Bool( 'Is this a draft conversation? This property duplicates ``draft``, ' 'but both are received in API responses at the same time so neither ' 'can be considered "deprecated".', ) draft = properties.Bool( 'Is this a draft conversation? This property duplicates ' '``is_draft``, but both are received in API responses at the same ' 'time so neither can be considered "deprecated".', ) owner = properties.Instance( 'The Help Scout user who is currently assigned to this conversation.', instance_class=Person, required=True, ) mailbox = properties.Instance( 'The mailbox to which this conversation belongs.', instance_class=MailboxRef, required=True, ) customer = properties.Instance( 'The customer who this conversation is associated with.', instance_class=Person, required=True, ) created_by = properties.Instance( 'The ``Person`` who created this conversation. The ``type`` property ' 'will specify whether it was created by a ``user`` or a ``customer``.', instance_class=Person, ) created_by_type = properties.String( 'The type of user that created this conversation.', ) created_at = properties.DateTime( 'UTC time when this conversation was created.', ) closed_at = properties.DateTime( 'UTC time when this conversation was closed. Null if not closed.', ) closed_by = properties.Instance( 'The Help Scout user who closed this conversation.', instance_class=Person, ) source = properties.Instance( 'Specifies the method in which this conversation was created.', instance_class=Source, ) threads = properties.List( 'Threads associated with the conversation.', prop=Thread, ) cc = properties.List( 'Emails that are CCd.', prop=properties.String('Email Address', ), ) bcc = properties.List( 'Emails that are BCCd.', prop=properties.String('Email Address', ), ) tags = properties.List( 'Tags for the conversation', prop=properties.String('Tag Name'), ) spam = properties.Bool('If this conversation is marked as SPAM.', ) locked = properties.Bool('If this conversation is locked from editing.') user_modified_at = properties.DateTime( 'Last time that this conversation was edited by a user.', )
class Survey(BaseEMSurvey, properties.HasProperties): """ Base DC survey """ rxPair = Rx.BaseRx srcPair = Src.BaseSrc # Survey survey_geometry = properties.StringChoice( "Survey geometry of DC surveys", default="surface", choices=["surface", "borehole", "general"]) survey_type = properties.StringChoice( "DC-IP Survey type", default="dipole-dipole", choices=["dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"]) a_locations = properties.Array( "locations of the positive (+) current electrodes", shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) b_locations = properties.Array( "locations of the negative (-) current electrodes", shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) m_locations = properties.Array( "locations of the positive (+) potential electrodes", shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) n_locations = properties.Array( "locations of the negative (-) potential electrodes", shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) electrode_locations = properties.Array( "unique locations of a, b, m, n electrodes", shape=('*', '*'), # ('*', 3) for 3D or ('*', 2) for 2D dtype=float # data are floats ) electrodes_info = None topo_function = None def __init__(self, srcList, **kwargs): BaseEMSurvey.__init__(self, srcList, **kwargs) def set_geometric_factor(self, data_type="volt", survey_type='dipole-dipole', space_type='half-space'): geometric_factor = SimPEG.EM.Static.Utils.geometric_factor( self, survey_type=survey_type, space_type=space_type) geometric_factor = SimPEG.Survey.Data(self, geometric_factor) for src in self.srcList: for rx in src.rxList: rx._geometric_factor = geometric_factor[src, rx] rx.data_type = data_type return geometric_factor def getABMN_locations(self): a_locations = [] b_locations = [] m_locations = [] n_locations = [] for src in self.srcList: for rx in src.rxList: nRx = rx.nD # Pole Source if isinstance(src, Src.Pole): a_locations.append( src.loc.reshape([1, -1]).repeat(nRx, axis=0)) b_locations.append( src.loc.reshape([1, -1]).repeat(nRx, axis=0)) # Dipole Source elif isinstance(src, Src.Dipole): a_locations.append(src.loc[0].reshape([1, -1]).repeat(nRx, axis=0)) b_locations.append(src.loc[1].reshape([1, -1]).repeat(nRx, axis=0)) # Pole RX if isinstance(rx, Rx.Pole) or isinstance(rx, Rx.Pole_ky): m_locations.append(rx.locs) n_locations.append(rx.locs) # Dipole RX elif isinstance(rx, Rx.Dipole) or isinstance(rx, Rx.Dipole_ky): m_locations.append(rx.locs[0]) n_locations.append(rx.locs[1]) self.a_locations = np.vstack(a_locations) self.b_locations = np.vstack(b_locations) self.m_locations = np.vstack(m_locations) self.n_locations = np.vstack(n_locations) def drapeTopo(self, mesh, actind, option='top', topography=None, force=False): if self.a_locations is None: self.getABMN_locations() # 2D if mesh.dim == 2: if self.survey_geometry == "surface": if self.electrodes_info is None or force: self.electrodes_info = SimPEG.Utils.uniqueRows( np.hstack(( self.a_locations[:, 0], self.b_locations[:, 0], self.m_locations[:, 0], self.n_locations[:, 0], )).reshape([-1, 1])) self.electrode_locations = SimPEG.EM.Static.Utils.drapeTopotoLoc( mesh, self.electrodes_info[0].flatten(), actind=actind, option=option) temp = ( self.electrode_locations[self.electrodes_info[2], 1]).reshape( (self.a_locations.shape[0], 4), order="F") self.a_locations = np.c_[self.a_locations[:, 0], temp[:, 0]] self.b_locations = np.c_[self.b_locations[:, 0], temp[:, 1]] self.m_locations = np.c_[self.m_locations[:, 0], temp[:, 2]] self.n_locations = np.c_[self.n_locations[:, 0], temp[:, 3]] # Make interpolation function self.topo_function = interp1d(self.electrode_locations[:, 0], self.electrode_locations[:, 1]) # Loop over all Src and Rx locs and Drape topo for src in self.srcList: # Pole Src if isinstance(src, Src.Pole): locA = src.loc.flatten() z_SrcA = self.topo_function(locA[0]) src.loc = np.array([locA[0], z_SrcA]) for rx in src.rxList: # Pole Rx if isinstance(rx, Rx.Pole) or isinstance( rx, Rx.Pole_ky): locM = rx.locs.copy() z_RxM = self.topo_function(locM[:, 0]) rx.locs = np.c_[locM[:, 0], z_RxM] # Dipole Rx elif isinstance(rx, Rx.Dipole) or isinstance( rx, Rx.Dipole_ky): locM = rx.locs[0].copy() locN = rx.locs[1].copy() z_RxM = self.topo_function(locM[:, 0]) z_RxN = self.topo_function(locN[:, 0]) rx.locs[0] = np.c_[locM[:, 0], z_RxM] rx.locs[1] = np.c_[locN[:, 0], z_RxN] else: raise Exception() # Dipole Src elif isinstance(src, Src.Dipole): locA = src.loc[0].flatten() locB = src.loc[1].flatten() z_SrcA = self.topo_function(locA[0]) z_SrcB = self.topo_function(locB[0]) src.loc[0] = np.array([locA[0], z_SrcA]) src.loc[1] = np.array([locB[0], z_SrcB]) for rx in src.rxList: # Pole Rx if isinstance(rx, Rx.Pole) or isinstance( rx, Rx.Pole_ky): locM = rx.locs.copy() z_RxM = self.topo_function(locM[:, 0]) rx.locs = np.c_[locM[:, 0], z_RxM] # Dipole Rx elif isinstance(rx, Rx.Dipole) or isinstance( rx, Rx.Dipole_ky): locM = rx.locs[0].copy() locN = rx.locs[1].copy() z_RxM = self.topo_function(locM[:, 0]) z_RxN = self.topo_function(locN[:, 0]) rx.locs[0] = np.c_[locM[:, 0], z_RxM] rx.locs[1] = np.c_[locN[:, 0], z_RxN] else: raise Exception() elif self.survey_geometry == "borehole": raise Exception( "Not implemented yet for borehole survey_geometry") else: raise Exception( "Input valid survey survey_geometry: surface or borehole") if mesh.dim == 3: if self.survey_geometry == "surface": if self.electrodes_info is None or force: self.electrodes_info = SimPEG.Utils.uniqueRows( np.vstack(( self.a_locations[:, :2], self.b_locations[:, :2], self.m_locations[:, :2], self.n_locations[:, :2], ))) self.electrode_locations = SimPEG.EM.Static.Utils.drapeTopotoLoc( mesh, self.electrodes_info[0], actind=actind, topo=topography) temp = ( self.electrode_locations[self.electrodes_info[2], 1]).reshape( (self.a_locations.shape[0], 4), order="F") self.a_locations = np.c_[self.a_locations[:, :2], temp[:, 0]] self.b_locations = np.c_[self.b_locations[:, :2], temp[:, 1]] self.m_locations = np.c_[self.m_locations[:, :2], temp[:, 2]] self.n_locations = np.c_[self.n_locations[:, :2], temp[:, 3]] # Make interpolation function self.topo_function = NearestNDInterpolator( self.electrode_locations[:, :2], self.electrode_locations[:, 2]) # Loop over all Src and Rx locs and Drape topo for src in self.srcList: # Pole Src if isinstance(src, Src.Pole): locA = src.loc.reshape([1, -1]) z_SrcA = self.topo_function(locA[0, :2]) src.loc = np.r_[locA[0, :2].flatten(), z_SrcA] for rx in src.rxList: # Pole Rx if isinstance(rx, Rx.Pole): locM = rx.locs.copy() z_RxM = self.topo_function(locM[:, :2]) rx.locs = np.c_[locM[:, 0], z_RxM] # Dipole Rx elif isinstance(rx, Rx.Dipole): locM = rx.locs[0].copy() locN = rx.locs[1].copy() z_RxM = self.topo_function(locM[:, :2]) z_RxN = self.topo_function(locN[:, :2]) rx.locs[0] = np.c_[locM[:, :2], z_RxM] rx.locs[1] = np.c_[locN[:, :2], z_RxN] else: raise Exception() # Dipole Src elif isinstance(src, Src.Dipole): locA = src.loc[0].reshape([1, -1]) locB = src.loc[1].reshape([1, -1]) z_SrcA = self.topo_function(locA[0, :2]) z_SrcB = self.topo_function(locB[0, :2]) src.loc[0] = np.r_[locA[0, :2].flatten(), z_SrcA] src.loc[1] = np.r_[locB[0, :2].flatten(), z_SrcB] for rx in src.rxList: # Pole Rx if isinstance(rx, Rx.Pole): locM = rx.locs.copy() z_RxM = self.topo_function(locM[:, :2]) rx.locs = np.c_[locM[:, :2], z_RxM] # Dipole Rx elif isinstance(rx, Rx.Dipole): locM = rx.locs[0].copy() locN = rx.locs[1].copy() z_RxM = self.topo_function(locM[:, :2]) z_RxN = self.topo_function(locN[:, :2]) rx.locs[0] = np.c_[locM[:, :2], z_RxM] rx.locs[1] = np.c_[locN[:, :2], z_RxN] else: raise Exception() elif self.survey_geometry == "borehole": raise Exception( "Not implemented yet for borehole survey_geometry") else: raise Exception( "Input valid survey survey_geometry: surface or borehole")
class BasePFSimulation(LinearSimulation): actInd = properties.Array("Array of active cells (ground)", dtype=(bool, int), default=None) n_cpu = properties.Integer( "Number of processors used for the forward simulation", default=int(multiprocessing.cpu_count()), ) store_sensitivities = properties.StringChoice( "Compute and store G", choices=["disk", "ram", "forward_only"], default="ram") def __init__(self, mesh, **kwargs): LinearSimulation.__init__(self, mesh, **kwargs) # Find non-zero cells if getattr(self, "actInd", None) is not None: if self.actInd.dtype == "bool": indices = np.where(self.actInd)[0] else: indices = self.actInd else: indices = np.asarray(range(self.mesh.nC)) self.nC = len(indices) # Create active cell projector projection = csr((np.ones(self.nC), (indices, range(self.nC))), shape=(self.mesh.nC, self.nC)) # Create vectors of nodal location for the lower and upper corners bsw = self.mesh.gridCC - self.mesh.h_gridded / 2.0 tne = self.mesh.gridCC + self.mesh.h_gridded / 2.0 xn1, xn2 = bsw[:, 0], tne[:, 0] yn1, yn2 = bsw[:, 1], tne[:, 1] self.Yn = projection.T * np.c_[mkvc(yn1), mkvc(yn2)] self.Xn = projection.T * np.c_[mkvc(xn1), mkvc(xn2)] # Allows for 2D mesh where Zn is defined by user if self.mesh.dim > 2: zn1, zn2 = bsw[:, 2], tne[:, 2] self.Zn = projection.T * np.c_[mkvc(zn1), mkvc(zn2)] def linear_operator(self): self.nC = self.modelMap.shape[0] components = np.array(list(self.survey.components.keys())) active_components = np.hstack([ np.c_[values] for values in self.survey.components.values() ]).tolist() nD = self.survey.nD if self.store_sensitivities == "disk": sens_name = self.sensitivity_path + "sensitivity.npy" if os.path.exists(sens_name): # do not pull array completely into ram, just need to check the size kernel = np.load(sens_name, mmap_mode="r") if kernel.shape == (nD, self.nC): print( f"Found sensitivity file at {sens_name} with expected shape" ) kernel = np.asarray(kernel) return kernel # Single threaded if self.store_sensitivities != "forward_only": kernel = np.vstack([ self.evaluate_integral(receiver, components[component]) for receiver, component in zip( self.survey.receiver_locations.tolist(), active_components) ]) else: kernel = np.hstack([ self.evaluate_integral(receiver, components[component]).dot(self.model) for receiver, component in zip( self.survey.receiver_locations.tolist(), active_components) ]) if self.store_sensitivities == "disk": print(f"writing sensitivity to {sens_name}") os.makedirs(self.sensitivity_path, exist_ok=True) np.save(sens_name, kernel) return kernel def evaluate_integral(self): """ evaluate_integral Compute the forward linear relationship between the model and the physics at a point. :param self: :return: """ raise RuntimeError( f"Integral calculations must implemented by the subclass {self}.") @property def forwardOnly(self): """The forwardOnly property has been deprecated. Please set the store_sensitivites property instead. This will be removed in version 0.15.0 of SimPEG """ warnings.warn( "The forwardOnly property has been deprecated. Please set the store_sensitivites " "property instead. This will be removed in version 0.15.0 of SimPEG", DeprecationWarning, ) return self.store_sensitivities == "forward_only" @forwardOnly.setter def forwardOnly(self, other): warnings.warn( "Do not set parallelized. If interested, try out " "loading dask for parallelism by doing ``import SimPEG.dask``. This will " "be removed in version 0.15.0 of SimPEG", DeprecationWarning, ) if self.other: self.store_sensitivities = "forward_only" @property def parallelized(self): """The parallelized property has been removed. If interested, try out loading dask for parallelism by doing ``import SimPEG.dask``. This will be removed in version 0.15.0 of SimPEG """ warnings.warn( "parallelized has been deprecated. If interested, try out " "loading dask for parallelism by doing ``import SimPEG.dask``. " "This will be removed in version 0.15.0 of SimPEG", DeprecationWarning, ) return False @parallelized.setter def parallelized(self, other): warnings.warn( "Do not set parallelized. If interested, try out " "loading dask for parallelism by doing ``import SimPEG.dask``. This will" "be removed in version 0.15.0 of SimPEG", DeprecationWarning, ) @property def n_cpu(self): """The parallelized property has been removed. If interested, try out loading dask for parallelism by doing ``import SimPEG.dask``. This will be removed in version 0.15.0 of SimPEG """ warnings.warn( "n_cpu has been deprecated. If interested, try out " "loading dask for parallelism by doing ``import SimPEG.dask``. " "This will be removed in version 0.15.0 of SimPEG", DeprecationWarning, ) return 1 @parallelized.setter def n_cpu(self, other): warnings.warn( "Do not set n_cpu. If interested, try out " "loading dask for parallelism by doing ``import SimPEG.dask``. This will" "be removed in version 0.15.0 of SimPEG", DeprecationWarning, )
class MagneticIntegral(Problem.LinearProblem): chi, chiMap, chiDeriv = Props.Invertible( "Magnetic Susceptibility (SI)", default=1. ) forwardOnly = False # If false, matrix is store to memory (watch your RAM) actInd = None #: Active cell indices provided M = None #: Magnetization matrix provided, otherwise all induced rx_type = 'tmi' #: Receiver type either "tmi" | "xyz" magType = 'H0' equiSourceLayer = False silent = False # Don't display progress on screen W = None gtgdiag = None memory_saving_mode = False n_cpu = None parallelized = False coordinate_system = properties.StringChoice( "Type of coordinate system we are regularizing in", choices=['cartesian', 'spherical'], default='cartesian' ) modelType = properties.StringChoice( "Type of magnetization model", choices=['susceptibility', 'vector', 'amplitude'], default='susceptibility' ) def __init__(self, mesh, **kwargs): assert mesh.dim == 3, 'Integral formulation only available for 3D mesh' Problem.BaseProblem.__init__(self, mesh, **kwargs) def fields(self, m): if self.coordinate_system == 'cartesian': m = self.chiMap*(m) else: m = self.chiMap*(matutils.spherical2cartesian(m.reshape((int(len(m)/3), 3), order='F'))) if self.forwardOnly: # Compute the linear operation without forming the full dense F fields = self.Intrgl_Fwr_Op(m=m) else: if getattr(self, '_Mxyz', None) is not None: fields = np.dot(self.G, (self.Mxyz*m).astype(np.float32)) else: fields = np.dot(self.G, m.astype(np.float32)) if self.modelType == 'amplitude': fields = self.calcAmpData(fields.astype(np.float64)) return fields.astype(np.float64) def calcAmpData(self, Bxyz): """ Compute amplitude of the field """ amplitude = np.sum( Bxyz.reshape((3, self.nD), order='F')**2., axis=0 )**0.5 return amplitude @property def G(self): if not self.ispaired: raise Exception('Need to pair!') if getattr(self, '_G', None) is None: if self.modelType == 'vector': self.magType = 'full' self._G = self.Intrgl_Fwr_Op(magType=self.magType, rx_type=self.rx_type) return self._G @property def nD(self): """ Number of data """ self._nD = self.survey.srcField.rxList[0].locs.shape[0] return self._nD @property def ProjTMI(self): if not self.ispaired: raise Exception('Need to pair!') if getattr(self, '_ProjTMI', None) is None: # Convert Bdecination from north to cartesian self._ProjTMI = Utils.matutils.dip_azimuth2cartesian( self.survey.srcField.param[1], self.survey.srcField.param[2] ) return self._ProjTMI def getJtJdiag(self, m, W=None): """ Return the diagonal of JtJ """ dmudm = self.chiMap.deriv(m) self._dSdm = None self._dfdm = None self.model = m if (self.gtgdiag is None) and (self.modelType != 'amplitude'): if W is None: w = np.ones(self.G.shape[1]) else: w = W.diagonal() self.gtgdiag = np.zeros(dmudm.shape[1]) for ii in range(self.G.shape[0]): self.gtgdiag += (w[ii]*self.G[ii, :]*dmudm)**2. if self.coordinate_system == 'cartesian': if self.modelType == 'amplitude': return np.sum((W * self.dfdm * self.G * dmudm)**2., axis=0) else: return self.gtgdiag else: # spherical if self.modelType == 'amplitude': return np.sum(((W * self.dfdm) * self.G * (self.dSdm * dmudm))**2., axis=0) else: Japprox = sdiag(mkvc(self.gtgdiag)**0.5*dmudm.T) * (self.dSdm * dmudm) return mkvc(np.sum(Japprox.power(2), axis=0)) def getJ(self, m, f=None): """ Sensitivity matrix """ if self.coordinate_system == 'cartesian': dmudm = self.chiMap.deriv(m) else: # spherical dmudm = self.dSdm * self.chiMap.deriv(m) if self.modelType == 'amplitude': return self.dfdm * (self.G * dmudm) else: return self.G * dmudm def Jvec(self, m, v, f=None): if self.coordinate_system == 'cartesian': dmudm = self.chiMap.deriv(m) else: dmudm = self.dSdm * self.chiMap.deriv(m) if getattr(self, '_Mxyz', None) is not None: vec = np.dot(self.G, (self.Mxyz*(dmudm*v)).astype(np.float32)) else: vec = np.dot(self.G, (dmudm*v).astype(np.float32)) if self.modelType == 'amplitude': return self.dfdm*vec.astype(np.float64) else: return vec.astype(np.float64) def Jtvec(self, m, v, f=None): if self.coordinate_system == 'spherical': dmudm = self.dSdm * self.chiMap.deriv(m) else: dmudm = self.chiMap.deriv(m) if self.modelType == 'amplitude': if getattr(self, '_Mxyz', None) is not None: vec = self.Mxyz.T*np.dot(self.G.T, (self.dfdm.T*v).astype(np.float32)).astype(np.float64) else: vec = np.dot(self.G.T, (self.dfdm.T*v).astype(np.float32)) else: vec = np.dot(self.G.T, v.astype(np.float32)) return dmudm.T * vec.astype(np.float64) @property def dSdm(self): if getattr(self, '_dSdm', None) is None: if self.model is None: raise Exception('Requires a chi') nC = int(len(self.model)/3) m_xyz = self.chiMap * matutils.spherical2cartesian(self.model.reshape((nC, 3), order='F')) nC = int(m_xyz.shape[0]/3.) m_atp = matutils.cartesian2spherical(m_xyz.reshape((nC, 3), order='F')) a = m_atp[:nC] t = m_atp[nC:2*nC] p = m_atp[2*nC:] Sx = sp.hstack([sp.diags(np.cos(t)*np.cos(p), 0), sp.diags(-a*np.sin(t)*np.cos(p), 0), sp.diags(-a*np.cos(t)*np.sin(p), 0)]) Sy = sp.hstack([sp.diags(np.cos(t)*np.sin(p), 0), sp.diags(-a*np.sin(t)*np.sin(p), 0), sp.diags(a*np.cos(t)*np.cos(p), 0)]) Sz = sp.hstack([sp.diags(np.sin(t), 0), sp.diags(a*np.cos(t), 0), sp.csr_matrix((nC, nC))]) self._dSdm = sp.vstack([Sx, Sy, Sz]) return self._dSdm @property def modelMap(self): """ Call for general mapping of the problem """ return self.chiMap @property def dfdm(self): if self.model is None: self.model = np.zeros(self.G.shape[1]) if getattr(self, '_dfdm', None) is None: Bxyz = self.Bxyz_a(self.chiMap * self.model) # Bx = sp.spdiags(Bxyz[:, 0], 0, self.nD, self.nD) # By = sp.spdiags(Bxyz[:, 1], 0, self.nD, self.nD) # Bz = sp.spdiags(Bxyz[:, 2], 0, self.nD, self.nD) ii = np.kron(np.asarray(range(self.survey.nD), dtype='int'), np.ones(3)) jj = np.asarray(range(3*self.survey.nD), dtype='int') # (data, (row, col)), shape=(3, 3)) # P = s self._dfdm = sp.csr_matrix(( mkvc(Bxyz), (ii,jj)), shape=(self.survey.nD, 3*self.survey.nD)) return self._dfdm def Bxyz_a(self, m): """ Return the normalized B fields """ # Get field data if self.coordinate_system == 'spherical': m = matutils.atp2xyz(m) if getattr(self, '_Mxyz', None) is not None: Bxyz = np.dot(self.G, (self.Mxyz*m).astype(np.float32)) else: Bxyz = np.dot(self.G, m.astype(np.float32)) amp = self.calcAmpData(Bxyz.astype(np.float64)) Bamp = sp.spdiags(1./amp, 0, self.nD, self.nD) return (Bxyz.reshape((3, self.nD), order='F')*Bamp) def Intrgl_Fwr_Op(self, m=None, magType='H0', rx_type='tmi'): """ Magnetic forward operator in integral form magType = 'H0' | 'x' | 'y' | 'z' rx_type = 'tmi' | 'x' | 'y' | 'z' Return _G = Linear forward operator | (forwardOnly)=data """ if m is not None: self.model = self.chiMap*m # Find non-zero cells if getattr(self, 'actInd', None) is not None: if self.actInd.dtype == 'bool': inds = np.where(self.actInd)[0] else: inds = self.actInd else: inds = np.asarray(range(self.mesh.nC)) nC = len(inds) # Create active cell projector P = sp.csr_matrix((np.ones(nC), (inds, range(nC))), shape=(self.mesh.nC, nC)) # Create vectors of nodal location # (lower and upper coners for each cell) if isinstance(self.mesh, Mesh.TreeMesh): # Get upper and lower corners of each cell bsw = (self.mesh.gridCC - self.mesh.h_gridded/2.) tne = (self.mesh.gridCC + self.mesh.h_gridded/2.) xn1, xn2 = bsw[:, 0], tne[:, 0] yn1, yn2 = bsw[:, 1], tne[:, 1] zn1, zn2 = bsw[:, 2], tne[:, 2] else: xn = self.mesh.vectorNx yn = self.mesh.vectorNy zn = self.mesh.vectorNz yn2, xn2, zn2 = np.meshgrid(yn[1:], xn[1:], zn[1:]) yn1, xn1, zn1 = np.meshgrid(yn[:-1], xn[:-1], zn[:-1]) # If equivalent source, use semi-infite prism if self.equiSourceLayer: zn1 -= 1000. self.Yn = P.T*np.c_[mkvc(yn1), mkvc(yn2)] self.Xn = P.T*np.c_[mkvc(xn1), mkvc(xn2)] self.Zn = P.T*np.c_[mkvc(zn1), mkvc(zn2)] # survey = self.survey self.rxLoc = self.survey.srcField.rxList[0].locs if magType == 'H0': if getattr(self, 'M', None) is None: self.M = matutils.dip_azimuth2cartesian(np.ones(nC) * self.survey.srcField.param[1], np.ones(nC) * self.survey.srcField.param[2]) Mx = sdiag(self.M[:, 0] * self.survey.srcField.param[0]) My = sdiag(self.M[:, 1] * self.survey.srcField.param[0]) Mz = sdiag(self.M[:, 2] * self.survey.srcField.param[0]) self.Mxyz = sp.vstack((Mx, My, Mz)) elif magType == 'full': self.Mxyz = sp.identity(3*nC) * self.survey.srcField.param[0] else: raise Exception('magType must be: "H0" or "full"') # Loop through all observations and create forward operator (nD-by-nC) print("Begin forward: M=" + magType + ", Rx type= " + self.rx_type) # Switch to determine if the process has to be run in parallel job = Forward( rxLoc=self.rxLoc, Xn=self.Xn, Yn=self.Yn, Zn=self.Zn, n_cpu=self.n_cpu, forwardOnly=self.forwardOnly, model=self.model, rx_type=self.rx_type, Mxyz=self.Mxyz, P=self.ProjTMI, parallelized=self.parallelized ) G = job.calculate() return G
class DataArray(BaseData): """Data array with unique values at every point in the mesh .. note: DataArray custom colormap is currently unsupported on steno3d.com """ _resource_class = 'array' array = properties.Array( doc='Data, unique values at every point in the mesh', shape=('*', ), dtype=(float, int), serializer=array_serializer, deserializer=array_download(('*', ), (float, int)), ) order = properties.StringChoice( doc='Data array order, for data on grid meshes', choices={ 'c': ('C-STYLE', 'NUMPY', 'ROW-MAJOR', 'ROW'), 'f': ('FORTRAN', 'MATLAB', 'COLUMN-MAJOR', 'COLUMN', 'COL') }, default='c', ) colormap = properties.List( doc='Colormap applied to data range or categories', prop=properties.Color(''), min_length=1, max_length=256, required=False, ) def __init__(self, array=None, **kwargs): super(DataArray, self).__init__(**kwargs) if array is not None: self.array = array def _nbytes(self, arr=None): if arr is None or (isinstance(arr, string_types) and arr == 'array'): arr = self.array if isinstance(arr, np.ndarray): return arr.astype('f4').nbytes raise ValueError('DataArray cannot calculate the number of ' 'bytes of {}'.format(arr)) @properties.observer('array') def _reject_large_files(self, change): self._validate_file_size(change['name'], change['value']) @properties.validator def _validate_array(self): self._validate_file_size('array', self.array) return True def _get_dirty_data(self, force=False): datadict = super(DataArray, self)._get_dirty_data(force) dirty = self._dirty_props if 'order' in dirty or force: datadict['order'] = self.order if self.colormap and ('colormap' in dirty or force): datadict['colormap'] = dumps(self.colormap) return datadict def _get_dirty_files(self, force=False): files = super(DataArray, self)._get_dirty_files(force) dirty = self._dirty_props if 'array' in dirty or force: files['array'] = self._props['array'].serialize(self.array) return files @classmethod def _build_from_json(cls, json, **kwargs): data = DataArray(title=kwargs['title'], description=kwargs['description'], order=json['order'], array=cls._props['array'].deserialize( json['array'], input_dtype=json.get('arrayType', None), )) if json.get('colormap'): data.colormap = json['colormap'] return data @classmethod def _build_from_omf(cls, omf_data): data = dict(location='N' if omf_data.location == 'vertices' else 'CC', data=DataArray(title=omf_data.name, description=omf_data.description, array=omf_data.array.array)) if omf_data.colormap: dlims = np.linspace(np.nanmin(omf_data.array.array), np.nanmax(omf_data.array.array), 257) dlims = (dlims[:-1] + dlims[1:]) / 2 omf_dlims = np.linspace( omf_data.colormap.limits[0], omf_data.colormap.limits[1], len(omf_data.colormap.gradient.array) + 1, ) omf_dlims = (omf_dlims[:-1] + omf_dlims[1:]) / 2 colormap = [] for dval in dlims: colormap += [ omf_data.colormap.gradient.array[np.argmin( np.abs(omf_dlims - dval))] ] data['data'].colormap = colormap return data def _to_omf(self): if self.order != 'c': raise ValueError('OMF data must be "c" order') import omf data = omf.ScalarData( name=self.title or '', description=self.description or '', array=omf.ScalarArray(self.array), ) if self.colormap: data.colormap = omf.ScalarColormap( gradient=omf.ColorArray(self.colormap, ), limits=[np.min(self.array), np.max(self.array)], ) return data
class Simulation2DCellCentered(BaseDCSimulation2D): """ 2.5D cell centered DC problem """ _solutionType = "phiSolution" _formulation = "HJ" # CC potentials means J is on faces fieldsPair = Fields2DCellCentered fieldsPair_fwd = Fields3DCellCentered bc_type = properties.StringChoice( "Type of boundary condition to use for simulation. Note that Robin and Mixed " "are equivalent.", choices=["Dirichlet", "Neumann", "Robin", "Mixed"], default="Robin", ) def __init__(self, mesh, **kwargs): BaseDCSimulation2D.__init__(self, mesh, **kwargs) V = sdiag(self.mesh.cell_volumes) self.Div = V @ self.mesh.face_divergence self.Grad = self.Div.T def getA(self, ky): """ Make the A matrix for the cell centered DC resistivity problem A = D MfRhoI G """ # To handle Mixed boundary condition self.setBC(ky=ky) D = self.Div G = self.Grad if self.bc_type != "Dirichlet": G = G - self._MBC[ky] MfRhoI = self.MfRhoI # Get resistivity rho A = D * MfRhoI * G + ky**2 * self.MccRhoi if self.bc_type == "Neumann": A[0, 0] = A[0, 0] + 1.0 return A def getADeriv(self, ky, u, v, adjoint=False): D = self.Div G = self.Grad if self.bc_type != "Dirichlet": G = G - self._MBC[ky] if adjoint: return self.MfRhoIDeriv( G * u.flatten(), D.T * v, adjoint=adjoint ) + ky**2 * self.MccRhoiDeriv(u.flatten(), v, adjoint=adjoint) else: return D * self.MfRhoIDeriv(G * u.flatten(), v, adjoint=adjoint ) + ky**2 * self.MccRhoiDeriv( u.flatten(), v, adjoint=adjoint) def getRHS(self, ky): """ RHS for the DC problem q """ RHS = self.getSourceTerm(ky) return RHS def getRHSDeriv(self, ky, src, v, adjoint=False): """ Derivative of the right hand side with respect to the model """ # TODO: add qDeriv for RHS depending on m # qDeriv = src.evalDeriv(self, ky, adjoint=adjoint) # return qDeriv return Zero() def setBC(self, ky=None): if self.bc_type == "Dirichlet": return if getattr(self, "_MBC", None) is None: self._MBC = {} if ky in self._MBC: # I have already created the BC matrix for this wavenumber return if self.bc_type == "Neumann": alpha, beta, gamma = 0, 1, 0 else: mesh = self.mesh boundary_faces = mesh.boundary_faces boundary_normals = mesh.boundary_face_outward_normals n_bf = len(boundary_faces) # Top gets 0 Neumann alpha = np.zeros(n_bf) beta = np.ones(n_bf) gamma = 0 # assume a source point at the middle of the top of the mesh middle = np.median(mesh.nodes, axis=0) top_v = np.max(mesh.nodes[:, -1]) source_point = np.r_[middle[:-1], top_v] r_vec = boundary_faces - source_point r = np.linalg.norm(r_vec, axis=-1) r_hat = r_vec / r[:, None] r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) # determine faces that are on the sides and bottom of the mesh... if mesh._meshType.lower() == "tree": not_top = boundary_faces[:, -1] != top_v else: # mesh faces are ordered, faces_x, faces_y, faces_z so... is_b = make_boundary_bool(mesh.shape_faces_y) is_t = np.zeros(mesh.shape_faces_y, dtype=bool, order="F") is_t[:, -1] = True is_t = is_t.reshape(-1, order="F")[is_b] not_top = np.zeros(boundary_faces.shape[0], dtype=bool) not_top[-len(is_t):] = ~is_t # use the exponentialy scaled modified bessel function of second kind, # (the division will cancel out the scaling) # This is more stable for large values of ky * r # actual ratio is k1/k0... alpha[not_top] = (ky * k1e(ky * r) / k0e(ky * r) * r_dot_n)[not_top] B, bc = self.mesh.cell_gradient_weak_form_robin(alpha, beta, gamma) # bc should always be 0 because gamma was always 0 above self._MBC[ky] = B
class BaseTimeRx(BaseRx): """SimPEG Receiver Object for time-domain simulations""" times = properties.Array( "times where the recievers measure data", shape=("*",), required=True ) projTLoc = properties.StringChoice( "location on the time mesh where the data are projected from", choices=["N", "CC"], default="N", ) def __init__(self, locations=None, times=None, **kwargs): super(BaseTimeRx, self).__init__(locations=locations, **kwargs) if times is not None: self.times = times @property def nD(self): """Number of data in the receiver.""" return self.locations.shape[0] * len(self.times) def getSpatialP(self, mesh): """ Returns the spatial projection matrix. .. note:: This is not stored in memory, but is created on demand. """ return mesh.getInterpolationMat(self.locations, self.projGLoc) def getTimeP(self, timeMesh): """ Returns the time projection matrix. .. note:: This is not stored in memory, but is created on demand. """ return timeMesh.getInterpolationMat(self.times, self.projTLoc) def getP(self, mesh, timeMesh): """ Returns the projection matrices as a list for all components collected by the receivers. .. note:: Projection matrices are stored as a dictionary (mesh, timeMesh) if storeProjections is True """ if (mesh, timeMesh) in self._Ps: return self._Ps[(mesh, timeMesh)] Ps = self.getSpatialP(mesh) Pt = self.getTimeP(timeMesh) P = sp.kron(Pt, Ps) if self.storeProjections: self._Ps[(mesh, timeMesh)] = P return P
class BaseRxNSEM_Point(BaseRx): """ Natural source receiver base class. Assumes that the data locations are xyz coordinates. :param numpy.ndarray locs: receiver locations (ie. :code:`np.r_[x,y,z]`) :param string orientation: receiver orientation 'x', 'y' or 'z' :param string component: real or imaginary component 'real' or 'imag' """ component = properties.StringChoice( "component of the field (real or imag)", { "real": ["re", "in-phase", "in phase"], "imag": ["imaginary", "im", "out-of-phase", "out of phase"], }, ) def __init__(self, locs, orientation=None, component=None): self.orientation = orientation self.component = component BaseRx.__init__(self, locs) # Set a mesh property - TODO: remove the following properties @property def mesh(self): return self._mesh @mesh.setter def mesh(self, value): if value is getattr(self, "_mesh", None): pass else: self._mesh = value @property def src(self): return self._src @src.setter def src(self, value): self._src = value @property def f(self): return self._f @f.setter def f(self, value): self._f = value def _locs_e(self): if self.locations.ndim == 3: loc = self.locations[:, :, 0] else: loc = self.locations return loc def _locs_b(self): if self.locations.ndim == 3: loc = self.locations[:, :, 1] else: loc = self.locations return loc # Location projection @property def Pex(self): if getattr(self, "_Pex", None) is None: self._Pex = self._mesh.getInterpolationMat(self._locs_e(), "Ex") return self._Pex @property def Pey(self): if getattr(self, "_Pey", None) is None: self._Pey = self._mesh.getInterpolationMat(self._locs_e(), "Ey") return self._Pey @property def Pbx(self): if getattr(self, "_Pbx", None) is None: self._Pbx = self._mesh.getInterpolationMat(self._locs_b(), "Fx") return self._Pbx @property def Pby(self): if getattr(self, "_Pby", None) is None: self._Pby = self._mesh.getInterpolationMat(self._locs_b(), "Fy") return self._Pby @property def Pbz(self): if getattr(self, "_Pbz", None) is None: self._Pbz = self._mesh.getInterpolationMat(self._locs_e(), "Fz") return self._Pbz # Utility for convienece def _sDiag(self, t): return sdiag(mkvc(t, 2)) # Get the components of the fields # px: x-polaration and py: y-polaration. @property def _ex_px(self): return self.Pex * self.f[self.src, "e_px"] @property def _ey_px(self): return self.Pey * self.f[self.src, "e_px"] @property def _ex_py(self): return self.Pex * self.f[self.src, "e_py"] @property def _ey_py(self): return self.Pey * self.f[self.src, "e_py"] @property def _hx_px(self): return self.Pbx * self.f[self.src, "b_px"] / mu_0 @property def _hy_px(self): return self.Pby * self.f[self.src, "b_px"] / mu_0 @property def _hz_px(self): return self.Pbz * self.f[self.src, "b_px"] / mu_0 @property def _hx_py(self): return self.Pbx * self.f[self.src, "b_py"] / mu_0 @property def _hy_py(self): return self.Pby * self.f[self.src, "b_py"] / mu_0 @property def _hz_py(self): return self.Pbz * self.f[self.src, "b_py"] / mu_0 # Get the derivatives def _ex_px_u(self, vec): return self.Pex * self.f._e_pxDeriv_u(self.src, vec) def _ey_px_u(self, vec): return self.Pey * self.f._e_pxDeriv_u(self.src, vec) def _ex_py_u(self, vec): return self.Pex * self.f._e_pyDeriv_u(self.src, vec) def _ey_py_u(self, vec): return self.Pey * self.f._e_pyDeriv_u(self.src, vec) def _hx_px_u(self, vec): return self.Pbx * self.f._b_pxDeriv_u(self.src, vec) / mu_0 def _hy_px_u(self, vec): return self.Pby * self.f._b_pxDeriv_u(self.src, vec) / mu_0 def _hz_px_u(self, vec): return self.Pbz * self.f._b_pxDeriv_u(self.src, vec) / mu_0 def _hx_py_u(self, vec): return self.Pbx * self.f._b_pyDeriv_u(self.src, vec) / mu_0 def _hy_py_u(self, vec): return self.Pby * self.f._b_pyDeriv_u(self.src, vec) / mu_0 def _hz_py_u(self, vec): return self.Pbz * self.f._b_pyDeriv_u(self.src, vec) / mu_0 # Define the components of the derivative @property def _Hd(self): return self._sDiag(1.0 / (self._sDiag(self._hx_px) * self._hy_py - self._sDiag(self._hx_py) * self._hy_px)) def _Hd_uV(self, v): return (self._sDiag(self._hy_py) * self._hx_px_u(v) + self._sDiag(self._hx_px) * self._hy_py_u(v) - self._sDiag(self._hx_py) * self._hy_px_u(v) - self._sDiag(self._hy_px) * self._hx_py_u(v)) # Adjoint @property def _aex_px(self): return mkvc(mkvc(self.f[self.src, "e_px"], 2).T * self.Pex.T) @property def _aey_px(self): return mkvc(mkvc(self.f[self.src, "e_px"], 2).T * self.Pey.T) @property def _aex_py(self): return mkvc(mkvc(self.f[self.src, "e_py"], 2).T * self.Pex.T) @property def _aey_py(self): return mkvc(mkvc(self.f[self.src, "e_py"], 2).T * self.Pey.T) @property def _ahx_px(self): return mkvc(mkvc(self.f[self.src, "b_px"], 2).T / mu_0 * self.Pbx.T) @property def _ahy_px(self): return mkvc(mkvc(self.f[self.src, "b_px"], 2).T / mu_0 * self.Pby.T) @property def _ahz_px(self): return mkvc(mkvc(self.f[self.src, "b_px"], 2).T / mu_0 * self.Pbz.T) @property def _ahx_py(self): return mkvc(mkvc(self.f[self.src, "b_py"], 2).T / mu_0 * self.Pbx.T) @property def _ahy_py(self): return mkvc(mkvc(self.f[self.src, "b_py"], 2).T / mu_0 * self.Pby.T) @property def _ahz_py(self): return mkvc(mkvc(self.f[self.src, "b_py"], 2).T / mu_0 * self.Pbz.T) # NOTE: need to add a .T at the end for the output to be (nU,) def _aex_px_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return self.f._e_pxDeriv_u( self.src, self.Pex.T * mkvc(vec, ), adjoint=True, ) def _aey_px_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return self.f._e_pxDeriv_u( self.src, self.Pey.T * mkvc(vec, ), adjoint=True, ) def _aex_py_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return self.f._e_pyDeriv_u( self.src, self.Pex.T * mkvc(vec, ), adjoint=True, ) def _aey_py_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return self.f._e_pyDeriv_u( self.src, self.Pey.T * mkvc(vec, ), adjoint=True, ) def _ahx_px_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return (self.f._b_pxDeriv_u( self.src, self.Pbx.T * mkvc(vec, ), adjoint=True, ) / mu_0) def _ahy_px_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return (self.f._b_pxDeriv_u( self.src, self.Pby.T * mkvc(vec, ), adjoint=True, ) / mu_0) def _ahz_px_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return (self.f._b_pxDeriv_u( self.src, self.Pbz.T * mkvc(vec, ), adjoint=True, ) / mu_0) def _ahx_py_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return (self.f._b_pyDeriv_u( self.src, self.Pbx.T * mkvc(vec, ), adjoint=True, ) / mu_0) def _ahy_py_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return (self.f._b_pyDeriv_u( self.src, self.Pby.T * mkvc(vec, ), adjoint=True, ) / mu_0) def _ahz_py_u(self, vec): """""" # vec is (nD,) and returns a (nU,) return (self.f._b_pyDeriv_u( self.src, self.Pbz.T * mkvc(vec, ), adjoint=True, ) / mu_0) # Define the components of the derivative @property def _aHd(self): return self._sDiag(1.0 / (self._sDiag(self._ahx_px) * self._ahy_py - self._sDiag(self._ahx_py) * self._ahy_px)) def _aHd_uV(self, x): return (self._ahx_px_u(self._sDiag(self._ahy_py) * x) + self._ahx_px_u(self._sDiag(self._ahy_py) * x) - self._ahy_px_u(self._sDiag(self._ahx_py) * x) - self._ahx_py_u(self._sDiag(self._ahy_px) * x)) def eval(self, src, mesh, f, return_complex=False): """ Function to evaluate datum for this receiver """ raise NotImplementedError( "SimPEG.EM.NSEM receiver has to have an eval method") def evalDeriv(self, src, mesh, f, v, adjoint=False): """ Function to evaluate datum for this receiver """ raise NotImplementedError( "SimPEG.EM.NSEM receiver has to have an evalDeriv method")
class BaseRx(properties.HasProperties): """SimPEG Receiver Object""" # TODO: write a validator that checks against mesh dimension in the # BaseSimulation # TODO: location locations = RxLocationArray( "Locations of the receivers (nRx x nDim)", shape=("*", "*"), required=True ) # TODO: project_grid? projGLoc = properties.StringChoice( "Projection grid location, default is CC", choices=["CC", "Fx", "Fy", "Fz", "Ex", "Ey", "Ez", "N"], default="CC", ) # TODO: store_projections storeProjections = properties.Bool( "Store calls to getP (organized by mesh)", default=True ) _uid = properties.Uuid("unique ID for the receiver") _Ps = properties.Dictionary("dictonary for storing projections",) def __init__(self, locations=None, **kwargs): super(BaseRx, self).__init__(**kwargs) if locations is not None: self.locations = locations rxType = kwargs.pop("rxType", None) if rxType is not None: warnings.warn( "BaseRx no longer has an rxType. Each rxType should instead " "be a different receiver class." ) if getattr(self, "_Ps", None) is None: self._Ps = {} locs = deprecate_property( locations, "locs", new_name="locations", removal_version="0.16.0", error=True, ) @property def nD(self): """Number of data in the receiver.""" return self.locations.shape[0] def getP(self, mesh, projGLoc=None): """ Returns the projection matrices as a list for all components collected by the receivers. .. note:: Projection matrices are stored as a dictionary listed by meshes. """ if projGLoc is None: projGLoc = self.projGLoc if (mesh, projGLoc) in self._Ps: return self._Ps[(mesh, projGLoc)] P = mesh.getInterpolationMat(self.locations, projGLoc) if self.storeProjections: self._Ps[(mesh, projGLoc)] = P return P def eval(self, **kwargs): raise NotImplementedError( "the eval method for {} has not been implemented".format(self) ) def evalDeriv(self, **kwargs): raise NotImplementedError( "the evalDeriv method for {} has not been implemented".format(self) )
class Point3DTipper(BaseRxNSEM_Point): """ Natural source 3D tipper receiver base class :param numpy.ndarray locs: receiver locations (ie. :code:`np.r_[x,y,z]`) :param string orientation: receiver orientation 'x', 'y' or 'z' :param string component: real or imaginary component 'real' or 'imag' """ orientation = properties.StringChoice( "orientation of the receiver. Must currently be 'zx', 'zy'", ["zx", "zy"]) def __init__(self, locs, orientation="zx", component="real"): super().__init__(locs, orientation=orientation, component=component) def eval(self, src, mesh, f, return_complex=False): """ Project the fields to natural source data. :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: The source of the fields to project :param discretize.TensorMesh mesh: Mesh defining the topology of the problem :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM f: Natural source fields object to project :rtype: numpy.ndarray :return: Evaluated component of the impedance data """ # NOTE: Maybe set this as a property self.src = src self.mesh = mesh self.f = f if "zx" in self.orientation: Tij = -self._hy_px * self._hz_py + self._hy_py * self._hz_px if "zy" in self.orientation: Tij = self._hx_px * self._hz_py - self._hx_py * self._hz_px rx_eval_complex = self._Hd * Tij # Return the full impedance if return_complex: return rx_eval_complex return getattr(rx_eval_complex, self.component) def evalDeriv(self, src, mesh, f, v, adjoint=False): """ The derivative of the projection wrt u :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: NSEM source :param discretize.TensorMesh mesh: Mesh defining the topology of the problem :param SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM f: NSEM fields object of the source :param numpy.ndarray v: Random vector of size :rtype: numpy.ndarray :return: Calculated derivative (nD,) (adjoint=False) and (nP,2) (adjoint=True) for both polarizations """ self.src = src self.mesh = mesh self.f = f if adjoint: if "zx" in self.orientation: Tij = self._sDiag(self._aHd * (-self._sDiag(self._ahz_py) * self._ahy_px + self._sDiag(self._ahz_px) * self._ahy_py)) def TijN_uV(x): return (-self._ahz_py_u(self._sDiag(self._ahy_px) * x) - self._ahy_px_u(self._sDiag(self._ahz_py) * x) + self._ahy_py_u(self._sDiag(self._ahz_px) * x) + self._ahz_px_u(self._sDiag(self._ahy_py) * x)) elif "zy" in self.orientation: Tij = self._sDiag(self._aHd * (self._sDiag(self._ahz_py) * self._ahx_px - self._sDiag(self._ahz_px) * self._ahx_py)) def TijN_uV(x): return (self._ahx_px_u(self._sDiag(self._ahz_py) * x) + self._ahz_py_u(self._sDiag(self._ahx_px) * x) - self._ahx_py_u(self._sDiag(self._ahz_px) * x) - self._ahz_px_u(self._sDiag(self._ahx_py) * x)) # Calculate the complex derivative rx_deriv_real = TijN_uV(self._aHd * v) - self._aHd_uV( Tij.T * self._aHd * v) # NOTE: Need to reshape the output to go from 2*nU array to a (nU,2) matrix for each polarization # rx_deriv_real = np.hstack((mkvc(rx_deriv_real[:len(rx_deriv_real)/2],2),mkvc(rx_deriv_real[len(rx_deriv_real)/2::],2))) rx_deriv_real = rx_deriv_real.reshape((2, self.mesh.nE)).T # Extract the data if self.component == "imag": rx_deriv_component = 1j * rx_deriv_real elif self.component == "real": rx_deriv_component = rx_deriv_real.astype(complex) else: if "zx" in self.orientation: TijN_uV = (-self._sDiag(self._hy_px) * self._hz_py_u(v) - self._sDiag(self._hz_py) * self._hy_px_u(v) + self._sDiag(self._hy_py) * self._hz_px_u(v) + self._sDiag(self._hz_px) * self._hy_py_u(v)) elif "zy" in self.orientation: TijN_uV = (self._sDiag(self._hz_py) * self._hx_px_u(v) + self._sDiag(self._hx_px) * self._hz_py_u(v) - self._sDiag(self._hx_py) * self._hz_px_u(v) - self._sDiag(self._hz_px) * self._hx_py_u(v)) Tij = self.eval(src, mesh, f, True) # Calculate the complex derivative rx_deriv_complex = self._Hd * (TijN_uV - self._sDiag(Tij) * self._Hd_uV(v)) rx_deriv_component = np.array( getattr(rx_deriv_complex, self.component)) return rx_deriv_component
class BaseIPSimulation(BaseDCSimulation): eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability") _data_type = properties.StringChoice( "IP data type", default="volt", choices=["volt", "apparent_chargeability"], ) data_type = deprecate_property( _data_type, "data_type", new_name="receiver.data_type", removal_version="0.17.0", future_warn=True, ) Ainv = None _f = None _Jmatrix = None gtgdiag = None _sign = None _pred = None _scale = None def fields(self, m=None): if m is not None: self.model = m # sensitivity matrix is fixed # self._Jmatrix = None if self._f is None: if self.verbose is True: print(">> Solve DC problem") self._f = super().fields(m=None) if self._scale is None: scale = Data(self.survey, np.full(self.survey.nD, self._sign)) # loop through receievers to check if they need to set the _dc_voltage for src in self.survey.source_list: for rx in src.receiver_list: if ( rx.data_type == "apparent_chargeability" or self._data_type == "apparent_chargeability" ): scale[src, rx] = self._sign / rx.eval(src, self.mesh, self._f) self._scale = scale.dobs if self.verbose is True: print(">> Compute predicted data") self._pred = self.forward(m, f=self._f) # if not self.storeJ: # self.Ainv.clean() return self._f def dpred(self, m=None, f=None): """ Predicted data. .. math:: d_\\text{pred} = Pf(m) """ if f is None: f = self.fields(m) return self._pred def getJtJdiag(self, m, W=None): """ Return the diagonal of JtJ """ if self.gtgdiag is None: J = self.getJ(m) if W is None: W = self._scale ** 2 else: W = (self._scale * W.diagonal()) ** 2 self.gtgdiag = np.einsum("i,ij,ij->j", W, J, J) return self.gtgdiag # @profile def Jvec(self, m, v, f=None): return self._scale * super().Jvec(m, v, f) def forward(self, m, f=None): return np.asarray(self.Jvec(m, m, f=f)) def Jtvec(self, m, v, f=None): return super().Jtvec(m, v * self._scale, f) def delete_these_for_sensitivity(self): del self._Jmatrix, self._MfRhoI, self._MeSigma @property def deleteTheseOnModelUpdate(self): toDelete = [] return toDelete @property def MfRhoDerivMat(self): """ Derivative of MfRho with respect to the model """ if getattr(self, "_MfRhoDerivMat", None) is None: drho_dlogrho = sdiag(self.rho) * self.etaDeriv self._MfRhoDerivMat = ( self.mesh.getFaceInnerProductDeriv(np.ones(self.mesh.nC))( np.ones(self.mesh.nF) ) * drho_dlogrho ) return self._MfRhoDerivMat def MfRhoIDeriv(self, u, v, adjoint=False): """ Derivative of :code:`MfRhoI` with respect to the model. """ dMfRhoI_dI = -self.MfRhoI ** 2 if self.storeInnerProduct: if adjoint: return self.MfRhoDerivMat.T * (sdiag(u) * (dMfRhoI_dI.T * v)) else: return dMfRhoI_dI * (sdiag(u) * (self.MfRhoDerivMat * v)) else: dMf_drho = self.mesh.getFaceInnerProductDeriv(self.rho)(u) drho_dlogrho = sdiag(self.rho) * self.etaDeriv if adjoint: return drho_dlogrho.T * (dMf_drho.T * (dMfRhoI_dI.T * v)) else: return dMfRhoI_dI * (dMf_drho * (drho_dlogrho * v)) @property def MeSigmaDerivMat(self): """ Derivative of MeSigma with respect to the model """ if getattr(self, "_MeSigmaDerivMat", None) is None: dsigma_dlogsigma = sdiag(self.sigma) * self.etaDeriv self._MeSigmaDerivMat = ( self.mesh.getEdgeInnerProductDeriv(np.ones(self.mesh.nC))( np.ones(self.mesh.nE) ) * dsigma_dlogsigma ) return self._MeSigmaDerivMat # TODO: This should take a vector def MeSigmaDeriv(self, u, v, adjoint=False): """ Derivative of MeSigma with respect to the model times a vector (u) """ if self.storeInnerProduct: if adjoint: return self.MeSigmaDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MeSigmaDerivMat * v) else: dsigma_dlogsigma = sdiag(self.sigma) * self.etaDeriv if adjoint: return dsigma_dlogsigma.T * ( self.mesh.getEdgeInnerProductDeriv(self.sigma)(u).T * v ) else: return self.mesh.getEdgeInnerProductDeriv(self.sigma)(u) * ( dsigma_dlogsigma * v )
class BaseIPSimulation2D(BaseDCSimulation2D): sigma = props.PhysicalProperty("Electrical conductivity (S/m)") rho = props.PhysicalProperty("Electrical resistivity (Ohm m)") props.Reciprocal(sigma, rho) eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") data_type = properties.StringChoice( "IP data type", default="volt", choices=["volt", "apparent_chargeability"], ) fieldsPair = Fields2D _Jmatrix = None _f = None sign = None _pred = None def fields(self, m): if self.verbose: print(">> Compute DC fields") if self._f is None: # re-uses the DC simulation's fields method self._f = super().fields(None) if self.verbose is True: print(">> Data type is apparaent chargeability") # call dpred function in 2D DC simulation # set data type as volt ... for DC simulation data_types = [] for src in self.survey.source_list: for rx in src.receiver_list: data_types.append(rx.data_type) rx.data_type = "volt" dc_voltage = super().dpred(m=[], f=self._f) dc_data = Data(self.survey, dc_voltage) icount = 0 for src in self.survey.source_list: for rx in src.receiver_list: rx.data_type = data_types[icount] rx._dc_voltage = dc_data[src, rx] rx._Ps = {} icount += 1 self._pred = self.forward(m, f=self._f) return self._f def dpred(self, m=None, f=None): """ Predicted data. .. math:: d_\\text{pred} = Pf(m) """ # return self.Jvec(m, m, f=f) if f is None: f = self.fields(m) return self._pred def Jvec(self, m, v, f=None): self.model = m J = self.getJ(m, f=f) Jv = J.dot(v) return self.sign * Jv def forward(self, m, f=None): return self.Jvec(m, m, f=f) def Jtvec(self, m, v, f=None): self.model = m J = self.getJ(m, f=f) Jtv = J.T.dot(v) return self.sign * Jtv @property def deleteTheseOnModelUpdate(self): toDelete = [] return toDelete @property def MeSigmaDerivMat(self): """ Derivative of MeSigma with respect to the model """ if getattr(self, "_MeSigmaDerivMat", None) is None: dsigma_dlogsigma = sdiag(self.sigma) * self.etaDeriv self._MeSigmaDerivMat = ( self.mesh.getEdgeInnerProductDeriv(np.ones(self.mesh.nC))( np.ones(self.mesh.nE) ) * dsigma_dlogsigma ) return self._MeSigmaDerivMat # TODO: This should take a vector def MeSigmaDeriv(self, u, v, adjoint=False): """ Derivative of MeSigma with respect to the model times a vector (u) """ if self.storeInnerProduct: if adjoint: return self.MeSigmaDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MeSigmaDerivMat * v) else: dsigma_dlogsigma = sdiag(self.sigma) * self.etaDeriv if adjoint: return dsigma_dlogsigma.T * ( self.mesh.getEdgeInnerProductDeriv(self.sigma)(u).T * v ) else: return self.mesh.getEdgeInnerProductDeriv(self.sigma)(u) * ( dsigma_dlogsigma * v ) @property def MccRhoiDerivMat(self): """ Derivative of MccRho with respect to the model """ if getattr(self, "_MccRhoiDerivMat", None) is None: rho = self.rho vol = self.mesh.vol drho_dlogrho = sdiag(rho) * self.etaDeriv self._MccRhoiDerivMat = sdiag(vol * (-1.0 / rho ** 2)) * drho_dlogrho return self._MccRhoiDerivMat def MccRhoiDeriv(self, u, v, adjoint=False): """ Derivative of :code:`MccRhoi` with respect to the model. """ if len(self.rho.shape) > 1: if self.rho.shape[1] > self.mesh.dim: raise NotImplementedError( "Full anisotropy is not implemented for MccRhoiDeriv." ) if self.storeInnerProduct: if adjoint: return self.MccRhoiDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MccRhoiDerivMat * v) else: vol = self.mesh.vol rho = self.rho drho_dlogrho = sdiag(rho) * self.etaDeriv if adjoint: return drho_dlogrho.T * (sdia(u * vol * (-1.0 / rho ** 2)) * v) else: return sdiag(u * vol * (-1.0 / rho ** 2)) * (drho_dlogrho * v) @property def MnSigmaDerivMat(self): """ Derivative of MnSigma with respect to the model """ if getattr(self, "_MnSigmaDerivMat", None) is None: sigma = self.sigma vol = self.mesh.vol dsigma_dlogsigma = sdiag(sigma) * self.etaDeriv self._MnSigmaDerivMat = self.mesh.aveN2CC.T * sdiag(vol) * dsigma_dlogsigma return self._MnSigmaDerivMat def MnSigmaDeriv(self, u, v, adjoint=False): """ Derivative of MnSigma with respect to the model times a vector (u) """ if self.storeInnerProduct: if adjoint: return self.MnSigmaDerivMat.T * (sdiag(u) * v) else: return sdiag(u) * (self.MnSigmaDerivMat * v) else: sigma = self.sigma vol = self.mesh.vol dsigma_dlogsigma = sdiag(sigma) * self.etaDeriv if adjoint: return dsigma_dlogsigma.T * (vol * (self.mesh.aveN2CC * (u * v))) else: dsig_dm_v = dsigma_dlogsigma * v return u * (self.mesh.aveN2CC.T * (vol * dsig_dm_v))
class Array(_BaseFile): """File resource for numeric array binary storage In addition to file metadata properties, Array instances have an :code:`array` attribute. This attribute can be set with a numeric list or a numpy array, and doing so will dynamically fill in the other properties. """ SUB_TYPE = 'array' dtype = properties.StringChoice( 'Data type of array', choices=ARRAY_DTYPES, ) shape = properties.List( 'Dimensions of the array', properties.Integer('', min=0), min_length=1, ) content_type = properties.StringChoice( 'Content type of the file', choices=['application/octet-stream'], default='application/octet-stream', ) @property def array(self): """Reference to underlying numpy array This attribute does not need to be set; however, doing so will set the associated metadata. """ return getattr(self, '_array', None) @array.setter def array(self, value): if not isinstance(value, (tuple, list, np.ndarray)): raise ValueError('Array must be numpy array, list, or tuple') self._array = np.array(value) kind = self._array.dtype.kind if kind not in NUMPY_DTYPES: raise ValueError('Invalid kind of array: {}'.format(kind)) sizes = NUMPY_DTYPES[kind] itemsize = self._array.dtype.itemsize for size in sizes: if itemsize <= size: itemsize = size break else: itemsize = sizes[-1] dtype = '<{}{}'.format(kind, itemsize) if kind == 'f': mask = ~np.isnan(self._array) close = np.allclose(self._array[mask], self._array.astype(dtype)[mask]) else: close = (self._array == self._array.astype(dtype)).all() if not close: raise ValueError( 'Converting array type {} to supported type {} failed'.format( self._array.dtype, dtype)) self._array = self._array.astype(dtype) self.dtype = dtype self.shape = list(self._array.shape) self.content_length = self._array.nbytes def __init__(self, array=None, **kwargs): """Array initialization includes the optional argument array By including this argument, assigning a numeric array to a :class:`properties.Instance` property will coerce the numeric array into an Array resource. """ super(Array, self).__init__(**kwargs) if array is not None: self.array = array @properties.validator('content_length') def _validate_content_length(self, change): """Ensure content_length matches specified shape and dtype content_length may be smaller than the size expected from shape/dtype, if the data is compressed. However, it cannot be larger. """ if not self.dtype or not self.shape: return length = np.dtype(ARRAY_DTYPES[self.dtype][0]).itemsize for dim in self.shape: length *= dim if change['value'] > length: raise properties.ValidationError( message='content_length is too large for given shape/dtype', reason='invalid', prop='content_length', instance=self, ) def is_1d(self): """Helper method to indicate if array is 1D""" if self.shape is None: return False return all([dim == 1 for dim in self.shape[1:]])
class Survey(BaseSurvey): """ Base DC survey """ source_list = properties.List( "A list of sources for the survey", properties.Instance("A DC source", Src.BaseSrc), default=[], ) # Survey survey_geometry = properties.StringChoice( "Survey geometry of DC surveys", default="surface", choices=["surface", "borehole", "general"], ) survey_type = properties.StringChoice( "DC-IP Survey type", default="dipole-dipole", choices=["dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"], ) def __init__(self, source_list, **kwargs): super(Survey, self).__init__(source_list, **kwargs) def __repr__(self): return (f"{self.__class__.__name__}({self.survey_type}; " f"#sources: {self.nSrc}; #data: {self.nD})") @property def locations_a(self): """ Location of the positive (+) current electrodes for each datum """ if getattr(self, "_locations_a", None) is None: self._set_abmn_locations() return self._locations_a @property def locations_b(self): """ Location of the negative (-) current electrodes for each datum """ if getattr(self, "_locations_b", None) is None: self._set_abmn_locations() return self._locations_b @property def locations_m(self): """ Location of the positive (+) potential electrodes for each datum """ if getattr(self, "_locations_m", None) is None: self._set_abmn_locations() return self._locations_m @property def locations_n(self): """ Location of the negative (-) potential electrodes for each datum """ if getattr(self, "_locations_n", None) is None: self._set_abmn_locations() return self._locations_n a_locations = deprecate_property( locations_a, "a_locations", new_name="locations_a", removal_version="0.16.0", future_warn=True, ) b_locations = deprecate_property( locations_b, "b_locations", new_name="locations_b", removal_version="0.16.0", future_warn=True, ) m_locations = deprecate_property( locations_m, "m_locations", new_name="locations_m", removal_version="0.16.0", future_warn=True, ) n_locations = deprecate_property( locations_n, "n_locations", new_name="locations_n", removal_version="0.16.0", future_warn=True, ) @property def unique_electrode_locations(self): """ Unique locations of the A, B, M, N electrodes """ loc_a = self.locations_a loc_b = self.locations_b loc_m = self.locations_m loc_n = self.locations_n return np.unique(np.vstack((loc_a, loc_b, loc_m, loc_n)), axis=0) electrode_locations = deprecate_property( unique_electrode_locations, "electrode_locations", new_name="unique_electrode_locations", removal_version="0.16.0", ) @property def source_locations(self): """ Returns, in order, the source locations for all sources in the survey. Input: :param self: SimPEG.electromagnetics.static.resistivity.Survey Output: :return source_locations: List of np.ndarray containing the A and B electrode locations. """ src_a = [] src_b = [] for src in self.source_list: src_a.append(src.location_a) src_b.append(src.location_b) return [np.vstack(src_a), np.vstack(src_b)] def set_geometric_factor( self, space_type="half-space", data_type=None, survey_type=None, ): if data_type is not None: warnings.warn( "The data_type kwarg is deprecated, please set the data_type on the " "receiver object itself. This behavoir will be removed in SimPEG " "0.16.0", DeprecationWarning, ) if survey_type is not None: warnings.warn( "The survey_type parameter is no longer needed, and it will be removed " "in SimPEG 0.16.0.", FutureWarning, ) geometric_factor = static_utils.geometric_factor(self, space_type=space_type) geometric_factor = data.Data(self, geometric_factor) for source in self.source_list: for rx in source.receiver_list: if data_type is not None: rx.data_type = data_type if rx.data_type == "apparent_resistivity": rx._geometric_factor[source] = geometric_factor[source, rx] return geometric_factor def _set_abmn_locations(self): locations_a = [] locations_b = [] locations_m = [] locations_n = [] for source in self.source_list: for rx in source.receiver_list: nRx = rx.nD # Pole Source if isinstance(source, Src.Pole): locations_a.append( source.location.reshape([1, -1]).repeat(nRx, axis=0)) locations_b.append( source.location.reshape([1, -1]).repeat(nRx, axis=0)) # Dipole Source elif isinstance(source, Src.Dipole): locations_a.append(source.location[0].reshape( [1, -1]).repeat(nRx, axis=0)) locations_b.append(source.location[1].reshape( [1, -1]).repeat(nRx, axis=0)) # Pole RX if isinstance(rx, Rx.Pole): locations_m.append(rx.locations) locations_n.append(rx.locations) # Dipole RX elif isinstance(rx, Rx.Dipole): locations_m.append(rx.locations[0]) locations_n.append(rx.locations[1]) self._locations_a = np.vstack(locations_a) self._locations_b = np.vstack(locations_b) self._locations_m = np.vstack(locations_m) self._locations_n = np.vstack(locations_n) def getABMN_locations(self): warnings.warn( "The getABMN_locations method has been deprecated. Please instead " "ask for the property of interest: survey.locations_a, " "survey.locations_b, survey.locations_m, or survey.locations_n. " "This will be removed in version 0.16.0 of SimPEG", FutureWarning, ) def drape_electrodes_on_topography(self, mesh, actind, option="top", topography=None, force=False): """Shift electrode locations to be on [top] of the active cells.""" if self.survey_geometry == "surface": loc_a = self.locations_a loc_b = self.locations_b loc_m = self.locations_m loc_n = self.locations_n unique_electrodes, inv = np.unique(np.vstack( (loc_a, loc_b, loc_m, loc_n)), return_inverse=True, axis=0) inv_a, inv = inv[:len(loc_a)], inv[len(loc_a):] inv_b, inv = inv[:len(loc_b)], inv[len(loc_b):] inv_m, inv_n = inv[:len(loc_m)], inv[len(loc_m):] electrodes_shifted = drapeTopotoLoc(mesh, unique_electrodes, actind=actind, option=option) a_shifted = electrodes_shifted[inv_a] b_shifted = electrodes_shifted[inv_b] m_shifted = electrodes_shifted[inv_m] n_shifted = electrodes_shifted[inv_n] # These should all be in the same order as the survey datas ind = 0 for src in self.source_list: a_loc, b_loc = a_shifted[ind], b_shifted[ind] if isinstance(src, Src.Pole): src.location = a_loc else: src.location = [a_loc, b_loc] for rx in src.receiver_list: end = ind + rx.nD m_locs, n_locs = m_shifted[ind:end], n_shifted[ind:end] if isinstance(rx, Rx.Pole): rx.locations = m_locs else: rx.locations = [m_locs, n_locs] ind = end self._locations_a = a_shifted self._locations_b = b_shifted self._locations_m = m_shifted self._locations_n = n_shifted elif self.survey_geometry == "borehole": raise Exception("Not implemented yet for borehole survey_geometry") else: raise Exception( "Input valid survey survey_geometry: surface or borehole") def drapeTopo(self, *args, **kwargs): warnings.warn( "The drapeTopo method has been deprecated. Please instead " "use the drape_electrodes_on_topography method. " "This will be removed in version 0.16.0 of SimPEG", FutureWarning, ) self.drape_electrodes_on_topography(*args, **kwargs)
class DataBasic(_BaseData): """Basic numeric attribute data This data type is defined by an array that directly maps to an element geometry. For no-data values, use NaN. Currently, this only accepts 1D arrays and, when applied to a grid, the array must be stored in row-major order. """ BASE_TYPE = 'data' SUB_TYPE = 'basic' array = Pointer( 'Array of numeric values at locations on an element geometry, ' 'specified by the location property', Array, ) location = properties.StringChoice( 'Location of the data on geometry', choices={ 'nodes': ['N', 'node', 'vertices', 'corners'], 'cells': ['CC', 'cell', 'segments', 'faces', 'blocks'], }) mappings = properties.List( 'Mappings associated with the data', prop=properties.Union( '', props=[ Pointer('', MappingContinuous), Pointer('', MappingDiscrete), ], ), max_length=100, default=list, ) @properties.validator('array') def _validate_array_1d(self, change): """Ensure the array is 1D""" if (isinstance(change['value'], string_types) or change['value'] is properties.undefined): return if not change['value'].is_1d(): raise properties.ValidationError( message='{} must use 1D array'.format( self.__class__.__name__, ), reason='invalid', prop='array', instance=self, ) def to_omf(self, cell_location): self.validate() if self.location == 'nodes': location = 'vertices' else: location = cell_location omf_data = omf.ScalarData( name=self.name or '', description=self.description or '', location=location, array=self.array.array, ) return omf_data
class SimulationNDCellCentered(BaseTimeSimulation): """Richards Simulation""" hydraulic_conductivity = properties.Instance( "hydraulic conductivity function", BaseHydraulicConductivity) water_retention = properties.Instance("water retention curve", BaseWaterRetention) # TODO: This can also be a function(time, u_ii) boundary_conditions = properties.Array("boundary conditions") initial_conditions = properties.Array("initial conditions") debug = properties.Bool("Show all messages", default=False) method = properties.StringChoice( "Formulation used, See notes in Celia et al., 1990", default="mixed", choices=["mixed", "head"], ) do_newton = properties.Bool("Do a Newton iteration vs. a Picard iteration", default=False) root_finder_max_iter = properties.Integer( "Maximum iterations for root_finder iteration", default=30) root_finder_tol = properties.Float("tolerance of the root_finder", default=1e-4) @properties.observer("model") def _on_model_change(self, change): """Update the nested model functions when the model of the problem changes. Specifically :code:`hydraulic_conductivity` and :code:`water_retention` models are updated iff they have mappings. """ if (not self.hydraulic_conductivity.needs_model and not self.water_retention.needs_model): warnings.warn("There is no model to set.") return model = change["value"] if self.hydraulic_conductivity.needs_model: self.hydraulic_conductivity.model = model if self.water_retention.needs_model: self.water_retention.model = model def getBoundaryConditions(self, ii, u_ii): if isinstance(self.boundary_conditions, np.ndarray): return self.boundary_conditions time = self.time_mesh.vectorCCx[ii] return self.boundary_conditions(time, u_ii) @properties.observer( ["do_newton", "root_finder_max_iter", "root_finder_tol"]) def _on_root_finder_update(self, change): """Setting do_newton etc. will clear the root_finder, which will be reinitialized when called """ if hasattr(self, "_root_finder"): del self._root_finder @property def root_finder(self): """Root-finding Algorithm""" if getattr(self, "_root_finder", None) is None: self._root_finder = optimization.NewtonRoot( doLS=self.do_newton, maxIter=self.root_finder_max_iter, tol=self.root_finder_tol, Solver=self.solver, ) return self._root_finder @utils.timeIt def fields(self, m=None): if self.water_retention.needs_model or self.hydraulic_conductivity.needs_model: assert m is not None else: assert m is None tic = time.time() u = list(range(self.nT + 1)) u[0] = self.initial_conditions for ii, dt in enumerate(self.time_steps): bc = self.getBoundaryConditions(ii, u[ii]) u[ii + 1] = self.root_finder.root( lambda hn1m, return_g=True: self.getResidual( m, u[ii], hn1m, dt, bc, return_g=return_g), u[ii], ) if self.debug: print("Solving Fields ({0:4d}/{1:d} - {2:3.1f}% Done) {3:d} " "Iterations, {4:4.2f} seconds".format( ii + 1, self.nT, 100.0 * (ii + 1) / self.nT, self.root_finder.iter, time.time() - tic, )) return u def dpred(self, m, f=None): """Create the projected data from a model. The field, f, (if provided) will be used for the predicted data instead of recalculating the fields (which may be expensive!). .. math:: d_\\text{pred} = P(f(m), m) Where P is a projection of the fields onto the data space. """ if f is None: f = self.fields(m) Ds = list(range(len(self.survey.receiver_list))) for ii, rx in enumerate(self.survey.receiver_list): Ds[ii] = rx(f, self) return np.concatenate(Ds) @property def Dz(self): if self.mesh.dim == 1: return self.mesh.faceDivx if self.mesh.dim == 2: mats = (utils.spzeros(self.mesh.nC, self.mesh.vnF[0]), self.mesh.faceDivy) elif self.mesh.dim == 3: mats = ( utils.spzeros(self.mesh.nC, self.mesh.vnF[0] + self.mesh.vnF[1]), self.mesh.faceDivz, ) return sp.hstack(mats, format="csr") @utils.timeIt def diagsJacobian(self, m, hn, hn1, dt, bc): """Diagonals and rhs of the jacobian system The matrix that we are computing has the form:: .- -. .- -. .- -. | Adiag | | h1 | | b1 | | Asub Adiag | | h2 | | b2 | | Asub Adiag | | h3 | = | b3 | | ... ... | | .. | | .. | | Asub Adiag | | hn | | bn | '- -' '- -' '- -' """ if m is not None: self.model = m DIV = self.mesh.faceDiv GRAD = self.mesh.cellGrad BC = self.mesh.cellGradBC AV = self.mesh.aveF2CC.T Dz = self.Dz dT = self.water_retention.derivU(hn) dT1 = self.water_retention.derivU(hn1) dTm = self.water_retention.derivM(hn) dTm1 = self.water_retention.derivM(hn1) K1 = self.hydraulic_conductivity(hn1) dK1 = self.hydraulic_conductivity.derivU(hn1) dKm1 = self.hydraulic_conductivity.derivM(hn1) # Compute part of the derivative of: # # DIV*diag(GRAD*hn1+BC*bc)*(AV*(1.0/K))^-1 DdiagGh1 = DIV * utils.sdiag(GRAD * hn1 + BC * bc) diagAVk2_AVdiagK2 = (utils.sdiag( (AV * (1.0 / K1))**(-2)) * AV * utils.sdiag(K1**(-2))) Asub = (-1.0 / dt) * dT Adiag = ((1.0 / dt) * dT1 - DdiagGh1 * diagAVk2_AVdiagK2 * dK1 - DIV * utils.sdiag(1.0 / (AV * (1.0 / K1))) * GRAD - Dz * diagAVk2_AVdiagK2 * dK1) B = (DdiagGh1 * diagAVk2_AVdiagK2 * dKm1 + Dz * diagAVk2_AVdiagK2 * dKm1 + (1.0 / dt) * (dTm - dTm1)) return Asub, Adiag, B @utils.timeIt def getResidual(self, m, hn, h, dt, bc, return_g=True): """Used by the root finder when going between timesteps Where h is the proposed value for the next time iterate (h_{n+1}) """ if m is not None: self.model = m DIV = self.mesh.faceDiv GRAD = self.mesh.cellGrad BC = self.mesh.cellGradBC AV = self.mesh.aveF2CC.T Dz = self.Dz T = self.water_retention(h) dT = self.water_retention.derivU(h) Tn = self.water_retention(hn) K = self.hydraulic_conductivity(h) dK = self.hydraulic_conductivity.derivU(h) aveK = 1.0 / (AV * (1.0 / K)) RHS = DIV * utils.sdiag(aveK) * (GRAD * h + BC * bc) + Dz * aveK if self.method == "mixed": r = (T - Tn) / dt - RHS elif self.method == "head": r = dT * (h - hn) / dt - RHS if not return_g: return r J = dT / dt - DIV * utils.sdiag(aveK) * GRAD if self.do_newton: DDharmAve = utils.sdiag(aveK**2) * AV * utils.sdiag(K**(-2)) * dK J = J - DIV * utils.sdiag(GRAD * h + BC * bc) * DDharmAve - Dz * DDharmAve return r, J @utils.timeIt def Jfull(self, m=None, f=None): if f is None: f = self.fields(m) nn = len(f) - 1 Asubs, Adiags, Bs = list(range(nn)), list(range(nn)), list(range(nn)) for ii in range(nn): dt = self.time_steps[ii] bc = self.getBoundaryConditions(ii, f[ii]) Asubs[ii], Adiags[ii], Bs[ii] = self.diagsJacobian( m, f[ii], f[ii + 1], dt, bc) Ad = sp.block_diag(Adiags) zRight = utils.spzeros((len(Asubs) - 1) * Asubs[0].shape[0], Adiags[0].shape[1]) zTop = utils.spzeros(Adiags[0].shape[0], len(Adiags) * Adiags[0].shape[1]) As = sp.vstack((zTop, sp.hstack((sp.block_diag(Asubs[1:]), zRight)))) A = As + Ad B = np.array(sp.vstack(Bs).todense()) Ainv = self.solver(A, **self.solver_opts) AinvB = Ainv * B z = np.zeros((self.mesh.nC, B.shape[1])) du_dm = np.vstack((z, AinvB)) J = self.survey.deriv(self, f, du_dm_v=du_dm) # not multiplied by v return J @utils.timeIt def Jvec(self, m, v, f=None): if f is None: f = self.fields(m) JvC = list(range(len(f) - 1)) # Cell to hold each row of the long vector # This is done via forward substitution. bc = self.getBoundaryConditions(0, f[0]) temp, Adiag, B = self.diagsJacobian(m, f[0], f[1], self.time_steps[0], bc) Adiaginv = self.solver(Adiag, **self.solver_opts) JvC[0] = Adiaginv * (B * v) for ii in range(1, len(f) - 1): bc = self.getBoundaryConditions(ii, f[ii]) Asub, Adiag, B = self.diagsJacobian(m, f[ii], f[ii + 1], self.time_steps[ii], bc) Adiaginv = self.solver(Adiag, **self.solver_opts) JvC[ii] = Adiaginv * (B * v - Asub * JvC[ii - 1]) du_dm_v = np.concatenate([np.zeros(self.mesh.nC)] + JvC) Jv = self.survey.deriv(self, f, du_dm_v=du_dm_v, v=v) return Jv @utils.timeIt def Jtvec(self, m, v, f=None): if f is None: f = self.field(m) PTv, PTdv = self.survey.derivAdjoint(self, f, v=v) # This is done via backward substitution. minus = 0 BJtv = 0 for ii in range(len(f) - 1, 0, -1): bc = self.getBoundaryConditions(ii - 1, f[ii - 1]) Asub, Adiag, B = self.diagsJacobian(m, f[ii - 1], f[ii], self.time_steps[ii - 1], bc) # select the correct part of v vpart = list( range((ii) * Adiag.shape[0], (ii + 1) * Adiag.shape[0])) AdiaginvT = self.solver(Adiag.T, **self.solver_opts) JTvC = AdiaginvT * (PTv[vpart] - minus) minus = Asub.T * JTvC # this is now the super diagonal. BJtv = BJtv + B.T * JTvC return BJtv + PTdv
class Point(BaseRx): """Point receiver""" times = properties.Array("Observation times", dtype=float) fieldType = properties.StringChoice( "Field type", choices=["h", "b", "dhdt", "dbdt"] ) orientation = properties.StringChoice( "Component of response", choices=["x", "y", "z"] ) def __init__(self, locations=None, **kwargs): if locations.shape[1] != 3: raise ValueError( "Rx locations (xi,yi,zi) must be np.array(N,3) where N is the number of stations" ) super(Point, self).__init__(locations, **kwargs) @property def n_times(self): """Number of measurements times.""" if self.times is not None: return len(self.times) nTimes = deprecate_property( n_times, "nTimes", new_name="n_times", removal_version="0.16.0", future_warn=True, ) @property def n_locations(self): """Number of locations.""" return self.locations.shape[0] nLocs = deprecate_property( n_locations, "nLocs", new_name="n_locations", removal_version="0.16.0", future_warn=True, ) @property def nD(self): """Number of data in the receiver.""" if self.times is not None: return self.locations.shape[0] * len(self.times) fieldComp = deprecate_property( orientation, "fieldComp", new_name="orientation", removal_version="0.16.0", future_warn=True, )
class BaseTDEMSrc(BaseEMSrc): # rxPair = Rx waveformPair = BaseWaveform #: type of waveform to pair with waveform = None #: source waveform srcType = properties.StringChoice( "is the source a galvanic of inductive source", choices=["inductive", "galvanic"], ) def __init__(self, rxList, **kwargs): super(BaseTDEMSrc, self).__init__(rxList, **kwargs) @property def waveform(self): "A waveform instance is not None" return getattr(self, '_waveform', None) @waveform.setter def waveform(self, val): if self.waveform is None: val._assertMatchesPair(self.waveformPair) self._waveform = val else: self._waveform = self.StepOffWaveform(val) def __init__(self, rxList, waveform=StepOffWaveform(), **kwargs): self.waveform = waveform BaseEMSrc.__init__(self, rxList, **kwargs) def bInitial(self, prob): return Zero() def bInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def eInitial(self, prob): return Zero() def eInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def hInitial(self, prob): return Zero() def hInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def jInitial(self, prob): return Zero() def jInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def eval(self, prob, time): s_m = self.s_m(prob, time) s_e = self.s_e(prob, time) return s_m, s_e def evalDeriv(self, prob, time, v=None, adjoint=False): if v is not None: return (self.s_mDeriv(prob, time, v, adjoint), self.s_eDeriv(prob, time, v, adjoint)) else: return (lambda v: self.s_mDeriv(prob, time, v, adjoint), lambda v: self.s_eDeriv(prob, time, v, adjoint)) def s_m(self, prob, time): return Zero() def s_e(self, prob, time): return Zero() def s_mDeriv(self, prob, time, v=None, adjoint=False): return Zero() def s_eDeriv(self, prob, time, v=None, adjoint=False): return Zero()
class BaseTDEMSrc(BaseEMSrc): # rxPair = Rx waveform = properties.Instance( "A source waveform", BaseWaveform, default=StepOffWaveform() ) srcType = properties.StringChoice( "is the source a galvanic of inductive source", choices=["inductive", "galvanic"], ) def __init__(self, receiver_list=None, **kwargs): if receiver_list is not None: kwargs["receiver_list"] = receiver_list super(BaseTDEMSrc, self).__init__(**kwargs) def bInitial(self, prob): return Zero() def bInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def eInitial(self, prob): return Zero() def eInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def hInitial(self, prob): return Zero() def hInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def jInitial(self, prob): return Zero() def jInitialDeriv(self, prob, v=None, adjoint=False, f=None): return Zero() def eval(self, prob, time): s_m = self.s_m(prob, time) s_e = self.s_e(prob, time) return s_m, s_e def evalDeriv(self, prob, time, v=None, adjoint=False): if v is not None: return ( self.s_mDeriv(prob, time, v, adjoint), self.s_eDeriv(prob, time, v, adjoint), ) else: return ( lambda v: self.s_mDeriv(prob, time, v, adjoint), lambda v: self.s_eDeriv(prob, time, v, adjoint), ) def s_m(self, prob, time): return Zero() def s_e(self, prob, time): return Zero() def s_mDeriv(self, prob, time, v=None, adjoint=False): return Zero() def s_eDeriv(self, prob, time, v=None, adjoint=False): return Zero()